quodlibet-plugins-3.0.2/0000755000175000017500000000000012173213476015417 5ustar lazkalazka00000000000000quodlibet-plugins-3.0.2/gstreamer/0000755000175000017500000000000012173213476017410 5ustar lazkalazka00000000000000quodlibet-plugins-3.0.2/gstreamer/compressor.py0000644000175000017500000001010512173213426022146 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gst, Gtk, GObject from quodlibet.plugins.gstelement import GStreamerPlugin from quodlibet import qltk from quodlibet import config from quodlibet.util import gobject_weak _PLUGIN_ID = "compressor" _SETTINGS = { "threshold": [_("_Threshold:"), _("Threshold until the filter is activated"), 1.0], "ratio": [_("R_atio:"), _("Compression ratio"), 1.0], } def get_cfg(option): cfg_option = "%s_%s" % (_PLUGIN_ID, option) default = _SETTINGS[option][2] if option == "threshold": return config.getfloat("plugins", cfg_option, default) elif option == "ratio": return config.getfloat("plugins", cfg_option, default) def set_cfg(option, value): cfg_option = "%s_%s" % (_PLUGIN_ID, option) if get_cfg(option) != value: config.set("plugins", cfg_option, value) class Preferences(Gtk.VBox): __gsignals__ = { 'changed': (GObject.SignalFlags.RUN_LAST, None, tuple()), } def __init__(self): super(Preferences, self).__init__(spacing=12) table = Gtk.Table(2, 2) table.set_col_spacings(6) table.set_row_spacings(6) labels = {} for idx, key in enumerate(["threshold", "ratio"]): text, tooltip = _SETTINGS[key][:2] label = Gtk.Label(label=text) labels[key] = label label.set_tooltip_text(tooltip) label.set_alignment(0.0, 0.5) label.set_padding(0, 6) label.set_use_underline(True) table.attach(label, 0, 1, idx, idx + 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) threshold_scale = Gtk.HScale( adjustment=Gtk.Adjustment(0, 0, 1, 0.01, 0.1)) threshold_scale.set_digits(2) labels["threshold"].set_mnemonic_widget(threshold_scale) threshold_scale.set_value_pos(Gtk.PositionType.RIGHT) def format_perc(scale, value): return _("%d %%") % (value * 100) threshold_scale.connect('format-value', format_perc) table.attach(threshold_scale, 1, 2, 0, 1) def threshold_changed(scale): value = scale.get_value() set_cfg("threshold", value) self.emit("changed") threshold_scale.connect('value-changed', threshold_changed) threshold_scale.set_value(get_cfg("threshold")) ratio_scale = Gtk.HScale(adjustment=Gtk.Adjustment(0, 0, 1, 0.01, 0.1)) ratio_scale.set_digits(2) labels["ratio"].set_mnemonic_widget(ratio_scale) ratio_scale.set_value_pos(Gtk.PositionType.RIGHT) table.attach(ratio_scale, 1, 2, 1, 2) def ratio_changed(scale): value = scale.get_value() set_cfg("ratio", value) self.emit("changed") ratio_scale.connect('value-changed', ratio_changed) ratio_scale.set_value(get_cfg("ratio")) self.pack_start(qltk.Frame(_("Preferences"), child=table), True, True, 0) class Compressor(GStreamerPlugin): PLUGIN_ID = _PLUGIN_ID PLUGIN_NAME = _("Audio Compressor") PLUGIN_DESC = _("Change the amplitude of all samples above a specific " "threshold with a specific ratio.") PLUGIN_ICON = "audio-volume-high" @classmethod def setup_element(cls): return Gst.ElementFactory.make('audiodynamic', cls.PLUGIN_ID) @classmethod def update_element(cls, element): element.set_property("characteristics", "soft-knee") element.set_property("mode", "compressor") element.set_property("ratio", get_cfg("ratio")) element.set_property("threshold", get_cfg("threshold")) @classmethod def PluginPreferences(cls, window): prefs = Preferences() gobject_weak(prefs.connect, "changed", lambda *x: cls.queue_update()) return prefs quodlibet-plugins-3.0.2/gstreamer/karaoke.py0000644000175000017500000000734512173213426021403 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gst, Gtk, GObject from quodlibet.plugins.gstelement import GStreamerPlugin from quodlibet import qltk from quodlibet import config from quodlibet.util import gobject_weak _PLUGIN_ID = "karaoke" _SETTINGS = { "band": [_("Filter _band:"), _("The Frequency band of the filter"), 220.0], "width": [_("Filter _width:"), _("The Frequency width of the filter"), 100.0], "level": [_("_Level:"), _("Level of the effect"), 1.0], } def get_cfg(option): cfg_option = "%s_%s" % (_PLUGIN_ID, option) default = _SETTINGS[option][2] return config.getfloat("plugins", cfg_option, default) def set_cfg(option, value): cfg_option = "%s_%s" % (_PLUGIN_ID, option) if get_cfg(option) != value: config.set("plugins", cfg_option, value) class Preferences(Gtk.VBox): __gsignals__ = { 'changed': (GObject.SignalFlags.RUN_LAST, None, tuple()), } def __init__(self): super(Preferences, self).__init__(spacing=12) table = Gtk.Table(3, 2) table.set_col_spacings(6) table.set_row_spacings(6) labels = {} for idx, key in enumerate(["level", "band", "width"]): label = Gtk.Label(label=_SETTINGS[key][0]) labels[key] = label label.set_alignment(0.0, 0.5) label.set_padding(0, 6) label.set_tooltip_text(_SETTINGS[key][1]) label.set_use_underline(True) table.attach(label, 0, 1, idx, idx + 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) def scale_changed(scale, option): value = scale.get_value() set_cfg(option, value) self.emit("changed") max_values = [1.0, 441, 100] steps = [0.01, 10, 10] pages = [0.1, 50, 25] scales = {} for idx, key in enumerate(["level", "band", "width"]): max_value = max_values[idx] step = steps[idx] page = pages[idx] scale = Gtk.HScale( adjustment=Gtk.Adjustment(0, 0, max_value, step, page)) scales[key] = scale if step < 0.1: scale.set_digits(2) scale.add_mark(_SETTINGS[key][2], Gtk.PositionType.BOTTOM, None) labels[key].set_mnemonic_widget(scale) scale.set_value_pos(Gtk.PositionType.RIGHT) table.attach(scale, 1, 2, idx, idx + 1) scale.connect('value-changed', scale_changed, key) scale.set_value(get_cfg(key)) def format_perc(scale, value): return _("%d %%") % (value * 100) scales["level"].connect('format-value', format_perc) self.pack_start(qltk.Frame(_("Preferences"), child=table), True, True, 0) class Karaoke(GStreamerPlugin): PLUGIN_ID = _PLUGIN_ID PLUGIN_NAME = _("Karaoke") PLUGIN_DESC = _("Remove voice from audio.") PLUGIN_ICON = "audio-volume-high" @classmethod def setup_element(cls): return Gst.ElementFactory.make('audiokaraoke', cls.PLUGIN_ID) @classmethod def update_element(cls, element): element.set_property("level", get_cfg("level")) element.set_property("filter-band", get_cfg("band")) element.set_property("filter-width", get_cfg("width")) @classmethod def PluginPreferences(cls, window): prefs = Preferences() gobject_weak(prefs.connect, "changed", lambda *x: cls.queue_update()) return prefs quodlibet-plugins-3.0.2/gstreamer/mono.py0000644000175000017500000000147112173212464020731 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gst from quodlibet.plugins.gstelement import GStreamerPlugin class MonoDownmix(GStreamerPlugin): PLUGIN_ID = "mono" PLUGIN_NAME = _("Mono Downmix") PLUGIN_DESC = _("Downmix channels to mono.") PLUGIN_ICON = "audio-volume-high" priority = -1 @classmethod def setup_element(cls): element = Gst.ElementFactory.make('capsfilter', cls.PLUGIN_ID) if not element: return caps = Gst.Caps.from_string('audio/x-raw,channels=1') element.set_property('caps', caps) return element quodlibet-plugins-3.0.2/gstreamer/crossfeed.py0000644000175000017500000001326412173213426021740 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk, Gst, GObject from quodlibet.plugins.gstelement import GStreamerPlugin from quodlibet import qltk from quodlibet import config from quodlibet.util import gobject_weak _PLUGIN_ID = "crossfeed" _SETTINGS = { "preset": [_("_Preset:"), _("Filter preset")], "fcut": [_("_Frequency cut:"), _("Low-pass filter cut frequency")], "feed": [_("Feed _level:"), _("Feed level")], } _PRESETS = [ ["default", _("Default"), _("Closest to virtual speaker placement (30°, 3 meter)"), 700, 45], ["cmoy", _("Chu Moy"), _("Close to Chu Moy's crossfeeder (popular)"), 700, 60], ["jmeier", _("Jan Meier"), _("Close to Jan Meier's CORDA amplifiers (little change)"), 650, 90], ["custom", _("Custom"), _("Custom settings"), -1, -1], ] _CUSTOM_INDEX = 3 def get_cfg(option): cfg_option = "%s_%s" % (_PLUGIN_ID, option) if option == "feed": return config.getint("plugins", cfg_option, _PRESETS[0][4]) elif option == "fcut": return config.getint("plugins", cfg_option, _PRESETS[0][3]) def set_cfg(option, value): cfg_option = "%s_%s" % (_PLUGIN_ID, option) if get_cfg(option) != value: config.set("plugins", cfg_option, value) class Preferences(Gtk.VBox): __gsignals__ = { 'changed': (GObject.SignalFlags.RUN_LAST, None, tuple()), } def __init__(self): super(Preferences, self).__init__(spacing=12) table = Gtk.Table(3, 2) table.set_col_spacings(6) table.set_row_spacings(6) labels = {} for idx, key in enumerate(["preset", "fcut", "feed"]): text, tooltip = _SETTINGS[key] label = Gtk.Label(label=text) labels[key] = label label.set_tooltip_text(tooltip) label.set_alignment(0.0, 0.5) label.set_padding(0, 6) label.set_use_underline(True) table.attach(label, 0, 1, idx, idx + 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) preset_combo = Gtk.ComboBoxText() self.__combo = preset_combo labels["preset"].set_mnemonic_widget(preset_combo) for preset in _PRESETS: preset_combo.append_text(preset[1]) preset_combo.set_active(-1) table.attach(preset_combo, 1, 2, 0, 1) fcut_scale = Gtk.HScale( adjustment=Gtk.Adjustment(700, 300, 2000, 10, 100)) fcut_scale.set_tooltip_text(_SETTINGS["fcut"][1]) labels["fcut"].set_mnemonic_widget(fcut_scale) fcut_scale.set_value_pos(Gtk.PositionType.RIGHT) def format_hz(scale, value): return _("%d Hz") % value fcut_scale.connect('format-value', format_hz) table.attach(fcut_scale, 1, 2, 1, 2) def fcut_changed(scale): value = int(scale.get_value()) set_cfg("fcut", value) self.__update_combo() self.emit("changed") fcut_scale.connect('value-changed', fcut_changed) fcut_scale.set_value(get_cfg("fcut")) level_scale = Gtk.HScale(adjustment=Gtk.Adjustment(45, 10, 150, 1, 5)) level_scale.set_tooltip_text(_SETTINGS["feed"][1]) labels["feed"].set_mnemonic_widget(level_scale) level_scale.set_value_pos(Gtk.PositionType.RIGHT) def format_db(scale, value): return _("%.1f dB") % (value / 10.0) level_scale.connect('format-value', format_db) table.attach(level_scale, 1, 2, 2, 3) def level_changed(scale): value = int(scale.get_value()) set_cfg("feed", value) self.__update_combo() self.emit("changed") level_scale.connect('value-changed', level_changed) level_scale.set_value(get_cfg("feed")) def combo_change(combo, level_scale, fcut_scale): index = combo.get_active() if index == _CUSTOM_INDEX: combo.set_tooltip_text("") return tooltip, fcut, feed = _PRESETS[index][-3:] combo.set_tooltip_text(tooltip) level_scale.set_value(feed) fcut_scale.set_value(fcut) preset_combo.connect("changed", combo_change, level_scale, fcut_scale) self.__update_combo() self.pack_start(qltk.Frame(_("Preferences"), child=table), True, True, 0) def __update_combo(self): feed = get_cfg("feed") fcut = get_cfg("fcut") for i, preset in enumerate(_PRESETS): def_fcut, def_feed = preset[-2:] if def_fcut == fcut and def_feed == feed: self.__combo.set_active(i) return self.__combo.set_active(_CUSTOM_INDEX) class Crossfeed(GStreamerPlugin): PLUGIN_ID = _PLUGIN_ID PLUGIN_NAME = _("Crossfeed") PLUGIN_DESC = _("Mixes the left and right channel in a way that simulates" " a speaker setup while using headphones, or to adjust " "for early Stereo recordings.") PLUGIN_ICON = "audio-volume-high" @classmethod def setup_element(cls): return Gst.ElementFactory.make('crossfeed', cls.PLUGIN_ID) @classmethod def update_element(cls, element): element.set_property("feed", get_cfg("feed")) element.set_property("fcut", get_cfg("fcut")) @classmethod def PluginPreferences(cls, window): prefs = Preferences() gobject_weak(prefs.connect, "changed", lambda *x: cls.queue_update()) return prefs quodlibet-plugins-3.0.2/gstreamer/pitch.py0000644000175000017500000000632412173213426021071 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk, GObject, Gst from quodlibet.plugins.gstelement import GStreamerPlugin from quodlibet import qltk from quodlibet import config from quodlibet.util import gobject_weak _PLUGIN_ID = "pitch" _SETTINGS = { "rate": [_("R_ate:"), 1.0], "tempo": [_("_Tempo:"), 1.0], "pitch": [_("_Pitch:"), 1.0], } def get_cfg(option): cfg_option = "%s_%s" % (_PLUGIN_ID, option) default = _SETTINGS[option][1] if option == "rate": return config.getfloat("plugins", cfg_option, default) elif option == "tempo": return config.getfloat("plugins", cfg_option, default) elif option == "pitch": return config.getfloat("plugins", cfg_option, default) def set_cfg(option, value): cfg_option = "%s_%s" % (_PLUGIN_ID, option) if get_cfg(option) != value: config.set("plugins", cfg_option, value) class Preferences(Gtk.VBox): __gsignals__ = { 'changed': (GObject.SignalFlags.RUN_LAST, None, tuple()), } def __init__(self): super(Preferences, self).__init__(spacing=12) table = Gtk.Table(3, 2) table.set_col_spacings(6) table.set_row_spacings(6) labels = {} for idx, key in enumerate(["tempo", "rate", "pitch"]): label = Gtk.Label(label=_SETTINGS[key][0]) labels[key] = label label.set_alignment(0.0, 0.5) label.set_padding(0, 6) label.set_use_underline(True) table.attach(label, 0, 1, idx, idx + 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) def scale_changed(scale, option): value = scale.get_value() set_cfg(option, value) self.emit("changed") for idx, key in enumerate(["tempo", "rate", "pitch"]): scale = Gtk.HScale(adjustment=Gtk.Adjustment(0, 0.1, 3, 0.1, 1)) scale.set_digits(2) scale.add_mark(1.0, Gtk.PositionType.BOTTOM, None) labels[key].set_mnemonic_widget(scale) scale.set_value_pos(Gtk.PositionType.RIGHT) table.attach(scale, 1, 2, idx, idx + 1) scale.connect('value-changed', scale_changed, key) scale.set_value(get_cfg(key)) self.pack_start(qltk.Frame(_("Preferences"), child=table), True, True, 0) class Pitch(GStreamerPlugin): PLUGIN_ID = _PLUGIN_ID PLUGIN_NAME = _("Audio Pitch / Speed") PLUGIN_DESC = _("Control the pitch of an audio stream.") PLUGIN_ICON = "audio-volume-high" @classmethod def setup_element(cls): return Gst.ElementFactory.make('pitch', cls.PLUGIN_ID) @classmethod def update_element(cls, element): for key in ["tempo", "rate", "pitch"]: element.set_property(key, get_cfg(key)) @classmethod def PluginPreferences(cls, window): prefs = Preferences() gobject_weak(prefs.connect, "changed", lambda *x: cls.queue_update()) return prefs quodlibet-plugins-3.0.2/README0000644000175000017500000000150312161032160016260 0ustar lazkalazka00000000000000Plugins for Quod Libet and Ex Falso =================================== This is the set of plugins maintained by the Quod Libet project, designed for use with Quod Libet and Ex Falso. It is recommended that users install both the applications and the plugins from their distribution's package manager if possible. Those running the latest version from the project's Mercurial repository should use the plugins from the same revision. If you are installing Quod Libet and Ex Falso from source, you may use these plugins by copying the contents of this folder to ~/.quodlibet/plugins/ with the directory structure intact. Some plugins require additional dependencies to work properly. These missing dependencies are reported from within the application's plugin screen. See http://code.google.com/p/quodlibet/ for more information. quodlibet-plugins-3.0.2/playorder/0000755000175000017500000000000012173213476017420 5ustar lazkalazka00000000000000quodlibet-plugins-3.0.2/playorder/track_repeat.py0000644000175000017500000000537112173212464022440 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2011,2012 Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # Repeats a given track a configurable number of times # Useful for musicians practising / working out songs... # or maybe you just REALLY like your playlist. # # TODO: notification of play count? Non-shuffle? Integration with main UI? # from gi.repository import Gtk from quodlibet.plugins.playorder import PlayOrderPlugin, PlayOrderShuffleMixin from quodlibet.util.dprint import print_d from quodlibet.plugins import PluginConfigMixin class TrackRepeatOrder(PlayOrderPlugin, PlayOrderShuffleMixin, PluginConfigMixin): PLUGIN_ID = "track_repeat" PLUGIN_NAME = _("Track Repeat") PLUGIN_ICON = "gtk-refresh" PLUGIN_VERSION = "0.2" PLUGIN_DESC = _("Shuffle songs, " "but repeat every track a set number of times.") PLAY_EACH_DEFAULT = 2 # Plays of the current song play_count = 0 @classmethod def PluginPreferences(cls, parent): def plays_changed(spin): cls.config_set("play_each", int(spin.get_value())) vb = Gtk.VBox(spacing=10) vb.set_border_width(10) hbox = Gtk.HBox(spacing=6) val = cls.config_get("play_each", cls.PLAY_EACH_DEFAULT) spin = Gtk.SpinButton( adjustment=Gtk.Adjustment(float(val), 2, 20, 1, 10)) spin.connect("value-changed", plays_changed) hbox.pack_start(spin, False, True, 0) lbl = Gtk.Label(label=_("Number of times to play each song")) hbox.pack_start(lbl, False, True, 0) vb.pack_start(hbox, True, True, 0) vb.show_all() return vb def restart_counting(self): self.play_count = 0 print_d("Resetting play count", context=self) def next(self, playlist, iter): self.play_count += 1 play_each = int(self.config_get('play_each', self.PLAY_EACH_DEFAULT)) print_d("Play count now at %d/%d" % (self.play_count, play_each)) if self.play_count < play_each and iter is not None: return iter else: self.restart_counting() return super(TrackRepeatOrder, self).next(playlist, iter) def next_explicit(self, *args): self.restart_counting() return super(TrackRepeatOrder, self).next(*args) def previous(self, *args): return super(TrackRepeatOrder, self).previous(*args) def set(self, playlist, iter): self.restart_counting() return super(TrackRepeatOrder, self).set(playlist, iter) def reset(self, playlist): super(TrackRepeatOrder, self).reset(playlist) self.play_count = 0 quodlibet-plugins-3.0.2/playorder/reverse.py0000644000175000017500000000106712161032160021433 0ustar lazkalazka00000000000000from quodlibet.plugins.playorder import PlayOrderPlugin, PlayOrderInOrderMixin class ReverseOrder(PlayOrderPlugin, PlayOrderInOrderMixin): PLUGIN_ID = "reverse" PLUGIN_NAME = _("Reverse") PLUGIN_ICON = "gtk-refresh" PLUGIN_VERSION = "1" PLUGIN_DESC = ("A simple play order plugin that plays songs in " "reverse order.") def previous(self, playlist, iter): return super(ReverseOrder, self).next(playlist, iter) def next(self, playlist, iter): return super(ReverseOrder, self).previous(playlist, iter) quodlibet-plugins-3.0.2/playorder/follow.py0000644000175000017500000000323612161032160021262 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2010 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. from quodlibet.plugins.playorder import PlayOrderPlugin, \ PlayOrderRememberedMixin, PlayOrderInOrderMixin from quodlibet import app class FollowOrder(PlayOrderPlugin, PlayOrderRememberedMixin, PlayOrderInOrderMixin): PLUGIN_ID = "follow" PLUGIN_NAME = _("Follow Cursor") PLUGIN_ICON = "gtk-jump-to" PLUGIN_VERSION = "1" PLUGIN_DESC = ("Playback follows your selection.") __last_path = None def next(self, playlist, iter): next_fallback = PlayOrderInOrderMixin.next(self, playlist, iter) PlayOrderRememberedMixin.next(self, playlist, iter) selected = app.window.songlist.get_selected_songs() if not selected: return next_fallback selected_iter = playlist.find(selected[0]) selected_path = playlist.get_path(selected_iter) current_path = iter and playlist.get_path(iter) if selected_path in (current_path, self.__last_path): return next_fallback self.__last_path = selected_path return selected_iter def previous(self, *args): return super(FollowOrder, self).previous(*args) self.__last_path = None def set(self, playlist, iter): if iter: self.__last_path = playlist.get_path(iter) return super(FollowOrder, self).set(playlist, iter) def reset(self, playlist): super(FollowOrder, self).reset(playlist) self.__last_path = None quodlibet-plugins-3.0.2/playorder/queue.py0000644000175000017500000000164512161032160021106 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2009 Steven Robertson # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. from quodlibet.plugins.playorder import PlayOrderPlugin, PlayOrderInOrderMixin from quodlibet import app class QueueOrder(PlayOrderPlugin, PlayOrderInOrderMixin): PLUGIN_ID = "queue" PLUGIN_NAME = _("Queue Only") PLUGIN_ICON = "gtk-media-next" PLUGIN_VERSION = "1" PLUGIN_DESC = ("Only songs in the queue will be played. Double-click on " "any song to enqueue it.") def next(self, playlist, iter): return None def set_explicit(self, playlist, iter): if iter is None: return song = playlist[iter][0] if song is None: return app.window.playlist.enqueue([playlist[iter][0]]) quodlibet-plugins-3.0.2/events/0000755000175000017500000000000012173213476016723 5ustar lazkalazka00000000000000quodlibet-plugins-3.0.2/events/themeswitcher.py0000644000175000017500000000742612173212464022155 0ustar lazkalazka00000000000000# Copyright 2011,2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os from gi.repository import Gtk from quodlibet import qltk from quodlibet import config from quodlibet import const from quodlibet.qltk.ccb import ConfigCheckButton from quodlibet.plugins.events import EventPlugin class ThemeSwitcher(EventPlugin): PLUGIN_ID = "Theme Switcher" PLUGIN_NAME = _("Theme Switcher") PLUGIN_DESC = ("Change the active GTK+ theme.") __enabled = False CONFIG_THEME = PLUGIN_ID + "_theme" CONFIG_DARK = PLUGIN_ID + "_prefer_dark" def PluginPreferences(self, *args): hb = Gtk.HBox(spacing=6) label = Gtk.Label(label=_("_Theme:")) combo = Gtk.ComboBoxText() theme = config.get("plugins", self.CONFIG_THEME, None) combo.append_text(_("Default Theme")) themes = self.__get_themes() select = 0 for i, name in enumerate(sorted(themes)): combo.append_text(name) if name == theme: select = i + 1 combo.set_active(select) combo.connect('changed', self.__changed) dark_button = ConfigCheckButton( _("Prefer dark theme version"), "plugins", self.CONFIG_DARK) dark_button.set_active( config.getboolean("plugins", self.CONFIG_DARK, False)) def dark_cb(button): self.__set_dark(button.get_active()) dark_button.connect('toggled', dark_cb) label.set_mnemonic_widget(combo) label.set_use_underline(True) hb.pack_start(label, False, True, 0) hb.pack_start(combo, False, True, 0) vbox = Gtk.VBox(spacing=6) vbox.pack_start(hb, False, True, 0) vbox.pack_start(dark_button, False, True, 0) return qltk.Frame(_("Preferences"), child=vbox) def __changed(self, combo): index = combo.get_active() name = (index and combo.get_active_text()) or "" config.set("plugins", self.CONFIG_THEME, name) self.__set_theme(name) def __get_themes(self): theme_dirs = [Gtk.rc_get_theme_dir(), os.path.join(const.HOME, ".themes")] themes = set() for theme_dir in theme_dirs: try: subdirs = os.listdir(theme_dir) except OSError: continue for dir_ in subdirs: gtk_dir = os.path.join(theme_dir, dir_, "gtk-3.0") if os.path.isdir(gtk_dir): themes.add(dir_) return themes def __set_theme(self, name): if not self.__enabled: return settings = Gtk.Settings.get_default() themes = self.__get_themes() name = ((name in themes) and name) or self.__default_theme settings.set_property('gtk-theme-name', name) def __set_dark(self, value): if not self.__enabled: return settings = Gtk.Settings.get_default() if value is None: value = self.__default_dark settings.set_property('gtk-application-prefer-dark-theme', value) def enabled(self): self.__enabled = True settings = Gtk.Settings.get_default() self.__default_theme = settings.get_property('gtk-theme-name') self.__default_dark = settings.get_property( 'gtk-application-prefer-dark-theme') theme = config.get("plugins", self.CONFIG_THEME, None) self.__set_theme(theme) is_dark = config.getboolean("plugins", self.CONFIG_DARK, False) self.__set_dark(is_dark) def disabled(self): self.__set_theme(None) self.__set_dark(None) self.__enabled = False quodlibet-plugins-3.0.2/events/trayicon.py0000644000175000017500000004031512173213426021123 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2004-2006 Joe Wreschnig, Michael Urman, Iñigo Serna # 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import sys from gi.repository import Gtk, Pango, Gdk, GdkPixbuf, GLib from quodlibet import browsers, config, qltk, util, app from quodlibet.parse import Pattern from quodlibet.plugins.events import EventPlugin from quodlibet.qltk.browser import LibraryBrowser from quodlibet.qltk.controls import StopAfterMenu from quodlibet.qltk.information import Information from quodlibet.qltk.playorder import ORDERS from quodlibet.qltk.properties import SongProperties from quodlibet.qltk.x import RadioMenuItem, SeparatorMenuItem from quodlibet.util.thumbnails import scale, calc_scale_size class Preferences(Gtk.VBox): """A small window to configure the tray icon's tooltip.""" def __init__(self, activator): super(Preferences, self).__init__(spacing=12) self.set_border_width(6) combo = Gtk.ComboBoxText() combo.append_text(_("Scroll wheel adjusts volume\n" "Shift and scroll wheel changes song")) combo.append_text(_("Scroll wheel changes song\n" "Shift and scroll wheel adjusts volume")) combo.set_active(int( config.getboolean("plugins", "icon_modifier_swap", False))) combo.connect('changed', self.__changed_combo) self.pack_start(qltk.Frame(_("Scroll _Wheel"), child=combo), True, True, 0) box = Gtk.VBox(spacing=12) table = Gtk.Table(2, 4) table.set_row_spacings(6) table.set_col_spacings(12) cbs = [] for i, tag in enumerate([ "genre", "artist", "album", "discnumber", "part", "tracknumber", "title", "version"]): cb = Gtk.CheckButton(util.tag(tag)) cb.tag = tag cbs.append(cb) table.attach(cb, i % 3, i % 3 + 1, i // 3, i // 3 + 1) box.pack_start(table, True, True, 0) entry = Gtk.Entry() box.pack_start(entry, False, True, 0) preview = Gtk.Label() preview.set_ellipsize(Pango.EllipsizeMode.END) ev = Gtk.EventBox() ev.add(preview) box.pack_start(ev, False, True, 0) frame = qltk.Frame(_("Tooltip Display"), child=box) frame.get_label_widget().set_mnemonic_widget(entry) self.pack_start(frame, True, True, 0) for cb in cbs: cb.connect('toggled', self.__changed_cb, cbs, entry) entry.connect( 'changed', self.__changed_entry, cbs, preview) try: entry.set_text(config.get("plugins", "icon_tooltip")) except: entry.set_text( "|" ">") self.show_all() def __changed_combo(self, combo): config.set( "plugins", "icon_modifier_swap", str(bool(combo.get_active()))) def __changed_cb(self, cb, cbs, entry): text = "<%s>" % "~".join([c.tag for c in cbs if c.get_active()]) entry.set_text(text) def __changed_entry(self, entry, cbs, label): text = entry.get_text() if text[0:1] == "<" and text[-1:] == ">": parts = text[1:-1].split("~") for cb in cbs: if parts and parts[0] == cb.tag: parts.pop(0) if parts: for cb in cbs: cb.set_inconsistent(True) else: parts = text[1:-1].split("~") for cb in cbs: cb.set_inconsistent(False) cb.set_active(cb.tag in parts) else: for cb in cbs: cb.set_inconsistent(True) if app.player.info is None: text = _("Not playing") else: text = Pattern(entry.get_text()) % app.player.info label.set_text(text) label.get_parent().set_tooltip_text(text) config.set("plugins", "icon_tooltip", entry.get_text()) class TrayIcon(EventPlugin): __icon = None __pixbuf = None __pixbuf_paused = None __icon_theme = None __menu = None __size = -1 __w_sig_map = None __w_sig_del = None __theme_sig = None __stop_after = None __first_map = True __pattern = Pattern( "|" ">") PLUGIN_ID = "Tray Icon" PLUGIN_NAME = _("Tray Icon") PLUGIN_DESC = _("Control Quod Libet from the system tray.") PLUGIN_VERSION = "2.0" def enabled(self): self.__icon = Gtk.StatusIcon() self.__icon_theme = Gtk.IconTheme.get_default() self.__theme_sig = self.__icon_theme.connect('changed', self.__theme_changed) self.__icon.connect('size-changed', self.__size_changed) #no size-changed under win32 if sys.platform == "win32": self.__size = 16 self.__icon.connect('popup-menu', self.__button_right) self.__icon.connect('activate', self.__button_left) self.__icon.connect('scroll-event', self.__scroll) self.__icon.connect('button-press-event', self.__button_middle) self.__w_sig_map = app.window.connect('map', self.__window_map) self.__w_sig_del = app.window.connect('delete-event', self.__window_delete) self.__stop_after = StopAfterMenu(app.player) self.plugin_on_paused() self.plugin_on_song_started(app.player.song) def disabled(self): self.__icon_theme.disconnect(self.__theme_sig) self.__icon_theme = None self.__stop_after = None app.window.disconnect(self.__w_sig_map) app.window.disconnect(self.__w_sig_del) self.__icon.set_visible(False) try: self.__icon.destroy() except AttributeError: pass self.__icon = None self.__show_window() def PluginPreferences(self, parent): p = Preferences(self) p.connect('destroy', self.__prefs_destroy) return p def __get_paused_pixbuf(self, size, diff): """Returns a pixbuf for a paused icon frokm the current theme. The returned pixbuf can have a size of size->size+diff""" names = ('media-playback-pause', Gtk.STOCK_MEDIA_PAUSE) theme = Gtk.IconTheme.get_default() # Get the suggested icon info = theme.choose_icon(names, size, Gtk.IconLookupFlags.USE_BUILTIN) if not info: return try: pixbuf = info.load_icon() except GLib.GError: pass else: # In case it is too big, rescale if pixbuf.get_height() - size > diff: return scale(pixbuf, (size,) * 2) return pixbuf def __update_icon(self): if self.__size <= 0: return if not self.__pixbuf: try: self.__pixbuf = self.__icon_theme.load_icon( "quodlibet", self.__size, 0) except GLib.GError: util.print_exc() return #we need to fill the whole height that is given to us, or #the KDE panel will emit size-changed until we reach 0 w, h = self.__pixbuf.get_width(), self.__pixbuf.get_height() if h < self.__size: bg = GdkPixbuf.Pixbuf( GdkPixbuf.Colorspace.RGB, True, 8, w, self.__size) bg.fill(0) self.__pixbuf.copy_area(0, 0, w, h, bg, 0, (self.__size - h) / 2) self.__pixbuf = bg if app.player.paused and not self.__pixbuf_paused: base = self.__pixbuf.copy() w, h = base.get_width(), base.get_height() pad = h / 15 # get the area where we can place the icon wn, hn = calc_scale_size((w - pad, 5 * (h - pad) / 8), (1, 1)) # get a pixbuf with roughly the size we want diff = (h - hn - pad) / 3 overlay = self.__get_paused_pixbuf(hn, diff) if overlay: wo, ho = overlay.get_width(), overlay.get_height() overlay.composite(base, w - wo - pad, h - ho - pad, wo, ho, w - wo - pad, h - ho - pad, 1, 1, GdkPixbuf.InterpType.BILINEAR, 255) self.__pixbuf_paused = base if app.player.paused: new_pixbuf = self.__pixbuf_paused else: new_pixbuf = self.__pixbuf self.__icon.set_from_pixbuf(new_pixbuf) def __theme_changed(self, theme, *args): self.__pixbuf = None self.__pixbuf_paused = None self.__update_icon() def __size_changed(self, icon, size, *args): if size != self.__size: self.__pixbuf = None self.__pixbuf_paused = None self.__size = size self.__update_icon() return True def __prefs_destroy(self, *args): if self.__icon: self.plugin_on_song_started(app.player.song) def __window_delete(self, win, event): return self.__hide_window() def __window_map(self, win): visible = config.getboolean("plugins", "icon_window_visible", False) config.set("plugins", "icon_window_visible", "true") #only restore window state on start if not visible and self.__first_map: self.__hide_window() def __hide_window(self): self.__first_map = False app.hide() config.set("plugins", "icon_window_visible", "false") return True def __show_window(self): app.present() def __button_left(self, icon): if self.__destroy_win32_menu(): return if app.window.get_property('visible'): self.__hide_window() else: self.__show_window() def __button_middle(self, widget, event): if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 2: if self.__destroy_win32_menu(): return self.__play_pause() def __play_pause(self, *args): player = app.player if player.song: player.paused ^= True else: player.reset() def __scroll(self, widget, event): state = event.get_state() try: state ^= config.getboolean("plugins", "icon_modifier_swap") except config.Error: pass DIR = Gdk.ScrollDirection if event.direction in [DIR.LEFT, DIR.RIGHT]: state = Gdk.ModifierType.SHIFT_MASK player = app.player if state & Gdk.ModifierType.SHIFT_MASK: if event.direction in [DIR.UP, DIR.LEFT]: player.previous() elif event.direction in [DIR.DOWN, DIR.RIGHT]: player.next() else: if event.direction in [DIR.UP, DIR.LEFT]: player.volume += 0.05 elif event.direction in [DIR.DOWN, DIR.RIGHT]: player.volume -= 0.05 def plugin_on_song_started(self, song): if not self.__icon: return if song: try: pattern = Pattern(config.get("plugins", "icon_tooltip")) except (ValueError, config.Error): pattern = self.__pattern tooltip = pattern % song else: tooltip = _("Not playing") self.__icon.set_tooltip_markup(util.escape(tooltip)) def __destroy_win32_menu(self): """Returns True if current action should only hide the menu""" if sys.platform == "win32" and self.__menu: self.__menu.destroy() self.__menu = None return True def __button_right(self, icon, button, time): if self.__destroy_win32_menu(): return self.__menu = menu = Gtk.Menu() player = app.player window = app.window pp_icon = [Gtk.STOCK_MEDIA_PAUSE, Gtk.STOCK_MEDIA_PLAY][player.paused] playpause = Gtk.ImageMenuItem.new_from_stock(pp_icon, None) playpause.connect('activate', self.__play_pause) previous = Gtk.ImageMenuItem.new_from_stock( Gtk.STOCK_MEDIA_PREVIOUS, None) previous.connect('activate', lambda *args: player.previous()) next = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_MEDIA_NEXT, None) next.connect('activate', lambda *args: player.next()) orders = Gtk.MenuItem(label=_("Play _Order"), use_underline=True) repeat = Gtk.CheckMenuItem(label=_("_Repeat"), use_underline=True) repeat.set_active(window.repeat.get_active()) repeat.connect('toggled', lambda s: window.repeat.set_active(s.get_active())) def set_safter(widget, stop_after): stop_after.active = widget.get_active() safter = Gtk.CheckMenuItem(label=_("Stop _after this song"), use_underline=True) safter.set_active(self.__stop_after.active) safter.connect('activate', set_safter, self.__stop_after) def set_order(widget, num): window.order.set_active(num) order_items = [] item = None for i, Kind in enumerate(ORDERS): item = RadioMenuItem( group=item, label=Kind.accelerated_name, use_underline=True) order_items.append(item) item.connect('toggled', set_order, i) order_items[window.order.get_active()].set_active(True) order_sub = Gtk.Menu() order_sub.append(repeat) order_sub.append(safter) order_sub.append(SeparatorMenuItem()) map(order_sub.append, order_items) orders.set_submenu(order_sub) browse = qltk.MenuItem(_("_Browse Library"), Gtk.STOCK_FIND) browse_sub = Gtk.Menu() for Kind in browsers.browsers: if not Kind.in_menu: continue i = Gtk.MenuItem(label=Kind.accelerated_name, use_underline=True) i.connect_object('activate', LibraryBrowser, Kind, app.library) browse_sub.append(i) browse.set_submenu(browse_sub) props = qltk.MenuItem(_("Edit _Tags"), Gtk.STOCK_PROPERTIES) props.connect('activate', self.__properties) info = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_INFO, None) info.connect('activate', self.__information) rating = Gtk.MenuItem(label=_("_Rating"), use_underline=True) rating_sub = Gtk.Menu() def set_rating(value): song = player.song if song is None: return else: song["~#rating"] = value app.librarian.changed([song]) for i in range(0, int(1.0 / util.RATING_PRECISION) + 1): j = i * util.RATING_PRECISION item = Gtk.MenuItem("%0.2f\t%s" % (j, util.format_rating(j))) item.connect_object('activate', set_rating, j) rating_sub.append(item) rating.set_submenu(rating_sub) quit = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_QUIT, None) quit.connect('activate', lambda *x: app.quit()) menu.append(playpause) menu.append(SeparatorMenuItem()) menu.append(previous) menu.append(next) menu.append(orders) menu.append(SeparatorMenuItem()) menu.append(browse) menu.append(SeparatorMenuItem()) menu.append(props) menu.append(info) menu.append(rating) menu.append(SeparatorMenuItem()) menu.append(quit) menu.show_all() if sys.platform == "win32": menu.popup(None, None, None, button, time, self.__icon) else: menu.popup(None, None, Gtk.StatusIcon.position_menu, self.__icon, button, time) plugin_on_paused = __update_icon plugin_on_unpaused = __update_icon def __properties(self, *args): song = app.player.song if song: SongProperties(app.librarian, [song]) def __information(self, *args): song = app.player.song if song: Information(app.librarian, [song]) quodlibet-plugins-3.0.2/events/telepathy_status.py0000644000175000017500000001352012173212464022674 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Quod Libet Telepathy Plugin # Copyright 2012 Nick Boultbee, Christoph Reiter # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import dbus from gi.repository import Gtk from quodlibet.parse._pattern import Pattern from quodlibet.qltk.entry import UndoEntry from quodlibet import util from quodlibet import qltk from quodlibet.plugins.events import EventPlugin from quodlibet.plugins import PluginConfigMixin from quodlibet.util.dprint import print_d AM_PATH = "/org/freedesktop/Telepathy/AccountManager" AM_NAME = "org.freedesktop.Telepathy.AccountManager" AM_IFACE = "org.freedesktop.Telepathy.AccountManager" AC_IFACE = "org.freedesktop.Telepathy.Account" PROPS_IFACE = "org.freedesktop.DBus.Properties" CONN_PRESENCE_TYPE_AVAILABLE = 2 is_valid_presence_type = lambda x: x not in [0, 7, 8] def get_active_account_paths(): bus = dbus.SessionBus() bus_object = bus.get_object(AM_NAME, AM_PATH) bus_iface = dbus.Interface(bus_object, dbus_interface=PROPS_IFACE) return bus_iface.Get(AM_IFACE, "ValidAccounts") def set_accounts_requested_presence(paths, message): bus = dbus.SessionBus() for path in paths: bus_object = bus.get_object(AM_NAME, path) bus_iface = dbus.Interface(bus_object, dbus_interface=PROPS_IFACE) presence_type, status = bus_iface.Get(AC_IFACE, "CurrentPresence")[:2] if not is_valid_presence_type(presence_type): presence_type = dbus.UInt32(CONN_PRESENCE_TYPE_AVAILABLE) value = dbus.Struct([presence_type, status, message]) bus_iface.Set(AC_IFACE, "RequestedPresence", value) class TelepathyStatusPlugin(EventPlugin, PluginConfigMixin): PLUGIN_ID = "Telepathy Status" PLUGIN_NAME = _("Telepathy Status Messages") PLUGIN_DESC = _("Updates all Telepathy-based IM accounts (as configured " "in Empathy etc) with a status message based on current " "song.") PLUGIN_ICON = Gtk.STOCK_CONNECT PLUGIN_VERSION = "0.3" DEFAULT_PAT = "♫ <~artist~title> ♫" DEFAULT_PAT_PAUSED = "<~artist~title> [%s]" % _("paused") CFG_STATUS_SONGLESS = 'no_song_text' CFG_LEAVE_STATUS = "leave_status" CFG_PAT_PLAYING = "playing_pattern" CFG_PAT_PAUSED = "paused_pattern" def _set_status(self, text): print_d("Setting status to \"%s\"..." % text) self.status = text try: accounts = get_active_account_paths() # TODO: account filtering set_accounts_requested_presence(accounts, text) except dbus.DBusException: print_d("...but setting failed") util.print_exc() def plugin_on_song_started(self, song): self.song = song pat_str = self.config_get(self.CFG_PAT_PLAYING, self.DEFAULT_PAT) pattern = Pattern(pat_str) status = (pattern.format(song) if song else self.config_get(self.CFG_STATUS_SONGLESS, "")) self._set_status(status) def plugin_on_paused(self): pat_str = self.config_get(self.CFG_PAT_PAUSED, self.DEFAULT_PAT_PAUSED) pattern = Pattern(pat_str) self.status = pattern.format(self.song) if self.song else "" self._set_status(self.status) def plugin_on_unpaused(self): self.plugin_on_song_started(self.song) def disabled(self): if self.status: self._set_status(self.config_get(self.CFG_STATUS_SONGLESS)) def enabled(self): self.song = None self.status = "" def PluginPreferences(self, parent): outer_vb = Gtk.VBox(spacing=12) vb = Gtk.VBox(spacing=12) # Playing hb = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text(self.config_get(self.CFG_PAT_PLAYING, self.DEFAULT_PAT)) entry.connect('changed', self.config_entry_changed, self.CFG_PAT_PLAYING) lbl = Gtk.Label(label=_("Playing:")) entry.set_tooltip_markup(_("Status text when a song is started. " "Accepts QL Patterns e.g. %s") % util.escape("<~artist~title>")) lbl.set_mnemonic_widget(entry) hb.pack_start(lbl, False, True, 0) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, True, True, 0) # Paused hb = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text(self.config_get(self.CFG_PAT_PAUSED, self.DEFAULT_PAT_PAUSED)) entry.connect('changed', self.config_entry_changed, self.CFG_PAT_PAUSED) lbl = Gtk.Label(label=_("Paused:")) entry.set_tooltip_markup(_("Status text when a song is paused. " "Accepts QL Patterns e.g. %s") % util.escape("<~artist~title>")) lbl.set_mnemonic_widget(entry) hb.pack_start(lbl, False, True, 0) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, True, True, 0) # No Song hb = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text(self.config_get(self.CFG_STATUS_SONGLESS, "")) entry.connect('changed', self.config_entry_changed, self.CFG_STATUS_SONGLESS) entry.set_tooltip_text( _("Plain text for status when there is no current song")) lbl = Gtk.Label(label=_("No song:")) lbl.set_mnemonic_widget(entry) hb.pack_start(lbl, False, True, 0) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, True, True, 0) # Frame frame = qltk.Frame(_("Status Patterns"), child=vb) outer_vb.pack_start(frame, False, True, 0) return outer_vb quodlibet-plugins-3.0.2/events/squeezebox.py0000644000175000017500000005126712173212464021476 0ustar lazkalazka00000000000000# Copyright 2011-2012 Nick Boultbee # # Inspired in parts by PySqueezeCenter (c) 2010 JingleManSweep # SqueezeCenter and SqueezeBox are copyright Logitech # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation # import os.path from telnetlib import Telnet from threading import Thread import socket import time import urllib from gi.repository import Gtk, GLib from quodlibet import app from quodlibet import config from quodlibet import qltk from quodlibet.plugins.events import EventPlugin from quodlibet.plugins import PluginConfigMixin from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.qltk.entry import UndoEntry from quodlibet.qltk.msg import Message from quodlibet.qltk.notif import Task from quodlibet.util import copool class SqueezeboxServerSettings(dict): """Encapsulates Server settings""" def __str__(self): try: return _("Squeezebox server at {hostname}:{port}").format(**self) except KeyError: return _("unidentified Squeezebox server") class SqueezeboxPlayerSettings(dict): """Encapsulates player settings""" def __str__(self): try: return "{name} [{playerid}]".format(**self) except KeyError: return _("unidentified Squeezebox player: %r" % self) class SqueezeboxException(Exception): """Errors communicating with the Squeezebox""" class SqueezeboxServer(object): """Encapsulates access to a Squeezebox player via a squeezecenter server""" _TIMEOUT = 10 _MAX_FAILURES = 3 telnet = None is_connected = False current_player = 0 players = [] config = SqueezeboxServerSettings() _debug = False def __init__(self, hostname="localhost", port=9090, user="", password="", library_dir='', current_player=0, debug=False): self._debug = debug self.failures = 0 self.delta = 600 # Default in ms self.config = SqueezeboxServerSettings(locals()) if hostname: del self.config["self"] del self.config["current_player"] self.current_player = int(current_player) or 0 try: if self._debug: print_d("Trying %s..." % self.config) self.telnet = Telnet(hostname, port, self._TIMEOUT) except socket.error: print_w(_("Couldn't talk to %s") % (self.config,)) else: result = self.__request("login %s %s" % (user, password)) if result != (6 * '*'): raise SqueezeboxException( "Couldn't log in to squeezebox: response was '%s'" % result) self.is_connected = True self.failures = 0 print_d("Connected to Squeezebox Server! %s" % self) # Reset players (forces reload) self.players = [] self.get_players() def get_library_dir(self): return self.config['library_dir'] def __request(self, line, raw=False, want_reply=True): """ Send a request to the server, if connected, and return its response """ line = line.strip() if not (self.is_connected or line.split()[0] == 'login'): print_d("Can't do '%s' - not connected" % line.split()[0], self) return None if self._debug: print_(">>>> \"%s\"" % line) try: self.telnet.write(line + "\n") if not want_reply: return None raw_response = self.telnet.read_until("\n").strip() except socket.error, e: print_w("Couldn't communicate with squeezebox (%s)" % e) self.failures += 1 if self.failures >= self._MAX_FAILURES: print_w("Too many Squeezebox failures. Disconnecting") self.is_connected = False return None response = raw_response if raw else urllib.unquote(raw_response) if self._debug: print_("<<<< \"%s\"" % (response,)) return response[len(line) - 1:] if line.endswith("?")\ else response[len(line) + 1:] def get_players(self): """ Returns (and caches) a list of the Squeezebox players available""" if self.players: return self.players pairs = self.__request("players 0 99", True).split(" ") def demunge(string): s = urllib.unquote(string) cpos = s.index(":") return (s[0:cpos], s[cpos + 1:]) # Do a meaningful URL-unescaping and tuplification for all values pairs = map(demunge, pairs) # First element is always count count = int(pairs.pop(0)[1]) self.players = [] for pair in pairs: if pair[0] == "playerindex": playerindex = int(pair[1]) self.players.append(SqueezeboxPlayerSettings()) else: self.players[playerindex][pair[0]] = pair[1] if self._debug: print_d("Found %d player(s): %s" % (len(self.players), self.players)) assert (count == len(self.players)) return self.players def player_request(self, line, want_reply=True): if not self.is_connected: return try: return self.__request( "%s %s" % (self.players[self.current_player]["playerid"], line), want_reply=want_reply) except IndexError: return None def get_version(self): if self.is_connected: return self.__request("version ?") else: return "(not connected)" def play(self): """Plays the current song""" self.player_request("play") def is_stopped(self): """Returns whether the player is in any sort of non-playing mode""" response = self.player_request("mode ?") return "play" != response def playlist_play(self, path): """Play song immediately""" self.player_request("playlist play %s" % (urllib.quote(path))) def playlist_add(self, path): self.player_request("playlist add %s" % (urllib.quote(path)), False) def playlist_save(self, name): self.player_request("playlist save %s" % (urllib.quote(name)), False) def playlist_clear(self): self.player_request("playlist clear", False) def playlist_resume(self, name, resume, wipe=False): self.player_request("playlist resume %s noplay:%d wipePlaylist:%d" % (urllib.quote(name), int(not resume), int(wipe)), want_reply=False) def change_song(self, path): """Queue up a song""" self.player_request("playlist clear") self.player_request("playlist insert %s" % (urllib.quote(path))) def seek_to(self, ms): """Seeks the current song to `ms` milliseconds from start""" if not self.is_connected: return if self._debug: print_d("Requested %0.2f s, adding drift of %d ms..." % (ms / 1000.0, self.delta)) ms += self.delta start = time.time() self.player_request("time %d" % round(int(ms) / 1000)) end = time.time() took = (end - start) * 1000 reported_time = self.get_milliseconds() ql_pos = app.player.get_position() # Assume 50% of the time taken to complete is response. new_delta = ql_pos - reported_time # TODO: Better predictive modelling self.delta = (self.delta + new_delta) / 2 if self._debug: print_d("Player at %0.0f but QL at %0.2f." "(Took %0.0f ms). Drift was %+0.0f ms" % (reported_time / 1000.0, ql_pos / 1000.0, took, new_delta)) def get_milliseconds(self): secs = self.player_request("time ?") or 0 return float(secs) * 1000.0 def pause(self): self.player_request("pause 1") def unpause(self): if self.is_stopped(): self.play() ms = app.player.get_position() self.seek_to(ms) #self.player_request("pause 0") def stop(self): self.player_request("stop") def __str__(self): return str(self.config) class GetPlayerDialog(Gtk.Dialog): def __init__(self, parent, players, current=0): title = _("Choose Squeezebox player") super(GetPlayerDialog, self).__init__(title, parent) self.set_border_width(6) self.set_has_separator(False) self.set_resizable(False) self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK) self.vbox.set_spacing(6) self.set_default_response(Gtk.ResponseType.OK) box = Gtk.VBox(spacing=6) label = Gtk.Label( label=_("Found Squeezebox server.\nPlease choose the player")) box.set_border_width(6) label.set_line_wrap(True) label.set_justify(Gtk.Justification.CENTER) box.pack_start(label, True, True, 0) player_combo = Gtk.ComboBoxText() for player in players: player_combo.append_text(player["name"]) player_combo.set_active(current) self._val = player_combo box.pack_start(self._val, True, True, 0) self.vbox.pack_start(box, True, True, 0) self.get_child().show_all() def run(self, text=""): self.show() #self._val.set_activates_default(True) self._val.grab_focus() resp = super(GetPlayerDialog, self).run() if resp == Gtk.ResponseType.OK: value = self._val.get_active() else: value = None self.destroy() return value class SqueezeboxPluginMixin(PluginConfigMixin): """ All the Squeezebox connection / communication code in one delicious class """ # Maintain a singleton; we only support one SB server live in QL server = None ql_base_dir = os.path.realpath(config.get("settings", "scan")) # We want all derived classes to share the config section CONFIG_SECTION = "squeezebox" @classmethod def get_path(cls, song): """Gets a SB path to `song` by simple substitution""" path = song('~filename') return path.replace(cls.ql_base_dir, cls.server.get_library_dir()) @classmethod def post_reconnect(cls): pass @staticmethod def _show_dialog(dialog_type, msg): dialog = Message(dialog_type, app.window, "Squeezebox", msg) dialog.connect('response', lambda dia, resp: dia.destroy()) dialog.show() @staticmethod def quick_dialog(msg, dialog_type=Gtk.MessageType.INFO): GLib.idle_add(SqueezeboxPluginMixin._show_dialog, dialog_type, msg) @classmethod def set_player(cls, val): cls.server.current_player = val cls.config_set("current_player", val) print_d("Setting player to #%d (%s)" % (val, cls.server.players[val])) @classmethod def check_settings(cls, button): cls.init_server() if cls.server.is_connected: ret = 0 if len(cls.server.players) > 1: dialog = GetPlayerDialog(app.window, cls.server.players, cls.server.current_player) ret = dialog.run() or 0 else: cls.quick_dialog("Squeezebox OK. Using the only player (%s)." % cls.server.players[0]) cls.set_player(ret) # TODO: verify sanity of SB library path # Manage the changeover as best we can... cls.post_reconnect() else: cls.quick_dialog(_("Couldn't connect to %s") % (cls.server,), Gtk.MessageType.ERROR) @classmethod def PluginPreferences(cls, parent): def value_changed(entry, key): if entry.get_property('sensitive'): cls.server.config[key] = entry.get_text() config.set("plugins", "squeezebox_" + key, entry.get_text()) vb = Gtk.VBox(spacing=12) if not cls.server: cls.init_server() cfg = cls.server.config # Server settings Frame cfg_frame = Gtk.Frame(label=_("Squeezebox Server")) cfg_frame.set_shadow_type(Gtk.ShadowType.NONE) cfg_frame.get_label_widget().set_use_markup(True) cfg_frame_align = Gtk.Alignment.new(0, 0, 1, 1) cfg_frame_align.set_padding(6, 6, 12, 12) cfg_frame.add(cfg_frame_align) # Tabulate all settings for neatness table = Gtk.Table(3, 2) table.set_col_spacings(6) table.set_row_spacings(6) rows = [] ve = UndoEntry() ve.set_text(cfg["hostname"]) ve.connect('changed', value_changed, 'server_hostname') rows.append((Gtk.Label(label=_("Hostname:")), ve)) ve = UndoEntry() ve.set_width_chars(5) ve.set_text(str(cfg["port"])) ve.connect('changed', value_changed, 'server_port') rows.append((Gtk.Label(label=_("Port:")), ve)) ve = UndoEntry() ve.set_text(cfg["user"]) ve.connect('changed', value_changed, 'server_user') rows.append((Gtk.Label(label=_("Username:")), ve)) ve = UndoEntry() ve.set_text(str(cfg["password"])) ve.connect('changed', value_changed, 'server_password') rows.append((Gtk.Label(label=_("Password:")), ve)) ve = UndoEntry() ve.set_text(str(cfg["library_dir"])) ve.set_tooltip_text(_("Library directory the server connects to.")) ve.connect('changed', value_changed, 'server_library_dir') rows.append((Gtk.Label(label=_("Library path:")), ve)) for (row, (label, entry)) in enumerate(rows): label.set_alignment(0.0, 0.5) table.attach(label, 0, 1, row, row + 1, xoptions=Gtk.AttachOptions.FILL) table.attach(entry, 1, 2, row, row + 1) # Add verify button button = Gtk.Button(_("_Verify settings"), use_underline=True) button.set_sensitive(cls.server is not None) button.connect('clicked', cls.check_settings) table.attach(button, 0, 2, row + 1, row + 2) cfg_frame_align.add(table) vb.pack_start(cfg_frame, True, True, 0) debug = cls.ConfigCheckButton(_("Debug"), "debug") vb.pack_start(debug, True, True, 0) return vb @classmethod def init_server(cls): """Initialises a server, and connects to check if it's alive""" try: cur = int(cls.config_get("current_player", 0)) except ValueError: cur = 0 cls.server = SqueezeboxServer( hostname=cls.config_get("server_hostname", "localhost"), port=cls.config_get("server_port", 9090), user=cls.config_get("server_user", ""), password=cls.config_get("server_password", ""), library_dir=cls.config_get("server_library_dir", cls.ql_base_dir), current_player=cur, debug=cls.config_get_bool("debug", False)) try: ver = cls.server.get_version() if cls.server.is_connected: print_d( "Squeezebox server version: %s. Current player: #%d (%s)." % (ver, cur, cls.server.get_players()[cur]["name"])) except (IndexError, KeyError), e: print_d("Couldn't get player info (%s)." % e) class SqueezeboxSyncPlugin(EventPlugin, SqueezeboxPluginMixin): PLUGIN_ID = 'Squeezebox Output' PLUGIN_NAME = _('Squeezebox Sync') PLUGIN_DESC = _("Make Logitech Squeezebox mirror Quod Libet output, " "provided both read from an identical library") PLUGIN_ICON = Gtk.STOCK_MEDIA_PLAY PLUGIN_VERSION = '0.3' server = None active = False _debug = False def __init__(self): super(EventPlugin, self).__init__() super(SqueezeboxPluginMixin, self).__init__() @classmethod def post_reconnect(cls): cls.server.stop() SqueezeboxPluginMixin.post_reconnect() player = app.player cls.plugin_on_song_started(player.info) cls.plugin_on_seek(player.info, player.get_position()) def enabled(self): print_d("Debug is set to %s" % self._debug) self.active = True self.init_server() self.server.pause() if not self.server.is_connected: qltk.ErrorMessage( None, _("Error finding Squeezebox server"), _("Error finding %s. Please check settings") % self.server.config ).run() def disabled(self): # Stopping might be annoying in some situations, but seems more correct if self.server: self.server.stop() self.active = False @classmethod def plugin_on_song_started(cls, song): # Yucky hack to allow some form of immediacy on re-configuration cls.server._debug = cls._debug = cls.config_get_bool("debug", False) if cls._debug: print_d("Paused" if app.player.paused else "Not paused") if song and cls.server and cls.server.is_connected: path = cls.get_path(song) print_d("Requesting to play %s..." % path) if app.player.paused: cls.server.change_song(path) else: cls.server.playlist_play(path) @classmethod def plugin_on_paused(cls): if cls.server: cls.server.pause() @classmethod def plugin_on_unpaused(cls): if cls.server: cls.server.unpause() @classmethod def plugin_on_seek(cls, song, msec): if not app.player.paused: if cls.server: cls.server.seek_to(msec) cls.server.play() else: pass class SqueezeboxPlaylistPlugin(SongsMenuPlugin, SqueezeboxPluginMixin): PLUGIN_ID = "Export to Squeezebox Playlist" PLUGIN_NAME = _("Export to Squeezebox Playlist") PLUGIN_DESC = _("Dynamically export songs to Logitech Squeezebox " "playlists, provided both share a directory structure. " "Shares configuration with Squeezebox Sync plugin") PLUGIN_ICON = Gtk.STOCK_EDIT PLUGIN_VERSION = '0.2' TEMP_PLAYLIST = "_quodlibet" def __add_songs(self, task, songs, name): """Generator for copool to add songs to the temp playlist""" print_d("Backing up current Squeezebox playlist") self.__cancel = False self.server.playlist_save(self.TEMP_PLAYLIST) self.server.playlist_clear() # Check if we're currently playing. stopped = self.server.is_stopped() total = len(songs) print_d("Adding %d song(s) to Squeezebox playlist. " "This might take a while..." % total) for i, song in enumerate(songs): if self.__cancel: print_d("Cancelled squeezebox export") self.__cancel = False break # Actually do the (slow) call worker = Thread(target=self.server.playlist_add, args=(self.get_path(song),)) worker.daemon = True worker.start() worker.join(timeout=3) #self.server.playlist_add(self.get_path(song)) task.update(float(i) / total) yield True print_d("Saving Squeezebox playlist \"%s\"" % name) task.pulse() self.server.playlist_save(name) yield True task.pulse() # Resume if we actually stopped self.server.playlist_resume(self.TEMP_PLAYLIST, not stopped, True) task.finish() def __cancel_add(self): """Tell the copool to stop (adding songs)""" self.__cancel = True def __get_playlist_name(self): dialog = qltk.GetStringDialog(None, _("Export selection to Squeezebox playlist"), _("Playlist name (will overwrite existing)"), okbutton=Gtk.STOCK_SAVE) name = dialog.run(text="Quod Libet playlist") return name def plugin_songs(self, songs): self.init_server() if not self.server.is_connected: qltk.ErrorMessage( None, _("Error finding Squeezebox server"), _("Error finding %s. Please check settings") % self.server.config ).run() else: name = self.__get_playlist_name() task = Task("Squeezebox", _("Export to Squeezebox playlist"), stop=self.__cancel_add) copool.add(self.__add_songs, task, songs, name, funcid="squeezebox-playlist-save") quodlibet-plugins-3.0.2/events/write_cover.py0000644000175000017500000000362312173212464021625 0ustar lazkalazka00000000000000# Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os import shutil from gi.repository import Gtk from quodlibet import config from quodlibet.const import USERDIR from quodlibet.plugins.events import EventPlugin try: config.get("plugins", __name__) except: out = os.path.join(USERDIR, "current.cover") config.set("plugins", __name__, out) class PictureSaver(EventPlugin): PLUGIN_ID = "Picture Saver" PLUGIN_NAME = _("Picture Saver") PLUGIN_DESC = "The cover image of the current song is saved to a file." PLUGIN_ICON = Gtk.STOCK_SAVE PLUGIN_VERSION = "0.21" def plugin_on_song_started(self, song): outfile = config.get("plugins", __name__) if song is None: try: os.unlink(outfile) except EnvironmentError: pass else: cover = song.find_cover() if cover is None: try: os.unlink(outfile) except EnvironmentError: pass else: f = file(outfile, "wb") f.write(cover.read()) f.close() def PluginPreferences(self, parent): def changed(entry): fn = entry.get_text() try: shutil.move(config.get("plugins", __name__), fn) except EnvironmentError: pass else: config.set("plugins", __name__, fn) hb = Gtk.HBox(spacing=6) hb.set_border_width(6) hb.pack_start(Gtk.Label(_("File:")), True, True, 0) e = Gtk.Entry() e.set_text(config.get("plugins", __name__)) e.connect('changed', changed) hb.pack_start(e, True, True, 0) return hb quodlibet-plugins-3.0.2/events/clock.py0000644000175000017500000000763712173212464020401 0ustar lazkalazka00000000000000# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import time from gi.repository import Gtk, GLib from quodlibet import app from quodlibet import config from quodlibet.plugins.events import EventPlugin from quodlibet.qltk.entry import ValidatingEntry class Alarm(EventPlugin): PLUGIN_ID = "Alarm Clock" PLUGIN_NAME = _("Alarm Clock") PLUGIN_DESC = _("Wake you up with loud music.") PLUGIN_ICON = Gtk.STOCK_DIALOG_INFO PLUGIN_VERSION = "0.22" _pref_name = "alarm_times" _times = ["HH:MM"] * 7 _enabled = False def __init__(self): try: self._times = config.get("plugins", self._pref_name).split(' ')[:7] except: pass else: self._times = (self._times + ["HH:MM"] * 7)[:7] GLib.timeout_add(30000, self._check) def enabled(self): self._enabled = True def disabled(self): self._enabled = False def is_valid_time(time): try: hour, minute = map(int, time.split(":")) except: return False else: return (hour < 24 and minute < 60) is_valid_time = staticmethod(is_valid_time) def plugin_on_song_started(self, song): pass def _entry_changed(self, entries): self._times = map(ValidatingEntry.get_text, entries) config.set("plugins", self._pref_name, " ".join(self._times)) def _ready(self): tdata = time.localtime() goal = self._times[tdata.tm_wday] try: ghour, gminute = map(int, goal.split(":")) except: return False else: return (tdata.tm_hour, tdata.tm_min) == (ghour, gminute) def _fire(self): if self._enabled: if app.player.paused: if app.player.song is None: app.player.next() else: app.player.paused = False GLib.timeout_add(60000, self._longer_check) def _longer_check(self): if self._ready(): self._fire() else: GLib.timeout_add(30000, self._check) def _check(self): if self._ready(): self._fire() else: return True def PluginPreferences(self, parent): t = Gtk.Table(2, 7) t.set_col_spacings(6) entries = [] for i in range(7): e = ValidatingEntry(Alarm.is_valid_time) e.set_size_request(100, -1) e.set_text(self._times[i]) e.set_max_length(5) e.set_width_chars(6) day = Gtk.Label( time.strftime("_%A:", (2000, 1, 1, 0, 0, 0, i, 1, 0))) day.set_mnemonic_widget(e) day.set_use_underline(True) day.set_alignment(0.0, 0.5) t.attach(day, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL) t.attach(e, 1, 2, i, i + 1, xoptions=Gtk.AttachOptions.FILL) entries.append(e) for e in entries: e.connect_object('changed', self._entry_changed, entries) return t class Lullaby(Alarm): PLUGIN_ID = "Lullaby" PLUGIN_NAME = _("Lullaby") PLUGIN_DESC = _("Fade out and pause your music.") PLUGIN_ICON = Gtk.STOCK_MEDIA_PAUSE PLUGIN_VERSION = "0.20" _pref_name = "lullaby_times" def _fire(self): if self._enabled: GLib.timeout_add(500, self._fade_out) self.__was_volume = app.player.volume else: GLib.timeout_add(30000, self._check) def _fade_out(self): app.player.volume -= 0.005 if app.player.volume == 0: app.player.paused = True if app.player.paused: app.player.volume = self.__was_volume GLib.timeout_add(30000, self._check) else: return True quodlibet-plugins-3.0.2/events/mpris.py0000644000175000017500000005562012173212464020433 0ustar lazkalazka00000000000000# Copyright 2010,2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import time import tempfile from gi.repository import Gtk import dbus import dbus.service try: import indicate except ImportError: indicate = None from quodlibet import app from quodlibet import config from quodlibet import qltk from quodlibet.qltk.ccb import ConfigCheckButton from quodlibet.util.uri import URI from quodlibet.util.dbusutils import DBusIntrospectable, DBusProperty from quodlibet.util.dbusutils import dbus_unicode_validate as unival from quodlibet.plugins.events import EventPlugin # TODO: OpenUri, CanXYZ # Date parsing (util?) class MPRIS(EventPlugin): PLUGIN_ID = "mpris" PLUGIN_NAME = _("MPRIS D-Bus support") PLUGIN_DESC = _("Control Quod Libet using the " "MPRIS 1.0/2.0 D-Bus Interface Specification.") PLUGIN_ICON = Gtk.STOCK_CONNECT PLUGIN_VERSION = "0.2" def PluginPreferences(self, parent): box = Gtk.HBox() ccb = ConfigCheckButton(_("Hide main window on close"), 'plugins', 'mpris_window_hide') ccb.set_active(self.__do_hide()) box.pack_start(qltk.Frame(_("Preferences"), child=ccb), True, True, 0) return box def __do_hide(self): return config.getboolean('plugins', 'mpris_window_hide', False) def __window_delete(self, win, event): if self.__do_hide(): win.hide() return True def enabled(self): self.__sig = app.window.connect('delete-event', self.__window_delete) self.objects = [MPRIS1Root(), MPRIS1DummyTracklist(), MPRIS1Player(), MPRIS2()] # Needed for sound menu support in some older Ubuntu versions if indicate: self.__indicate_server = s = indicate.indicate_server_ref_default() s.set_type("music.quodlibet") s.set_desktop_file("/usr/share/applications/quodlibet.desktop") s.show() def disabled(self): if indicate: self.__indicate_server.hide() for obj in self.objects: obj.remove_from_connection() self.objects = [] import gc gc.collect() app.window.disconnect(self.__sig) def plugin_on_paused(self): for obj in self.objects: obj.paused() def plugin_on_unpaused(self): for obj in self.objects: obj.unpaused() def plugin_on_song_started(self, song): for obj in self.objects: obj.song_started(song) def plugin_on_song_ended(self, song, skipped): for obj in self.objects: obj.song_ended(song, skipped) class MPRISObject(dbus.service.Object): def paused(self): pass def unpaused(self): pass def song_started(self, song): pass def song_ended(self, song, skipped): pass # http://xmms2.org/wiki/MPRIS class MPRIS1Root(MPRISObject): PATH = "/" BUS_NAME = "org.mpris.quodlibet" IFACE = "org.freedesktop.MediaPlayer" def __init__(self): bus = dbus.SessionBus() name = dbus.service.BusName(self.BUS_NAME, bus) super(MPRIS1Root, self).__init__(name, self.PATH) @dbus.service.method(IFACE, out_signature="s") def Identity(self): return "Quod Libet" @dbus.service.method(IFACE) def Quit(self): app.quit() @dbus.service.method(IFACE, out_signature="(qq)") def MprisVersion(self): return (1, 0) class MPRIS1DummyTracklist(MPRISObject): PATH = "/TrackList" BUS_NAME = "org.mpris.quodlibet" IFACE = "org.freedesktop.MediaPlayer" def __init__(self): bus = dbus.SessionBus() name = dbus.service.BusName(self.BUS_NAME, bus) super(MPRIS1DummyTracklist, self).__init__(name, self.PATH) @dbus.service.method(IFACE, in_signature="i", out_signature="a{sv}") def GetMetadata(self, position): song = app.player.info if position != 0: song = None return MPRIS1Player._get_metadata(song) @dbus.service.method(IFACE, out_signature="i") def GetCurrentTrack(self): return 0 @dbus.service.method(IFACE, out_signature="i") def GetLength(self): return 0 @dbus.service.method(IFACE, in_signature="sb", out_signature="i") def AddTrack(self, uri, play): return -1 @dbus.service.method(IFACE, in_signature="b") def SetLoop(self, loop): app.window.repeat.set_active(loop) @dbus.service.method(IFACE, in_signature="b") def SetRandom(self, shuffle): window = app.window shuffle_on = window.order.get_active_name() == "shuffle" if shuffle_on and not shuffle: window.order.set_active("inorder") elif not shuffle_on and shuffle: window.order.set_active("shuffle") class MPRIS1Player(MPRISObject): PATH = "/Player" BUS_NAME = "org.mpris.quodlibet" IFACE = "org.freedesktop.MediaPlayer" def __init__(self): bus = dbus.SessionBus() name = dbus.service.BusName(self.BUS_NAME, bus) super(MPRIS1Player, self).__init__(name, self.PATH) self.__rsig = app.window.repeat.connect( "toggled", self.__update_status) self.__ssig = app.window.order.connect( "changed", self.__update_status) self.__lsig = app.librarian.connect( "changed", self.__update_track_changed) def remove_from_connection(self, *arg, **kwargs): super(MPRIS1Player, self).remove_from_connection(*arg, **kwargs) app.window.repeat.disconnect(self.__rsig) app.window.order.disconnect(self.__ssig) app.librarian.disconnect(self.__lsig) def paused(self): self.StatusChange(self.__get_status()) unpaused = paused def song_started(self, song): self.TrackChange(self._get_metadata(song)) def __update_track_changed(self, library, songs): if app.player.info in songs: self.TrackChange(self._get_metadata(app.player.info)) def __update_status(self, *args): self.StatusChange(self.__get_status()) @staticmethod def _get_metadata(song): #http://xmms2.org/wiki/MPRIS_Metadata#MPRIS_v1.0_Metadata_guidelines metadata = dbus.Dictionary(signature="sv") if not song: return metadata # Missing: "audio-samplerate", "video-bitrate" strings = {"location": "~uri", "title": "title", "artist": "artist", "album": "album", "tracknumber": "tracknumber", "genre": "genre", "comment": "comment", "asin": "asin", "puid fingerprint": "musicip_puid", "mb track id": "musicbrainz_trackid", "mb artist id": "musicbrainz_artistid", "mb artist sort name": "artistsort", "mb album id": "musicbrainz_albumid", "mb release date": "date", "mb album artist": "albumartist", "mb album artist id": "musicbrainz_albumartistid", "mb album artist sort name": "albumartistsort", } for key, tag in strings.iteritems(): val = song.comma(tag) if val: metadata[key] = unival(val) nums = [("audio-bitrate", 1024, "~#bitrate"), ("rating", 5, "~#rating"), ("year", 1, "~#year"), ("time", 1, "~#length"), ("mtime", 1000, "~#length")] for target, mul, key in nums: value = song(key, None) if value is None: continue value = int(value * mul) # dbus uses python types to guess the dbus type without # checking maxint, also we need uint (dbus always trys int) try: value = dbus.UInt32(value) except OverflowError: continue metadata[target] = value year = song("~year") if year: try: tuple_time = time.strptime(year, "%Y") except ValueError: pass else: try: date = int(time.mktime(tuple_time)) date = dbus.UInt32(date) except (ValueError, OverflowError): pass else: metadata["date"] = date return metadata def __get_status(self): window = app.window play = (not app.player.info and 2) or int(app.player.paused) shuffle = (window.order.get_active_name() != "inorder") repeat_one = (window.order.get_active_name() == "onesong" and window.repeat.get_active()) repeat_all = int(window.repeat.get_active()) return (play, shuffle, repeat_one, repeat_all) @dbus.service.method(IFACE) def Next(self): app.player.next() @dbus.service.method(IFACE) def Prev(self): app.player.previous() @dbus.service.method(IFACE) def Pause(self): if app.player.song is None: app.player.reset() else: app.player.paused ^= True @dbus.service.method(IFACE) def Stop(self): app.player.stop() @dbus.service.method(IFACE) def Play(self): player = app.player if player.song is None: player.reset() else: if player.paused: player.paused = False else: player.seek(0) @dbus.service.method(IFACE) def Repeat(self): pass @dbus.service.method(IFACE, out_signature="(iiii)") def GetStatus(self): return self.__get_status() @dbus.service.method(IFACE, out_signature="a{sv}") def GetMetadata(self): return self._get_metadata(app.player.info) @dbus.service.method(IFACE, out_signature="i") def GetCaps(self): # everything except Tracklist return (1 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 | 1 << 5) @dbus.service.method(IFACE, in_signature="i") def VolumeSet(self, volume): app.player.volume = volume / 100.0 @dbus.service.method(IFACE, out_signature="i") def VolumeGet(self): return int(round(app.player.volume * 100)) @dbus.service.method(IFACE, in_signature="i") def PositionSet(self, position): app.player.seek(position) @dbus.service.method(IFACE, out_signature="i") def PositionGet(self): return int(app.player.get_position()) @dbus.service.signal(IFACE, signature="a{sv}") def TrackChange(self, metadata): pass @dbus.service.signal(IFACE, signature="(iiii)") def StatusChange(self, status): pass @dbus.service.signal(IFACE, signature="i") def CapsChange(self, status): pass # http://www.mpris.org/2.0/spec/ class MPRIS2(DBusProperty, DBusIntrospectable, MPRISObject): BUS_NAME = "org.mpris.MediaPlayer2.quodlibet" PATH = "/org/mpris/MediaPlayer2" ROOT_IFACE = "org.mpris.MediaPlayer2" ROOT_ISPEC = """ """ ROOT_PROPS = """ """ PLAYER_IFACE = "org.mpris.MediaPlayer2.Player" PLAYER_ISPEC = """ """ PLAYER_PROPS = """ """ def __init__(self): DBusIntrospectable.__init__(self) DBusProperty.__init__(self) self.set_introspection(MPRIS2.ROOT_IFACE, MPRIS2.ROOT_ISPEC) self.set_properties(MPRIS2.ROOT_IFACE, MPRIS2.ROOT_PROPS) self.set_introspection(MPRIS2.PLAYER_IFACE, MPRIS2.PLAYER_ISPEC) self.set_properties(MPRIS2.PLAYER_IFACE, MPRIS2.PLAYER_PROPS) bus = dbus.SessionBus() name = dbus.service.BusName(self.BUS_NAME, bus) MPRISObject.__init__(self, bus, self.PATH, name) self.__rsig = app.window.repeat.connect("toggled", self.__repeat_changed) self.__ssig = app.window.order.connect("changed", self.__order_changed) self.__lsig = app.librarian.connect("changed", self.__library_changed) self.__vsig = app.player.connect("notify::volume", self.__volume_changed) self.__seek_sig = app.player.connect("seek", self.__seeked) def remove_from_connection(self, *arg, **kwargs): super(MPRIS2, self).remove_from_connection(*arg, **kwargs) self.__cover = None app.window.repeat.disconnect(self.__rsig) app.window.order.disconnect(self.__ssig) app.librarian.disconnect(self.__lsig) app.player.disconnect(self.__vsig) app.player.disconnect(self.__seek_sig) def __volume_changed(self, *args): self.emit_properties_changed(self.PLAYER_IFACE, ["Volume"]) def __repeat_changed(self, *args): self.emit_properties_changed(self.PLAYER_IFACE, ["LoopStatus"]) def __order_changed(self, *args): self.emit_properties_changed(self.PLAYER_IFACE, ["Shuffle", "LoopStatus"]) def __seeked(self, player, song, ms): self.Seeked(ms * 1000) def __library_changed(self, library, song): if song and song is not app.player.info: return self.emit_properties_changed(self.PLAYER_IFACE, ["Metadata"]) @dbus.service.method(ROOT_IFACE) def Raise(self): app.present() @dbus.service.method(ROOT_IFACE) def Quit(self): app.quit() @dbus.service.signal(PLAYER_IFACE, signature="x") def Seeked(self, position): pass @dbus.service.method(PLAYER_IFACE) def Next(self): player = app.player paused = player.paused player.next() player.paused = paused @dbus.service.method(PLAYER_IFACE) def Previous(self): player = app.player paused = player.paused player.previous() player.paused = paused @dbus.service.method(PLAYER_IFACE) def Pause(self): app.player.paused = True @dbus.service.method(PLAYER_IFACE) def Play(self): if app.player.song is None: app.player.reset() else: app.player.paused = False @dbus.service.method(PLAYER_IFACE) def PlayPause(self): player = app.player if player.song is None: player.reset() else: player.paused ^= True @dbus.service.method(PLAYER_IFACE) def Stop(self): app.player.stop() @dbus.service.method(PLAYER_IFACE, in_signature="x") def Seek(self, offset): new_pos = app.player.get_position() + offset / 1000 app.player.seek(new_pos) @dbus.service.method(PLAYER_IFACE, in_signature="ox") def SetPosition(self, track_id, position): if track_id == self.__get_current_track_id(): app.player.seek(position / 1000) def paused(self): self.emit_properties_changed(self.PLAYER_IFACE, ["PlaybackStatus"]) unpaused = paused def song_started(self, song): # so the position in clients gets updated faster self.Seeked(0) self.emit_properties_changed(self.PLAYER_IFACE, ["PlaybackStatus", "Metadata"]) def __get_current_track_id(self): path = "/net/sacredchao/QuodLibet" if not app.player.info: return dbus.ObjectPath(path + "/" + "NoTrack") return dbus.ObjectPath(path + "/" + str(id(app.player.info))) def __get_metadata(self): """http://xmms2.org/wiki/MPRIS_Metadata""" metadata = {} metadata["mpris:trackid"] = self.__get_current_track_id() song = app.player.info if not song: return metadata metadata["mpris:length"] = dbus.Int64(song("~#length") * 10 ** 6) self.__cover = cover = song.find_cover() is_temp = False if cover: name = cover.name is_temp = name.startswith(tempfile.gettempdir()) # This doesn't work for embedded images.. the file gets unlinked # after loosing the file handle metadata["mpris:artUrl"] = str(URI.frompath(name)) if not is_temp: self.__cover = None # All list values list_val = {"artist": "artist", "albumArtist": "albumartist", "comment": "comment", "composer": "composer", "genre": "genre", "lyricist": "lyricist"} for xesam, tag in list_val.iteritems(): vals = song.list(tag) if vals: metadata["xesam:" + xesam] = map(unival, vals) # All single values sing_val = {"album": "album", "title": "title", "asText": "~lyrics"} for xesam, tag in sing_val.iteritems(): vals = song.comma(tag) if vals: metadata["xesam:" + xesam] = unival(vals) # URI metadata["xesam:url"] = song("~uri") # Integers num_val = {"audioBPM": "bpm", "discNumber": "disc", "trackNumber": "track", "useCount": "playcount"} for xesam, tag in num_val.iteritems(): val = song("~#" + tag, None) if val is not None: metadata["xesam:" + xesam] = int(val) # Rating metadata["xesam:userRating"] = float(song("~#rating")) # Dates ISO_8601_format = "%Y-%m-%dT%H:%M:%S" tuple_time = time.gmtime(song("~#lastplayed")) iso_time = time.strftime(ISO_8601_format, tuple_time) metadata["xesam:lastUsed"] = iso_time year = song("~year") if year: try: tuple_time = time.strptime(year, "%Y") iso_time = time.strftime(ISO_8601_format, tuple_time) except ValueError: pass else: metadata["xesam:contentCreated"] = iso_time return metadata def set_property(self, interface, name, value): player = app.player window = app.window if interface == self.PLAYER_IFACE: if name == "LoopStatus": if value == "Playlist": window.repeat.set_active(True) window.order.set_active("inorder") elif value == "Track": window.repeat.set_active(True) window.order.set_active("onesong") elif value == "None": window.repeat.set_active(False) elif name == "Rate": pass elif name == "Shuffle": if value: window.order.set_active("shuffle") else: window.order.set_active("inorder") elif name == "Volume": player.volume = value def get_property(self, interface, name): player = app.player window = app.window if interface == self.ROOT_IFACE: if name == "CanQuit": return True elif name == "CanRaise": return True elif name == "CanSetFullscreen": return False elif name == "HasTrackList": return False elif name == "Identity": return "Quod Libet" elif name == "DesktopEntry": return "quodlibet" elif name == "SupportedUriSchemes": # TODO: enable once OpenUri is done can = lambda s: False #can = lambda s: app.player.can_play_uri("%s://fake" % s) schemes = ["http", "https", "ftp", "file", "mms"] return filter(can, schemes) elif name == "SupportedMimeTypes": from quodlibet import formats return formats.mimes elif interface == self.PLAYER_IFACE: if name == "PlaybackStatus": if not player.song: return "Stopped" return ("Playing", "Paused")[int(player.paused)] elif name == "LoopStatus": repeat = window.repeat.get_active() if repeat: onesong = window.order.get_active_name() == "onesong" if onesong: return "Track" else: return "Playlist" else: return "None" elif name == "Rate": return 1.0 elif name == "Shuffle": return (window.order.get_active_name() == "shuffle") elif name == "Metadata": return self.__get_metadata() elif name == "Volume": return player.volume elif name == "Position": return player.get_position() * 1000 elif name == "MinimumRate": return 1.0 elif name == "MaximumRate": return 1.0 elif name == "CanGoNext": return True elif name == "CanGoPrevious": return True elif name == "CanPlay": return True elif name == "CanPause": return True elif name == "CanSeek": return True elif name == "CanControl": return True quodlibet-plugins-3.0.2/events/autorating.py0000644000175000017500000000165312161032160021441 0ustar lazkalazka00000000000000# Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from quodlibet.plugins.events import EventPlugin class AutoRating(EventPlugin): PLUGIN_ID = "Automatic Rating" PLUGIN_NAME = _("Automatic Rating") PLUGIN_VERSION = "0.22" PLUGIN_DESC = ("Rate songs automatically when they are played or " "skipped. This uses the 'accelerated' algorithm from " "vux by Brian Nelson.") def plugin_on_song_ended(self, song, skipped): if song is not None: rating = song("~#rating") invrating = 1.0 - rating delta = min(rating, invrating) / 2.0 if skipped: rating -= delta else: rating += delta song["~#rating"] = rating quodlibet-plugins-3.0.2/events/inhibit.py0000644000175000017500000000411512173212464020720 0ustar lazkalazka00000000000000# Copyright 2011 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. from gi.repository import Gtk import dbus from quodlibet import app from quodlibet.plugins.events import EventPlugin def get_toplevel_xid(): if app.window.get_window(): try: return app.window.get_window().get_xid() except AttributeError: # non x11 pass return 0 class InhibitFlags(object): LOGOUT = 1 USERSWITCH = 1 << 1 SUSPEND = 1 << 2 IDLE = 1 << 3 class SessionInhibit(EventPlugin): PLUGIN_ID = "screensaver_inhibit" PLUGIN_NAME = _("Inhibit Screensaver") PLUGIN_DESC = _("Prevent the GNOME screensaver from activating while" " a song is playing.") PLUGIN_ICON = Gtk.STOCK_STOP PLUGIN_VERSION = "0.3" DBUS_NAME = "org.gnome.SessionManager" DBUS_INTERFACE = "org.gnome.SessionManager" DBUS_PATH = "/org/gnome/SessionManager" APPLICATION_ID = "quodlibet" INHIBIT_REASON = _("Music is playing") __cookie = None def enabled(self): if not app.player.paused: self.plugin_on_unpaused() def disabled(self): if not app.player.paused: self.plugin_on_paused() def plugin_on_unpaused(self): xid = dbus.UInt32(get_toplevel_xid()) flags = dbus.UInt32(InhibitFlags.IDLE) try: bus = dbus.SessionBus() obj = bus.get_object(self.DBUS_NAME, self.DBUS_PATH) self.__cookie = obj.Inhibit( self.APPLICATION_ID, xid, self.INHIBIT_REASON, flags) except dbus.DBusException: pass def plugin_on_paused(self): if self.__cookie is None: return try: bus = dbus.SessionBus() obj = bus.get_object(self.DBUS_NAME, self.DBUS_PATH) obj.Uninhibit(self.__cookie) self.__cookie = None except dbus.DBusException: pass quodlibet-plugins-3.0.2/events/notify.py0000644000175000017500000003766712173212464020624 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2010 Felix Krull # 2011-2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # Note: This plugin is based on notify.py as distributed in the # quodlibet-plugins package; however, that file doesn't contain a copyright # note. As for the license, GPLv2 is the only choice anyway, as it calls # Quod Libet code, which is GPLv2 as well, so I thought it safe to add this. import re import tempfile import dbus from gi.repository import Gtk, GObject, GLib from quodlibet import config, qltk, app from quodlibet.plugins.events import EventPlugin from quodlibet.parse import XMLFromPattern from quodlibet.qltk.textedit import TextView, TextBuffer from quodlibet.qltk.entry import UndoEntry from quodlibet.qltk.msg import ErrorMessage from quodlibet.util import unescape from quodlibet.util.uri import URI # configuration stuff DEFAULT_CONFIG = { "timeout": 4000, "show_notifications": "all", "show_only_when_unfocused": True, "titlepattern": " - >", "bodypattern": """<~length> <album|<album><discsubtitle| - <discsubtitle>> ><~year|<~year>>""", } def get_conf_value(name, accessor="get"): try: value = getattr(config, accessor)("plugins", "notify_%s" % name) except Exception: value = DEFAULT_CONFIG[name] return value get_conf_bool = lambda name: get_conf_value(name, "getboolean") get_conf_int = lambda name: get_conf_value(name, "getint") def set_conf_value(name, value): config.set("plugins", "notify_%s" % name, unicode(value)) class PreferencesWidget(Gtk.VBox): def __init__(self, parent, plugin_instance): GObject.GObject.__init__(self, spacing=12) self.plugin_instance = plugin_instance # notification text settings table = Gtk.Table(2, 3) table.set_col_spacings(6) table.set_row_spacings(6) text_frame = qltk.Frame(_("Notification text"), child=table) title_entry = UndoEntry() title_entry.set_text(get_conf_value("titlepattern")) title_entry.connect("focus-out-event", self.on_entry_unfocused, "titlepattern") table.attach(title_entry, 1, 2, 0, 1) title_label = Gtk.Label(label=_("_Title:")) title_label.set_use_underline(True) title_label.set_alignment(0, 0.5) title_label.set_mnemonic_widget(title_entry) table.attach(title_label, 0, 1, 0, 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) title_revert = Gtk.Button() title_revert.add(Gtk.Image.new_from_stock( Gtk.STOCK_REVERT_TO_SAVED, Gtk.IconSize.MENU)) title_revert.set_tooltip_text(_("Revert to default pattern")) title_revert.connect_object( "clicked", title_entry.set_text, DEFAULT_CONFIG["titlepattern"]) table.attach(title_revert, 2, 3, 0, 1, xoptions=Gtk.AttachOptions.SHRINK) body_textbuffer = TextBuffer() body_textview = TextView(buffer=body_textbuffer) body_textview.set_size_request(-1, 85) body_textview.get_buffer().set_text(get_conf_value("bodypattern")) body_textview.connect("focus-out-event", self.on_textview_unfocused, "bodypattern") body_scrollarea = Gtk.ScrolledWindow() body_scrollarea.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) body_scrollarea.set_shadow_type(Gtk.ShadowType.ETCHED_OUT) body_scrollarea.add(body_textview) table.attach(body_scrollarea, 1, 2, 1, 2) body_label = Gtk.Label(label=_("_Body:")) body_label.set_padding(0, 3) body_label.set_use_underline(True) body_label.set_alignment(0, 0) body_label.set_mnemonic_widget(body_textview) table.attach(body_label, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.SHRINK) revert_align = Gtk.Alignment() body_revert = Gtk.Button() body_revert.add(Gtk.Image.new_from_stock( Gtk.STOCK_REVERT_TO_SAVED, Gtk.IconSize.MENU)) body_revert.set_tooltip_text(_("Revert to default pattern")) body_revert.connect_object( "clicked", body_textbuffer.set_text, DEFAULT_CONFIG["bodypattern"]) revert_align.add(body_revert) table.attach( revert_align, 2, 3, 1, 2, xoptions=Gtk.AttachOptions.SHRINK, yoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) # preview button preview_button = qltk.Button( _("_Show notification"), Gtk.STOCK_EXECUTE) preview_button.set_sensitive(app.player.info is not None) preview_button.connect("clicked", self.on_preview_button_clicked) self.qlplayer_connected_signals = [ app.player.connect("paused", self.on_player_state_changed, preview_button), app.player.connect("unpaused", self.on_player_state_changed, preview_button), ] table.attach( preview_button, 0, 3, 2, 3, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) self.pack_start(text_frame, True, True, 0) # notification display settings display_box = Gtk.VBox(spacing=12) display_frame = qltk.Frame(_("Show notifications"), child=display_box) radio_box = Gtk.VBox(spacing=6) display_box.pack_start(radio_box, True, True, 0) only_user_radio = Gtk.RadioButton(label=_( "Only on <i>_manual</i> song changes" ), use_underline=True) only_user_radio.get_child().set_use_markup(True) only_user_radio.connect("toggled", self.on_radiobutton_toggled, "show_notifications", "user") radio_box.pack_start(only_user_radio, True, True, 0) only_auto_radio = Gtk.RadioButton(group=only_user_radio, label=_( "Only on <i>_automatic</i> song changes" ), use_underline=True) only_auto_radio.get_child().set_use_markup(True) only_auto_radio.connect("toggled", self.on_radiobutton_toggled, "show_notifications", "auto") radio_box.pack_start(only_auto_radio, True, True, 0) all_radio = Gtk.RadioButton(group=only_user_radio, label=_( "On <i>a_ll</i> song changes" ), use_underline=True) all_radio.get_child().set_use_markup(True) all_radio.connect("toggled", self.on_radiobutton_toggled, "show_notifications", "all") radio_box.pack_start(all_radio, True, True, 0) try: { "user": only_user_radio, "auto": only_auto_radio, "all": all_radio }[get_conf_value("show_notifications")].set_active(True) except KeyError: all_radio.set_active(True) set_conf_value("show_notifications", "all") focus_check = Gtk.CheckButton(_("Only when the main window is not " "_focused")) focus_check.set_active(get_conf_bool("show_only_when_unfocused")) focus_check.connect("toggled", self.on_checkbutton_toggled, "show_only_when_unfocused") display_box.pack_start(focus_check, True, True, 0) self.pack_start(display_frame, True, True, 0) self.show_all() self.connect("destroy", self.on_destroyed) # callbacks def on_entry_unfocused(self, entry, event, cfgname): set_conf_value(cfgname, entry.get_text()) def on_textview_unfocused(self, textview, event, cfgname): text_buffer = textview.get_buffer() start, end = text_buffer.get_bounds() text = text_buffer.get_text(start, end, True) set_conf_value(cfgname, text) def on_radiobutton_toggled(self, radio, cfgname, value): if radio.get_active(): set_conf_value(cfgname, value) def on_checkbutton_toggled(self, button, cfgname): set_conf_value(cfgname, button.get_active()) def on_preview_button_clicked(self, button): if app.player.info is not None: if not self.plugin_instance.show_notification(app.player.info): ErrorMessage(self, _("Connection Error"), _("Couldn't connect to notification daemon.")).run() def on_player_state_changed(self, player, preview_button): preview_button.set_sensitive(player.info is not None) def on_destroyed(self, ev): for sig in self.qlplayer_connected_signals: app.player.disconnect(sig) self.qlplayer_connected_signals = [] self.plugin_instance = None class Notify(EventPlugin): PLUGIN_ID = "Notify" PLUGIN_NAME = _("Song Notifications") PLUGIN_DESC = _("Display a notification when the song changes.") PLUGIN_ICON = Gtk.STOCK_DIALOG_INFO PLUGIN_VERSION = "1.1" DBUS_NAME = "org.freedesktop.Notifications" DBUS_IFACE = "org.freedesktop.Notifications" DBUS_PATH = "/org/freedesktop/Notifications" # these can all be used even if it wasn't enabled __enabled = False __last_id = 0 __image_fp = None __interface = None __action_sig = None def enabled(self): self.__enabled = True # This works because: # - if paused, any on_song_started event will be generated by user # interaction # - if playing, an on_song_ended event will be generated before any # on_song_started event in any case. self.__was_stopped_by_user = True self.__force_notification = False self.__caps = None self.__spec_version = None self.__enable_watch() def disabled(self): self.__disable_watch() self.__disconnect() self.__enabled = False self.__image_fp = None def __enable_watch(self): """Enable events for dbus name owner change""" bus = dbus.Bus(dbus.Bus.TYPE_SESSION) # This also triggers for existing name owners self.__watch = bus.watch_name_owner(self.DBUS_NAME, self.__owner_changed) def __disable_watch(self): """Disable name owner change events""" if self.__watch: self.__watch.cancel() self.__watch = None def __disconnect(self): self.__interface = None if self.__action_sig: self.__action_sig.remove() self.__action_sig = None def __owner_changed(self, owner): # In case the owner gets removed, remove all references to it if not owner: self.__disconnect() def PluginPreferences(self, parent): return PreferencesWidget(parent, self) def __get_interface(self): """Returns a fresh proxy + info about the server""" obj = dbus.SessionBus().get_object(self.DBUS_NAME, self.DBUS_PATH) interface = dbus.Interface(obj, self.DBUS_IFACE) name, vendor, version, spec_version = \ map(str, interface.GetServerInformation()) spec_version = map(int, spec_version.split(".")) caps = map(str, interface.GetCapabilities()) return interface, caps, spec_version def close_notification(self): """Closes the last opened notification""" if not self.__last_id: return try: obj = dbus.SessionBus().get_object(self.DBUS_NAME, self.DBUS_PATH) interface = dbus.Interface(obj, self.DBUS_IFACE) interface.CloseNotification(self.__last_id) except dbus.DBusException: pass else: self.__last_id = 0 def show_notification(self, song): """Returns True if showing the notification was successful""" if not song: return True try: if self.__enabled: # we are enabled try to work with the data we have and # keep it fresh if not self.__interface: iface, caps, spec = self.__get_interface() self.__interface = iface self.__caps = caps self.__spec_version = spec if "actions" in caps: self.__action_sig = iface.connect_to_signal( "ActionInvoked", self.on_dbus_action) else: iface = self.__interface caps = self.__caps spec = self.__spec_version else: # not enabled, just get everything temporary, # propably preview iface, caps, spec = self.__get_interface() except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False strip_markup = lambda t: re.subn("\</?[iub]\>", "", t)[0] strip_links = lambda t: re.subn("\</?a.*?\>", "", t)[0] strip_images = lambda t: re.subn("\<img.*?\>", "", t)[0] title = XMLFromPattern(get_conf_value("titlepattern")) % song title = unescape(strip_markup(strip_links(strip_images(title)))) body = "" if "body" in caps: body = XMLFromPattern(get_conf_value("bodypattern")) % song if "body-markup" not in caps: body = strip_markup(body) if "body-hyperlinks" not in caps: body = strip_links(body) if "body-images" not in caps: body = strip_images(body) image_path = "" if "icon-static" in caps: self.__image_fp = song.find_cover() if self.__image_fp: image_path = self.__image_fp.name is_temp = image_path.startswith(tempfile.gettempdir()) # If it is not an embeded cover, drop the file handle if not is_temp: self.__image_fp = None # spec recommends it, and it seems to work if image_path and spec >= (1, 1): image_path = URI.frompath(image_path) actions = [] if "actions" in caps: actions = ["next", _("Next")] hints = { "desktop-entry": "quodlibet", } try: self.__last_id = iface.Notify( "Quod Libet", self.__last_id, image_path, title, body, actions, hints, get_conf_int("timeout")) except dbus.DBusException: print_w("[notify] %s" % _("Couldn't connect to notification daemon.")) self.__disconnect() return False # preview done, remove all references again if not self.__enabled: self.__disconnect() return True def on_dbus_action(self, notify_id, key): if notify_id == self.__last_id and key == "next": # Always show a new notification if the next button got clicked self.__force_notification = True app.player.next() def on_song_change(self, song, typ): if not song: self.close_notification() if get_conf_value("show_notifications") in [typ, "all"] \ and not (get_conf_bool("show_only_when_unfocused") and app.window.has_toplevel_focus()) \ or self.__force_notification: def idle_show(song): self.show_notification(song) GLib.idle_add(idle_show, song) self.__force_notification = False def plugin_on_song_started(self, song): typ = (self.__was_stopped_by_user and "user") or "auto" self.on_song_change(song, typ) def plugin_on_song_ended(self, song, stopped): # if `stopped` is `True`, this song was ended due to some kind of user # interaction. self.__was_stopped_by_user = stopped �������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/radioadmute.py�������������������������������������������������������0000644�0001750�0001750�00000003466�12161032160�021566� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2011, 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import re from quodlibet import app from quodlibet.plugins.events import EventPlugin class RadioAdMute(EventPlugin): PLUGIN_ID = "radio_ad_mute" PLUGIN_NAME = _("Mute radio ads") PLUGIN_VERSION = "0.1" PLUGIN_DESC = ("Mute while radio advertisements are playing.\n" "Stations: di.fm") SPAM = ["www.webex.co.uk", "di.fm/premium", "There's more to Digitally Imported!", "Digitally Imported AMTAG_60 ADWTAG_30000_START=0", "PhotonVPS.com", "Get Digitally Imported Premium", "More of the show after these messages", ] RE_SPAM = ["Sponsored Message\s+\([0-9]+\)", ] SPAM = map(re.escape, SPAM) + RE_SPAM SPAM = [re.compile(s, re.I) for s in SPAM] __old_volume = 0 __muted = False def disabled(self): self.plugin_on_song_ended() def plugin_on_song_started(self, song): # only check stream info songs if not song or not song.streamsong: return player = app.player data = song("~title~artist") for spam in self.SPAM: if spam.search(data): self.__old_volume = player.volume self.__muted = True player.volume = 0 break def plugin_on_song_ended(self, *args): if not self.__muted: return self.__muted = False player = app.player if player.volume != 0: # volume changed, do nothing return # restore old volume player.volume = self.__old_volume ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/viewlyrics.py��������������������������������������������������������0000644�0001750�0001750�00000007731�12173212464�021501� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- # # View Lyrics: a Quod Libet plugin for viewing lyrics. # Copyright (C) 2008, 2011, 2012 Vasiliy Faronov <vfaronov@gmail.com> # 2013 Nick Boultbee # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You can get a copy of the GNU General Public License at: # http://www.gnu.org/licenses/gpl-2.0.html """Provides the `ViewLyrics` plugin for viewing lyrics in the main window.""" import os from gi.repository import Gtk, Gdk from quodlibet import app from quodlibet.plugins.events import EventPlugin class ViewLyrics(EventPlugin): """The plugin for viewing lyrics in the main window.""" PLUGIN_ID = 'View Lyrics' PLUGIN_NAME = _('View Lyrics') PLUGIN_DESC = _('View lyrics beneath the song list.') PLUGIN_VERSION = '0.4' def enabled(self): self.expander = Gtk.Expander(label=_("_Lyrics"), use_underline=True) self.expander.set_expanded(True) self.scrolled_window = Gtk.ScrolledWindow() self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.scrolled_window.set_size_request(-1, 200) self.adjustment = self.scrolled_window.get_vadjustment() self.textview = Gtk.TextView() self.textbuffer = self.textview.get_buffer() self.textview.set_editable(False) self.textview.set_cursor_visible(False) self.textview.set_wrap_mode(Gtk.WrapMode.WORD) self.textview.set_justification(Gtk.Justification.CENTER) self.textview.connect('key-press-event', self.key_press_event_cb) self.scrolled_window.add_with_viewport(self.textview) self.textview.show() self.expander.add(self.scrolled_window) self.scrolled_window.show() app.window.get_child().pack_start(self.expander, False, True, 0) # We don't show the expander here because it will be shown when a song # starts playing (see plugin_on_song_started). def disabled(self): self.textview.destroy() self.scrolled_window.destroy() self.expander.destroy() def plugin_on_song_started(self, song): """Called when a song is started. Loads the lyrics. If there are lyrics associated with `song`, load them into the lyrics viewer. Otherwise, hides the lyrics viewer. """ if (song is not None) and os.path.exists(song.lyric_filename): with open(song.lyric_filename, 'r') as lyric_file: self.textbuffer.set_text(lyric_file.read()) self.adjustment.set_value(0) # Scroll to the top. self.expander.show() else: self.expander.hide() def key_press_event_cb(self, widget, event): """Handles up/down "key-press-event" in the lyrics view.""" adj = self.scrolled_window.get_vadjustment().props if event.keyval == Gdk.KEY_Up: adj.value = max(adj.value - adj.step_increment, adj.lower) elif event.keyval == Gdk.KEY_Down: adj.value = min(adj.value + adj.step_increment, adj.upper - adj.page_size) elif event.keyval == Gdk.KEY_Page_Up: adj.value = max(adj.value - adj.page_increment, adj.lower) elif event.keyval == Gdk.KEY_Page_Down: adj.value = min(adj.value + adj.page_increment, adj.upper - adj.page_size) elif event.keyval == Gdk.KEY_Home: adj.value = adj.lower elif event.keyval == Gdk.KEY_End: adj.value = adj.upper - adj.page_size else: return False return True ���������������������������������������quodlibet-plugins-3.0.2/events/automask.py����������������������������������������������������������0000644�0001750�0001750�00000002702�12173212464�021116� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os import gi gi.require_version("Gio", "2.0") from gi.repository import Gio from quodlibet import app from quodlibet.plugins.events import EventPlugin from quodlibet.util.uri import URI class AutoMasking(EventPlugin): PLUGIN_ID = "automask" PLUGIN_NAME = _("Automatic Masking") PLUGIN_DESC = _("Automatically mask and unmask drives when they " "are unmounted or mounted, using GNOME-VFS.") PLUGIN_VERSION = "0.1" __sigs = None __monitor = None def enabled(self): if self.__monitor is None: self.__monitor = Gio.VolumeMonitor.get() self.__sigs = [ self.__monitor.connect('mount-added', self.__mounted), self.__monitor.connect('mount-removed', self.__unmounted), ] else: map(self.__monitor.handler_unblock, self.__sigs) def disabled(self): map(self.__monitor.handler_block, self.__sigs) def __mounted(self, monitor, mount): path = mount.get_default_location().get_path() app.library.unmask(os.path.normpath(path)) def __unmounted(self, monitor, mount): path = mount.get_default_location().get_path() app.library.mask(os.path.normpath(path)) ��������������������������������������������������������������quodlibet-plugins-3.0.2/events/randomalbum.py�������������������������������������������������������0000644�0001750�0001750�00000021256�12173212464�021600� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005-2009 Joe Wreschnig, Steven Robertson # 2012 Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import random from gi.repository import Gtk, GLib from quodlibet import app from quodlibet import config from quodlibet.plugins.events import EventPlugin from quodlibet import util try: from quodlibet.qltk import notif except Exception: notif = None class RandomAlbum(EventPlugin): PLUGIN_ID = 'Random Album Playback' PLUGIN_NAME = _('Random Album Playback') PLUGIN_DESC = ("When your playlist reaches its end a new album will " "be chosen randomly and started. It requires that your " "active browser supports filtering by album.") PLUGIN_VERSION = '2.4' weights = {} use_weights = False # Not a dict because we want to impose a particular order # Third item is to specify a non-default aggregation function keys = [ ("rating", _("Rated higher"), None), ("playcount", _("Played more often"), 'avg'), ("skipcount", _("Skipped more often"), 'avg'), ("lastplayed", _("Played more recently"), None), ("laststarted", _("Started more recently"), None), ("added", _("Added more recently"), None), ("length", _("Longer albums"), None), ] def __init__(self): for (key, text, func) in self.keys: val = config.getfloat("plugins", "randomalbum_%s" % key, 0.0) self.weights[key] = val use = config.getint("plugins", "randomalbum_use_weights", 0) self.use_weights = use delay = config.getint("plugins", "randomalbum_delay", 0) self.delay = delay def PluginPreferences(self, song): def changed_cb(hscale, key): val = hscale.get_value() self.weights[key] = val config.set("plugins", "randomalbum_%s" % key, val) def delay_changed_cb(spin): self.delay = int(spin.get_value()) config.set("plugins", "randomalbum_delay", str(self.delay)) def toggled_cb(check, widgets): self.use_weights = check.get_active() for w in widgets: w.set_sensitive(self.use_weights) config.set("plugins", "randomalbum_use_weights", str(int(self.use_weights))) vbox = Gtk.VBox(spacing=12) table = Gtk.Table(len(self.keys) + 1, 3) table.set_border_width(3) hbox = Gtk.HBox(spacing=6) spin = Gtk.SpinButton( adjustment=Gtk.Adjustment(self.delay, 0, 3600, 1, 10)) spin.connect("value-changed", delay_changed_cb) hbox.pack_start(spin, False, True, 0) lbl = Gtk.Label(label=_("seconds before starting next album")) hbox.pack_start(lbl, False, True, 0) vbox.pack_start(hbox, True, True, 0) frame = Gtk.Frame(label=_("Weights")) check = Gtk.CheckButton(_("Play some albums more than others")) vbox.pack_start(check, False, True, 0) # Toggle both frame and contained table; frame doesn't always work? check.connect("toggled", toggled_cb, [frame, table]) check.set_active(self.use_weights) toggled_cb(check, [frame, table]) frame.add(table) vbox.pack_start(frame, True, True, 0) # Less label less_lbl = Gtk.Label() arr = Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.OUT) less_lbl.set_markup("<i>%s</i>" % util.escape(_("avoid"))) less_lbl.set_alignment(0, 0) hb = Gtk.HBox(spacing=0) hb.pack_start(arr, False, True, 0) hb.pack_start(less_lbl, True, True, 0) table.attach(hb, 1, 2, 0, 1, xpadding=3, xoptions=Gtk.AttachOptions.FILL) # More label more_lbl = Gtk.Label() arr = Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.OUT) more_lbl.set_markup("<i>%s</i>" % util.escape(_("prefer"))) more_lbl.set_alignment(1, 0) hb = Gtk.HBox(spacing=0) hb.pack_end(arr, False, True, 0) hb.pack_end(more_lbl, True, True, 0) table.attach(hb, 2, 3, 0, 1, xpadding=3, xoptions=Gtk.AttachOptions.FILL) for (idx, (key, text, func)) in enumerate(self.keys): lbl = Gtk.Label(label=text) lbl.set_alignment(0, 0) table.attach(lbl, 0, 1, idx + 1, idx + 2, xoptions=Gtk.AttachOptions.FILL, xpadding=3, ypadding=3) adj = Gtk.Adjustment(lower=-1.0, upper=1.0, step_incr=0.1) hscale = Gtk.HScale(adjustment=adj) hscale.set_value(self.weights[key]) hscale.set_draw_value(False) hscale.set_show_fill_level(False) hscale.connect("value-changed", changed_cb, key) lbl.set_mnemonic_widget(hscale) table.attach(hscale, 1, 3, idx + 1, idx + 2, xpadding=3, ypadding=3) return vbox def _score(self, albums): """Score each album. Returns a list of (score, name) tuples.""" # Score the album based on its weighted rank ordering for each key # Rank ordering is more resistant to clustering than weighting # based on normalized means, and also normalizes the scale of each # weight slider in the prefs pane. ranked = {} for (tag, text, func) in self.keys: tag_key = ("~#%s:%s" % (tag, func) if func else "~#%s" % tag) ranked[tag] = sorted(albums, key=lambda al: al.get(tag_key)) scores = {} for album in albums: scores[album] = 0 for (tag, text, func) in self.keys: rank = ranked[tag].index(album) scores[album] += rank * self.weights[tag] return [(score, name) for name, score in scores.items()] def plugin_on_song_started(self, song): if (song is None and config.get("memory", "order") != "onesong" and not app.player.paused): browser = app.window.browser if not browser.can_filter('album'): return albumlib = app.library.albums albumlib.load() if browser.can_filter_albums(): keys = browser.list_albums() values = [albumlib[k] for k in keys] else: keys = set(browser.list("album")) values = [a for a in albumlib if a("album") in keys] if self.use_weights: # Select 3% of albums, or at least 3 albums nr_albums = int(min(len(values), max(0.03 * len(values), 3))) chosen_albums = random.sample(values, nr_albums) album_scores = sorted(self._score(chosen_albums)) for score, album in album_scores: print_d("%0.2f scored by %s" % (score, album("album"))) album = max(album_scores)[1] else: album = random.choice(values) if album is not None: self.schedule_change(album) def schedule_change(self, album): if self.delay: srcid = GLib.timeout_add(1000 * self.delay, self.change_album, album) if notif is None: return task = notif.Task(_("Random Album"), _("Waiting to start <i>%s</i>") % album("album"), stop=lambda: GLib.source_remove(srcid)) def countdown(): for i in range(10 * self.delay): task.update(i / (10. * self.delay)) yield True task.finish() yield False GLib.timeout_add(100, countdown().next) else: self.change_album(album) def change_album(self, album): browser = app.window.browser if not browser.can_filter('album'): return if browser.can_filter_albums(): browser.filter_albums([album.key]) else: browser.filter('album', [album("album")]) GLib.idle_add(self.unpause) def unpause(self): # Wait for the next GTK loop to make sure everything's tidied up # after the song ended. Also, if this is program startup and the # previous current song wasn't found, we'll get this condition # as well, so just leave the player paused if that's the case. try: app.player.next() except AttributeError: app.player.paused = True ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/iradiolog.py���������������������������������������������������������0000644�0001750�0001750�00000002205�12161032160�021227� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from quodlibet import app from quodlibet.plugins.events import EventPlugin class IRadioLog(EventPlugin): PLUGIN_ID = "Internet Radio Log" PLUGIN_NAME = _("Internet Radio Log") PLUGIN_DESC = _("Record the last 10 songs played on radio stations, " "and list them in the seek context menu.") PLUGIN_ICON = 'gtk-edit' PLUGIN_VERSION = "0.22" def plugin_on_song_started(self, song): if song is None: return player = app.player if player.song.multisong and not song.multisong: time = player.get_position() title = song("title") bookmarks = player.song.bookmarks bookmarks.append([time // 1000, title]) try: bookmarks.pop(-10) except IndexError: pass player.song.bookmarks = bookmarks elif song.multisong: song.bookmarks = [] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/osxmmkey.py����������������������������������������������������������0000644�0001750�0001750�00000011741�12161032160�021137� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- # Copyright 2012 Martijn Pieters <mj@zopatista.com> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation # # osxmmkey - Mac OS X Media Keys support # -------------------------------------- # # The osxmmkey plugin adds support for media keys under mac; when enabled the # standard play, next and previous buttons control quodlibet the way you'd # expect. # # Requires the PyObjC, with the Cocoa and Quartz bindings to be installed. # Under macports, that's the `py27-pyobjc`, `py27-pyobjc-cocoa` # and`py27-pyobjc-quartz` ports, or equivalents for the python version used by # quodlibet. # # This plugin also requires that 'access for assistive devices' is enabled, see # the Universal Access preference pane in the OS X System Prefences. # # We run a separate process (not a fork!) so we can run a Quartz event loop # without having to bother with making that work with the GTK event loop. # There we register a Quartz event tap to listen for the multimedia keys and # control QL through it's const.CONTROL pipe. import subprocess import sys try: from quodlibet import const from quodlibet.plugins.events import EventPlugin if not sys.platform.startswith("darwin"): from quodlibet.plugins import PluginImportException raise PluginImportException("wrong platform", ["darwin"]) except ImportError: # When executing the event tap process, we may not be able to import # quodlibet libraries, which is fine. pass else: __all__ = ['OSXMMKey'] class OSXMMKey(EventPlugin): PLUGIN_ID = "OSXMMKey" PLUGIN_NAME = _("Mac OS X Multimedia Keys") PLUGIN_DESC = _("Enable Mac OS X Multimedia Shortcut Keys.\n\n" "Requires the PyObjC bindings (with both the Cocoa and Quartz " "framework bridges), and that access for assistive devices " "is enabled (see the Universal Access preference pane).") PLUGIN_VERSION = "0.1" __eventsapp = None def enabled(self): # Start the event capturing process self.__eventsapp = subprocess.Popen( (sys.executable, __file__, const.CONTROL)) def disabled(self): if self.__eventsapp is not None: self.__eventsapp.kill() self.__eventsapp = None # # Quartz event tap, listens for media key events and translates these to # control messages for quodlibet. # import os import signal from AppKit import NSKeyUp, NSSystemDefined, NSEvent import Quartz class MacKeyEventsTap(object): def __init__(self, controlPath): self._keyControls = { 16: 'play-pause', 19: 'next', 20: 'previous', } self.controlPath = controlPath def eventTap(self, proxy, type_, event, refcon): # Convert the Quartz CGEvent into something more useful keyEvent = NSEvent.eventWithCGEvent_(event) if keyEvent.subtype() is 8: # subtype 8 is media keys data = keyEvent.data1() keyCode = (data & 0xFFFF0000) >> 16 keyState = (data & 0xFF00) >> 8 if keyState == NSKeyUp and keyCode in self._keyControls: self.sendControl(self._keyControls[keyCode]) def sendControl(self, control): # Send our control message to QL. if not os.path.exists(self.controlPath): sys.exit() try: # This is a total abuse of Python! Hooray! # Totally copied from the quodlibet command line handler too.. signal.signal(signal.SIGALRM, lambda: "" + 2) signal.alarm(1) f = file(self.controlPath, "w") signal.signal(signal.SIGALRM, signal.SIG_IGN) f.write(control) f.close() except (OSError, IOError, TypeError): sys.exit() @classmethod def runEventsCapture(cls, controlPath): tapHandler = cls(controlPath) tap = Quartz.CGEventTapCreate( Quartz.kCGSessionEventTap, # Session level is enough for our needs Quartz.kCGHeadInsertEventTap, # Insert wherever, we do not filter Quartz.kCGEventTapOptionListenOnly, # Listening is enough # NSSystemDefined for media keys Quartz.CGEventMaskBit(NSSystemDefined), tapHandler.eventTap, None ) # Create a runloop source and add it to the current loop runLoopSource = Quartz.CFMachPortCreateRunLoopSource(None, tap, 0) Quartz.CFRunLoopAddSource( Quartz.CFRunLoopGetCurrent(), runLoopSource, Quartz.kCFRunLoopDefaultMode ) # Enable the tap Quartz.CGEventTapEnable(tap, True) # and run! This won't return until we exit or are terminated. Quartz.CFRunLoopRun() if __name__ == '__main__': # In the subprocess, capture media key events MacKeyEventsTap.runEventsCapture(sys.argv[1]) �������������������������������quodlibet-plugins-3.0.2/events/auto_library_update.py�����������������������������������������������0000644�0001750�0001750�00000014040�12173212464�023326� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Automatic library update plugin # # (c) 2009 Joe Higton # 2011, 2012 Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from pyinotify import WatchManager, EventsCodes, ProcessEvent, Notifier,\ ThreadedNotifier from quodlibet import config, print_d from quodlibet.plugins.events import EventPlugin from quodlibet import app from gi.repository import GLib import os class LibraryEvent(ProcessEvent): """pynotify event handler for library changes""" # Slightly dodgy state mechanism for updates _being_created = set() def __init__(self, library): self._library = library def process_default(self, event): print_d('Uncaught event for %s' % (event.maskname if event else "??")) def process_IN_CLOSE_WRITE(self, event): path = os.path.join(event.path, event.name) # No need to add files for modifications only if path in self._being_created: GLib.idle_add(self.add, event) self._being_created.remove(path) elif event.path in self._being_created: # The first file per new-directory gets missed for me (bug?) # TODO: so work out how/when to remove parent path properly GLib.idle_add(self.add, event) self._being_created.remove(event.path) else: print_d("Ignoring modification on %s" % path) def process_IN_MOVED_TO(self, event): print_d('Triggered for "%s"' % event.name) GLib.idle_add(self.add, event) def process_IN_CREATE(self, event): #print_d('Triggered for "%s"' % event.name) # Just remember that they've been created, process in further updates path = os.path.join(event.path, event.name) self._being_created.add(path) def process_IN_DELETE(self, event): print_d('Triggered for "%s"' % event.name) GLib.idle_add(self.update, event) def process_IN_MOVED_FROM(self, event): print_d('Triggered for "%s"' % event.name) GLib.idle_add(self.update, event) def add(self, event): """Add a library file / folder based on an incoming event""" lib = self._library path = os.path.join(event.path, event.name) if event.dir: print_d('Scanning directories...') songs = [] for path, dnames, fnames in os.walk(path): print_d('Found %d file(s) in "%s"' % (len(fnames), path)) for filename in (os.path.join(path, fn) for fn in fnames): song = lib.add_filename(filename, add=False) if song: songs.append(song) lib.add(songs) else: lib.add_filename(path) return False def update(self, event): """Update a library / file. Typically this means deleting it""" lib = self._library path = os.path.join(event.path, event.name) if event.dir: print_d('Checking directory %s...' % path) to_reload = [] for filename in lib._contents: if filename.startswith(path): item = lib.get(filename, None) if item: # Don't modify whilst iterating... to_reload.append(item) print_d('Reloading %d matching songs(s)' % len(to_reload)) for item in to_reload: lib.reload(item) else: item = lib.get(path, None) if item: lib.reload(item) return False class AutoLibraryUpdate(EventPlugin): PLUGIN_ID = "Automatic library update" PLUGIN_DESC = _("Keep your library up to date with inotify. " "Requires %s.") % "pyinotify" PLUGIN_VERSION = "0.3" # TODO: make a config option USE_THREADS = True event_handler = None running = False def enabled(self): if not self.running: wm = WatchManager() self.event_handler = LibraryEvent(app.library) # Choose event types to watch for # FIXME: watch for IN_CREATE or for some reason folder copies # are missed, --nickb FLAGS = ['IN_DELETE', 'IN_CLOSE_WRITE',# 'IN_MODIFY', 'IN_MOVED_FROM', 'IN_MOVED_TO', 'IN_CREATE'] mask = reduce(lambda x, s: x | EventsCodes.ALL_FLAGS[s], FLAGS, 0) if self.USE_THREADS: print_d("Using threaded notifier") self.notifier = ThreadedNotifier(wm, self.event_handler) # Daemonize to ensure thread dies on exit self.notifier.daemon = True self.notifier.start() else: self.notifier = Notifier(wm, self.event_handler, timeout=100) GLib.timeout_add(1000, self.unthreaded_callback) for path in self.get_library_dirs(): print_d('Watching directory %s for %s' % (path, FLAGS)) # See https://github.com/seb-m/pyinotify/wiki/ # Frequently-Asked-Questions wm.add_watch(path, mask, rec=True, auto_add=True) self.running = True def unthreaded_callback(self): """Processes as much of the inotify events as allowed""" assert self.notifier._timeout is not None, \ 'Notifier must be constructed with a [short] timeout' self.notifier.process_events() # loop in case more events appear while we are processing while self.notifier.check_events(): self.notifier.read_events() self.notifier.process_events() return True # disable hook, stop the notifier: def disabled(self): if self.running: self.running = False if self.notifier: print_d("Stopping inotify watch...") self.notifier.stop() # find list of directories to scan def get_library_dirs(self): return config.get("settings", "scan").split(":") ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/equalizer.py���������������������������������������������������������0000644�0001750�0001750�00000016717�12173212464�021306� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2010 Steven Robertson # 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation # # TODO: Include presets, saving and loading. from gi.repository import Gtk from quodlibet import app from quodlibet import config from quodlibet.plugins.events import EventPlugin # Presets taken from pulseaudio equalizer PRESET_BANDS = [50, 100, 156, 220, 311, 440, 622, 880, 1250, 1750, 2500, 3500, 5000, 10000, 20000] PRESETS = { "flat": (_("Flat"), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), "live": (_("Live"), [-9.0, -5.5, 0.0, 1.5, 2.1, 3.4, 3.4, 3.4, 3.4, 3.4, 3.4, 3.4, 2.8, 1.6, 1.8]), "full_bass_treble": (_("Full Bass & Treble"), [4.8, 4.8, 3.5, 2.5, 0.0, -7.0, -14.0, -10.0, -10.0, -8.0, 1.0, 1.0, 5.2, 7.7, 9.5]), "club": (_("Club"), [-0.2, -0.2, -0.2, -0.2, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 3.5, 2.5, 2.5, 0.0, 0.0]), "large_hall": (_("Large Hall"), [7.0, 7.0, 7.0, 3.5, 3.0, 3.0, 3.0, 1.5, 0.0, -2.0, -3.5, -6.0, -9.0, -1.0, 0.0]), "party": (_("Party"), [4.8, 4.8, 4.8, 3.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.5, 4.8]), "rock": (_("Rock"), [5.3, 2.6, 2.6, -8.5, -10.5, -11.2, -16.0, -14.7, -6.6, -5.7, -3.0, 3.0, 6.7, 7.3, 7.3]), "soft": (_("Soft"), [3.2, 2.8, 0.8, 0.9, 0.0, -2.4, -4.8, 1.5, 0.0, 1.1, 3.0, 3.0, 5.8, 7.8, 7.8]), "full_bass": (_("Full Bass"), [-16.0, -16.0, 6.5, 6.5, 6.0, 5.5, 4.5, 1.0, 1.0, 1.0, -8.0, -10.0, -16.0, -16.0, -20.4]), "classical": (_("Classical"), [-0.2, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2, -21.0, -21.0, -27.0]), "reggae": (_("Reggae"), [0.0, 0.0, 0.0, 0.0, 0.0, -4.5, -10.0, -6.0, 0.5, 1.0, 2.0, 4.0, 4.0, 0.0, 0.0]), "headphones": (_("Headphones"), [3.0, 3.0, 7.3, 7.0, 3.0, -1.0, -6.6, -6.3, -4.5, -4.0, 1.1, 1.2, 5.8, 7.9, 8.8]), "soft_rock": (_("Soft Rock"), [2.7, 2.7, 2.7, 1.5, 1.5, 1.4, 0.0, -3.6, -8.0, -7.2, -9.8, -8.9, -6.6, 1.4, 5.8]), "full_treble": (_("Full Treble"), [4.8, -18.6, -18.6, -18.6, -18.6, -10.0, -8.0, -6.5, 1.5, 1.5, 1.5, 8.5, 10.6, 10.6, 10.6]), "dance": (_("Dance"), [6.1, 4.3, 4.3, 1.7, 1.7, 1.7, -0.1, -0.1, -0.1, 0.8, -10.7, -14.2, -15.1, -7.2, 0.0]), "pop": (_("Pop"), [-3.4, 1.7, 2.0, 3.0, 5.0, 5.6, 6.5, 5.2, 3.2, 1.5, 0.0, -2.5, -4.8, -4.8, -3.2]), "techno": (_("Techno"), [5.0, 4.0, 3.9, 3.3, 0.0, -4.5, -10.0, -8.9, -8.1, -5.5, -1.5, 3.0, 6.0, 6.1, 5.8]), "ska": (_("Ska"), [-4.5, -8.1, -8.9, -8.5, -8.0, -6.0, 0.0, 1.5, 2.5, 2.7, 3.2, 3.3, 5.8, 6.4, 6.4]), "laptop": (_("Laptop"), [-1, -1, -1, -1, -5, -10, -18, -15, -10, -5, -5, -5, -5, 0, 0]), } def interp_bands(src_band, target_band, src_gain): """Linear interp from one band to another. All must be sorted.""" gain = [] for i, b in enumerate(target_band): if b in src_band: gain.append(src_gain[i]) continue idx = sorted(src_band + [b]).index(b) idx = min(max(idx, 1), len(src_band) - 1) x1, x2 = src_band[idx - 1:idx + 1] y1, y2 = src_gain[idx - 1:idx + 1] g = y1 + ((y2 - y1) * (b - x1)) / float(x2 - x1) gain.append(min(12.0, g)) return gain def get_config(): try: return map(float, config.get('plugins', 'equalizer_levels').split(',')) except (config.Error, ValueError): return [] class Equalizer(EventPlugin): PLUGIN_ID = "Equalizer" PLUGIN_NAME = _("Equalizer") PLUGIN_DESC = _("Control the balance of your music with an equalizer.") PLUGIN_ICON = 'gtk-connect' PLUGIN_VERSION = '2.3' @property def player_has_eq(self): return hasattr(app.player, 'eq_bands') and app.player.eq_bands def __init__(self): super(Equalizer, self).__init__() self._enabled = False def apply(self): if not self.player_has_eq: return levels = self._enabled and get_config() or [] lbands = len(app.player.eq_bands) app.player.eq_values = (levels[:min(len(levels), lbands)] + [0.] * max(0, (lbands - len(levels)))) def enabled(self): self._enabled = True self.apply() def disabled(self): self._enabled = False self.apply() def PluginPreferences(self, win): vb = Gtk.VBox(spacing=6) if not self.player_has_eq: l = Gtk.Label() l.set_markup('The current backend does not support equalization.') vb.pack_start(l, False, True, 0) return vb bands = [(band >= 1000 and ('%.1f kHz' % (band / 1000.)) or ('%d Hz' % band)) for band in app.player.eq_bands] levels = get_config() + [0.] * len(bands) table = Gtk.Table(rows=len(bands), columns=3) table.set_col_spacings(6) def set_band(adj, idx): levels[idx] = adj.get_value() config.set('plugins', 'equalizer_levels', ','.join(map(str, levels))) self.apply() adjustments = [] for i, band in enumerate(bands): # align numbers and suffixes in separate rows for great justice lbl = Gtk.Label(label=band.split()[0]) lbl.set_alignment(1, 0.5) lbl.set_padding(0, 4) table.attach(lbl, 0, 1, i, i + 1, xoptions=Gtk.AttachOptions.FILL) lbl = Gtk.Label(label=band.split()[1]) lbl.set_alignment(1, 0.5) table.attach(lbl, 1, 2, i, i + 1, xoptions=Gtk.AttachOptions.FILL) adj = Gtk.Adjustment(levels[i], -24., 12., 0.1) adj.connect('value-changed', set_band, i) adjustments.append(adj) hs = Gtk.HScale(adjustment=adj) hs.set_draw_value(True) hs.set_value_pos(Gtk.PositionType.RIGHT) hs.connect('format-value', lambda s, v: '%.1f dB' % v) table.attach(hs, 2, 3, i, i + 1) vb.pack_start(table, True, True, 0) def clicked_cb(button): [adj.set_value(0) for adj in adjustments] sorted_presets = sorted(PRESETS.iteritems()) def combo_changed(combo): # custom, skip if not combo.get_active(): return gain = sorted_presets[combo.get_active() - 1][1][1] gain = interp_bands(PRESET_BANDS, app.player.eq_bands, gain) for (g, a) in zip(gain, adjustments): a.set_value(g) combo = Gtk.ComboBoxText() combo.append_text(_("Custom")) combo.set_active(0) for key, (name, gain) in sorted_presets: combo.append_text(name) combo.connect("changed", combo_changed) bbox = Gtk.HButtonBox() clear = Gtk.Button(stock=Gtk.STOCK_CLEAR) clear.connect('clicked', clicked_cb) bbox.pack_start(combo, True, True, 0) bbox.pack_start(clear, True, True, 0) vb.pack_start(bbox, True, True, 0) return vb �������������������������������������������������quodlibet-plugins-3.0.2/events/zeitgeist_client.py��������������������������������������������������0000644�0001750�0001750�00000004771�12161032160�022635� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import time try: import __builtin__ # zeitgeist overrides our gettext functions old_builtin = __builtin__.__dict__.copy() from zeitgeist.client import ZeitgeistClient from zeitgeist.datamodel import Event, Subject from zeitgeist.datamodel import Interpretation, Manifestation __builtin__.__dict__.update(old_builtin) except ImportError: from quodlibet.plugins import PluginImportException raise PluginImportException( _("Couldn't find 'zeitgeist' (Event logging service).")) from quodlibet.plugins.events import EventPlugin class Zeitgeist(EventPlugin): PLUGIN_ID = "zeitgeist" PLUGIN_NAME = _("Event Logging") PLUGIN_DESC = _("Send song events to the Zeitgeist event logging service") PLUGIN_ICON = 'gtk-network' PLUGIN_VERSION = "0.1" def enabled(self): self.client = ZeitgeistClient() self.__stopped_by_user = False def disabled(self): del self.client del self.__stopped_by_user def plugin_on_song_started(self, song): if self.__stopped_by_user: manifestation = Manifestation.USER_ACTIVITY else: manifestation = Manifestation.SCHEDULED_ACTIVITY self.__send_event(song, Interpretation.ACCESS_EVENT, manifestation) def plugin_on_song_ended(self, song, stopped): self.__stopped_by_user = stopped if stopped: manifestation = Manifestation.USER_ACTIVITY else: manifestation = Manifestation.SCHEDULED_ACTIVITY self.__send_event(song, Interpretation.LEAVE_EVENT, manifestation) def __send_event(self, song, interpretation, manifestation): if not song: return print_d("event: interpretation=%s, manifestation=%s" % (interpretation.__name__, manifestation.__name__)) subject = Subject.new_for_values( uri=song("~uri"), interpretation=Interpretation.AUDIO, manifestation=Manifestation.FILE_DATA_OBJECT, ) event = Event.new_for_values( timestamp=int(time.time() * 1000), interpretation=interpretation, manifestation=manifestation, actor="application://quodlibet.desktop", subjects=[subject] ) self.client.insert_event(event) �������quodlibet-plugins-3.0.2/events/gajim_status.py������������������������������������������������������0000644�0001750�0001750�00000014362�12173212464�021771� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005-2006 Sergey Fedoseev <fedoseev.sergey@gmail.com> # Copyright 2007 Simon Morgan <zen84964@zen.co.uk> # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. from string import join from gi.repository import Gtk import dbus import quodlibet from quodlibet.plugins.events import EventPlugin from quodlibet.parse import Pattern from quodlibet.qltk import Frame from quodlibet import config class GajimStatusMessage(EventPlugin): PLUGIN_ID = 'Gajim status message' PLUGIN_NAME = _('Gajim Status Message') PLUGIN_DESC = _("Change Gajim status message according to what " "you are currently listening to.") PLUGIN_VERSION = '0.7.4' c_accounts = __name__ + '_accounts' c_paused = __name__ + '_paused' c_statuses = __name__ + '_statuses' c_pattern = __name__ + '_pattern' def __init__(self): try: self.accounts = config.get('plugins', self.c_accounts).split() except: self.accounts = [] config.set('plugins', self.c_accounts, '') try: self.paused = config.getboolean('plugins', self.c_paused) except: self.paused = True config.set('plugins', self.c_paused, 'True') try: self.statuses = config.get('plugins', self.c_statuses).split() except: self.statuses = ['online', 'chat'] config.set('plugins', self.c_statuses, join(self.statuses)) try: self.pattern = config.get('plugins', self.c_pattern) except: self.pattern = '<artist> - <title>' config.set('plugins', self.c_pattern, self.pattern) quodlibet.quit_add(0, self.quit) self.interface = None self.current = '' def quit(self): if self.current != '': self.change_status(self.accounts, '') def change_status(self, enabled_accounts, status_message): if not self.interface: try: bus = dbus.SessionBus() obj = bus.get_object( 'org.gajim.dbus', '/org/gajim/dbus/RemoteObject') self.interface = dbus.Interface( obj, 'org.gajim.dbus.RemoteInterface') except dbus.DBusException: self.interface = None if self.interface: try: for account in self.interface.list_accounts(): status = self.interface.get_status(account) if enabled_accounts != [] and \ account not in enabled_accounts: continue if status in self.statuses: self.interface.change_status( status, status_message, account) except dbus.DBusException: self.interface = None def plugin_on_song_started(self, song): if song: self.current = Pattern(self.pattern) % song else: self.current = '' self.change_status(self.accounts, self.current) def plugin_on_paused(self): if self.paused and self.current != '': self.change_status(self.accounts, self.current + " [paused]") def plugin_on_unpaused(self): self.change_status(self.accounts, self.current) def accounts_changed(self, entry): self.accounts = entry.get_text().split() config.set('plugins', self.c_accounts, entry.get_text()) def pattern_changed(self, entry): self.pattern = entry.get_text() config.set('plugins', self.c_pattern, self.pattern) def paused_changed(self, c): config.set('plugins', self.c_paused, str(c.get_active())) def statuses_changed(self, b): if b.get_active() and b.get_name() not in self.statuses: self.statuses.append(b.get_name()) elif b.get_active() is False and b.get_name() in self.statuses: self.statuses.remove(b.get_name()) config.set('plugins', self.c_statuses, join(self.statuses)) def PluginPreferences(self, parent): vb = Gtk.VBox(spacing=3) pattern_box = Gtk.HBox(spacing=3) pattern_box.set_border_width(3) pattern = Gtk.Entry() pattern.set_text(self.pattern) pattern.connect('changed', self.pattern_changed) pattern_box.pack_start(Gtk.Label("Pattern:"), True, True, 0) pattern_box.pack_start(pattern, True, True, 0) accounts_box = Gtk.HBox(spacing=3) accounts_box.set_border_width(3) accounts = Gtk.Entry() accounts.set_text(join(self.accounts)) accounts.connect('changed', self.accounts_changed) accounts.set_tooltip_text("List accounts, separated by spaces, for " "changing status message. If none are specified, " "status message of all accounts will be changed.") accounts_box.pack_start(Gtk.Label("Accounts:"), True, True, 0) accounts_box.pack_start(accounts, True, True, 0) c = Gtk.CheckButton(label="Add '[paused]'") c.set_active(self.paused) c.connect('toggled', self.paused_changed) c.set_tooltip_text("If checked, '[paused]' will be added to " "status message on pause.") table = Gtk.Table() self.list = [] i = 0 j = 0 for status in ['online', 'offline', 'chat', 'away', 'xa', 'invisible']: button = Gtk.CheckButton(label=status) button.set_name(status) if status in self.statuses: button.set_active(True) button.connect('toggled', self.statuses_changed) self.list.append(button) table.attach(button, i, i + 1, j, j + 1) if i == 2: i = 0 j += 1 else: i += 1 vb.pack_start(pattern_box, True, True, 0) vb.pack_start(accounts_box, True, True, 0) vb.pack_start(c, True, True, 0) vb.pack_start(Frame(label="Statuses for which status message\n" "will be changed"), True, True, 0) vb.pack_start(table, True, True, 0) return vb ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/animosd.py�����������������������������������������������������������0000644�0001750�0001750�00000057521�12173213426�020734� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (C) 2012 Nick Boultbee, Thomas Vogt # Copyright (C) 2008 Andreas Bombe # Copyright (C) 2005 Michael Urman # Based on osd.py (C) 2005 Ton van den Heuvel, Joe Wreshnig # (C) 2004 Gustavo J. A. M. Carneiro # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # from collections import namedtuple import gi gi.require_version("PangoCairo", "1.0") from gi.repository import Gtk, GObject, GLib from gi.repository import Gdk, GdkPixbuf from gi.repository import Pango, PangoCairo import cairo from math import pi from quodlibet import config, qltk, app from quodlibet.qltk.textedit import PatternEdit from quodlibet import parse from quodlibet.plugins.events import EventPlugin from quodlibet.plugins import PluginConfigMixin from quodlibet.util.dprint import print_d def Label(text): l = Gtk.Label(label=text, use_underline=True) l.set_alignment(0.0, 0.5) return l class OSDWindow(Gtk.Window): __gsignals__ = { 'fade-finished': (GObject.SignalFlags.RUN_LAST, None, (bool,)), } def __init__(self, conf, song): Gtk.Window.__init__(self, Gtk.WindowType.POPUP) self.set_type_hint(Gdk.WindowTypeHint.NOTIFICATION) # for non-composite operation self.background_pixbuf = None self.titleinfo_surface = None screen = self.get_screen() # FIXME: GIPORT """cmap = screen.get_rgba_colormap() if cmap is None: cmap = screen.get_rgb_colormap() self.set_colormap(cmap)""" self.conf = conf self.iteration_source = None cover = song.find_cover() try: if cover is not None: cover = GdkPixbuf.Pixbuf.new_from_file(cover.name) except GLib.GError, gerror: print 'Error while loading cover image:', gerror.message except: from traceback import print_exc print_exc() # now calculate size of window mgeo = screen.get_monitor_geometry(conf.monitor) coverwidth = min(120, mgeo.width // 8) textwidth = mgeo.width - 2 * (conf.border + conf.margin) if cover is not None: textwidth -= coverwidth + conf.border coverheight = int(cover.get_height() * (float(coverwidth) / cover.get_width())) else: coverheight = 0 self.cover_pixbuf = cover layout = self.create_pango_layout('') layout.set_alignment((Pango.Alignment.LEFT, Pango.Alignment.CENTER, Pango.Alignment.RIGHT)[conf.align]) layout.set_spacing(Pango.SCALE * 7) layout.set_font_description(Pango.FontDescription(conf.font)) try: layout.set_markup(parse.XMLFromPattern(conf.string) % song) except parse.error: layout.set_markup("") layout.set_width(Pango.SCALE * textwidth) layoutsize = layout.get_pixel_size() if layoutsize[0] < textwidth: layout.set_width(Pango.SCALE * layoutsize[0]) layoutsize = layout.get_pixel_size() self.title_layout = layout winw = layoutsize[0] + 2 * conf.border if cover is not None: winw += coverwidth + conf.border winh = max(coverheight, layoutsize[1]) + 2 * conf.border self.set_default_size(winw, winh) rect = namedtuple("Rect", ["x", "y", "width", "height"]) rect.x = conf.border rect.y = (winh - coverheight) // 2 rect.width = coverwidth rect.height = coverheight self.cover_rectangle = rect winx = int((mgeo.width - winw) * conf.pos_x) winx = max(conf.margin, min(mgeo.width - conf.margin - winw, winx)) winy = int((mgeo.height - winh) * conf.pos_y) winy = max(conf.margin, min(mgeo.height - conf.margin - winh, winy)) self.move(winx + mgeo.x, winy + mgeo.y) def do_draw(self, cr): self.draw_title_info(cr) return # FIXME: GIPORT if self.is_composited(): # the simple case self.draw_title_info(cr) return # manual transparency rendering follows back_pbuf = self.background_pixbuf title_surface = self.titleinfo_surface walloc = self.get_allocation() wpos = self.get_position() if back_pbuf is None and 0: root = self.get_screen().get_root_window() back_pbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, walloc.width, walloc.height) back_pbuf.get_from_drawable(root, root.get_colormap(), wpos[0], wpos[1], 0, 0, walloc.width, walloc.height) self.background_pixbuf = back_pbuf if title_surface is None: title_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, walloc.width, walloc.height) titlecr = Gdk.cairo_create(self.get_window()) titlecr.set_source_surface(title_surface) self.draw_title_info(titlecr) cr.set_operator(cairo.OPERATOR_SOURCE) if back_pbuf is not None: cr.set_source_pixbuf(back_pbuf, 0, 0) else: cr.set_source_rgb(0.3, 0.3, 0.3) cr.paint() cr.set_operator(cairo.OPERATOR_OVER) cr.set_source_surface(title_surface, 0, 0) cr.paint_with_alpha(self.get_opacity()) def rounded_rectangle(self, cr, x, y, radius, width, height): cr.move_to(x + radius, y) cr.line_to(x + width - radius, y) cr.arc(x + width - radius, y + radius, radius, - 90.0 * pi / 180.0, 0.0 * pi / 180.0) cr.line_to(x + width, y + height - radius) cr.arc(x + width - radius, y + height - radius, radius, 0.0 * pi / 180.0, 90.0 * pi / 180.0) cr.line_to(x + radius, y + height) cr.arc(x + radius, y + height - radius, radius, 90.0 * pi / 180.0, 180.0 * pi / 180.0) cr.line_to(x, y + radius) cr.arc(x + radius, y + radius, radius, 180.0 * pi / 180.0, 270.0 * pi / 180.0) cr.close_path() def draw_conf_rect(self, cr, x, y, width, height, radius): if self.conf.corners != 0: self.rounded_rectangle(cr, x, y, radius, width, height) else: cr.rectangle(x, y, width, height) def draw_title_info(self, cr): do_shadow = (self.conf.shadow[0] != -1.0) do_outline = (self.conf.outline[0] != -1.0) cr.set_operator(cairo.OPERATOR_CLEAR) cr.rectangle(0, 0, *self.get_size()) cr.fill() cr.set_operator(cairo.OPERATOR_OVER) cr.set_source_rgba(*self.conf.fill) radius = min(25, self.conf.corners * min(*self.get_size())) self.draw_conf_rect(cr, 0, 0, self.get_size()[0], self.get_size()[1], radius) cr.fill() # draw border if do_outline: # Make border darker and more translucent than the fill f = self.conf.fill rgba = (f[0] / 1.25, f[1] / 1.25, f[2] / 1.25, f[3] / 2.0) cr.set_source_rgba(*rgba) self.draw_conf_rect(cr, 1, 1, self.get_size()[0] - 2, self.get_size()[1] - 2, radius) cr.set_line_width(2.0) cr.stroke() textx = self.conf.border if self.cover_pixbuf is not None: rect = self.cover_rectangle textx += rect.width + self.conf.border pbuf = self.cover_pixbuf transmat = cairo.Matrix() if do_shadow: cr.set_source_rgba(*self.conf.shadow) self.draw_conf_rect(cr, rect.x + 2, rect.y + 2, rect.width, rect.height, 0.6 * self.conf.corners * rect.width) cr.fill() if do_outline: cr.set_source_rgba(*self.conf.outline) self.draw_conf_rect(cr, rect.x, rect.y, rect.width, rect.height, 0.6 * self.conf.corners * rect.width) cr.stroke() Gdk.cairo_set_source_pixbuf(cr, pbuf, 0, 0) transmat.scale(pbuf.get_width() / float(rect.width), pbuf.get_height() / float(rect.height)) transmat.translate(-rect.x, -rect.y) cr.get_source().set_matrix(transmat) self.draw_conf_rect(cr, rect.x, rect.y, rect.width, rect.height, 0.6 * self.conf.corners * rect.width) cr.fill() PangoCairo.update_layout(cr, self.title_layout) height = self.title_layout.get_pixel_size()[1] texty = (self.get_size()[1] - height) // 2 if do_shadow: cr.set_source_rgba(*self.conf.shadow) cr.move_to(textx + 2, texty + 2) PangoCairo.show_layout(cr, self.title_layout) if do_outline: cr.set_source_rgba(*self.conf.outline) cr.move_to(textx, texty) PangoCairo.layout_path(cr, self.title_layout) cr.stroke() cr.set_source_rgb(*self.conf.text[:3]) cr.move_to(textx, texty) PangoCairo.show_layout(cr, self.title_layout) def fade_in(self): self.do_fade_inout(True) def fade_out(self): self.do_fade_inout(False) def do_fade_inout(self, fadein): fadein = bool(fadein) self.fading_in = fadein now = GObject.get_current_time() fraction = self.get_opacity() if not fadein: fraction = 1.0 - fraction self.fade_start_time = now - fraction * self.conf.fadetime if self.iteration_source is None: self.iteration_source = GLib.timeout_add(self.conf.ms, self.fade_iteration_callback) def fade_iteration_callback(self): delta = GObject.get_current_time() - self.fade_start_time fraction = delta / self.conf.fadetime if self.fading_in: self.set_opacity(fraction) else: self.set_opacity(1.0 - fraction) if not self.is_composited(): self.queue_draw() if fraction >= 1.0: self.iteration_source = None self.emit('fade-finished', self.fading_in) return False return True class AnimOsd(EventPlugin, PluginConfigMixin): PLUGIN_ID = "Animated On-Screen Display" PLUGIN_NAME = _("Animated On-Screen Display") PLUGIN_DESC = _("Display song information on your screen when it changes.") PLUGIN_VERSION = "1.2" # Retain compatibility with old configuration CONFIG_SECTION = 'animosd' def PluginPreferences(self, parent): def __coltofloat(x): return x / 65535.0 def __floattocol(x): return int(x * 65535) def cfg_set_tuple(name, t): string = " ".join(map(str, t)) #print_d("Writing config: %s=%s" % (name, string)) self.config_set("%s" % name, string) class ConfigLabel(Gtk.Label): def __init__(self, text, widget): super(ConfigLabel, self).__init__(text) self.set_use_underline(True) self.set_mnemonic_widget(widget) def set_text(button): color = button.get_color() color = map(__coltofloat, (color.red, color.green, color.blue, 0.0)) self.conf.text = tuple(color) cfg_set_tuple("text", self.conf.text) self.plugin_single_song(app.player.song) def set_fill(button): color = button.get_color() color = map(__coltofloat, (color.red, color.green, color.blue, button.get_alpha())) self.conf.fill = tuple(color) cfg_set_tuple("fill", self.conf.fill) self.plugin_single_song(app.player.song) def set_font(button): font = button.get_font_name() self.config_set("font", font) self.conf.font = font self.plugin_single_song(app.player.song) def change_delay(button): value = int(button.get_value() * 1000) self.config_set("delay", str(value)) self.conf.delay = value def change_monitor(button): """Monitor number config change handler""" value = int(button.get_value()) self.config_set("monitor", str(value)) self.conf.monitor = value self.plugin_single_song(app.player.song) def change_position(button): value = button.get_active() / 2.0 self.config_set("pos_y", str(value)) self.conf.pos_y = value self.plugin_single_song(app.player.song) def change_align(button): value = button.get_active() self.config_set("align", str(value)) self.conf.align = value self.plugin_single_song(app.player.song) def change_shadow(button): if button.get_active(): self.conf.shadow = (0.0, 0.0, 0.0, self.conf.fill[3]) else: self.conf.shadow = (-1.0, 0.0, 0.0, 0.0) cfg_set_tuple("shadow", self.conf.shadow) self.plugin_single_song(app.player.song) def change_outline(button): if button.get_active(): # Vary with fill alpha to create a smoother outline edge alpha = (min(1.0, self.conf.fill[3] * 1.25)) self.conf.outline = (0.1, 0.1, 0.1, alpha) else: self.conf.outline = (-1.0, 0.0, 0.0) cfg_set_tuple("outline", self.conf.outline) self.plugin_single_song(app.player.song) def change_rounded(button): if button.get_active(): self.conf.corners = 0.14 else: self.conf.corners = 0 self.config_set("corners", str(self.conf.corners)) self.plugin_single_song(app.player.song) def edit_string(button): w = PatternEdit(button, AnimOsd.conf.string) w.text = self.conf.string w.apply.connect_object_after('clicked', set_string, w) def set_string(window): value = window.text self.config_set("string", value) self.conf.string = value self.plugin_single_song(app.player.song) # Main VBox to return vb = Gtk.VBox(spacing=6) # Display vb2 = Gtk.VBox(spacing=3) hb = Gtk.HBox(spacing=6) # Set monitor to display OSD on if there's more than one monitor_cnt = Gdk.Screen.get_default().get_n_monitors() if monitor_cnt > 1: adj = Gtk.Adjustment(value=self.conf.monitor, lower=0, upper=monitor_cnt - 1, step_incr=1) monitor = Gtk.SpinButton(adjustment=adj) monitor.set_numeric(True) monitor.connect('value-changed', change_monitor) l2 = ConfigLabel("_Monitor:", monitor) hb.pack_start(l2, False, True, 0) hb.pack_start(monitor, False, True, 0) vb2.pack_start(hb, True, True, 0) else: self.conf.monitor = 0 # should be this by default anyway hb = Gtk.HBox(spacing=6) cb = Gtk.ComboBoxText() cb.append_text(_("Top of screen")) cb.append_text(_("Middle of screen")) cb.append_text(_("Bottom of screen")) cb.set_active(int(self.conf.pos_y * 2.0)) cb.connect('changed', change_position) lbl = ConfigLabel(_("_Position:"), cb) hb.pack_start(lbl, False, True, 0) hb.pack_start(cb, False, True, 0) vb2.pack_start(hb, False, True, 0) frame = qltk.Frame(label=_("Display"), child=vb2) frame.set_border_width(6) vb.pack_start(frame, False, True, 0) # Text vb2 = Gtk.VBox(spacing=6) hb = Gtk.HBox(spacing=6) font = Gtk.FontButton() font.set_font_name(self.conf.font) font.connect('font-set', set_font) lbl = ConfigLabel(_("_Font:"), font) hb.pack_start(lbl, False, True, 0) hb.pack_start(font, True, True, 0) vb2.pack_start(hb, False, True, 0) hb = Gtk.HBox(spacing=3) align = Gtk.ComboBoxText() align.append_text(_("Left")) align.append_text(_("Center")) align.append_text(_("Right")) align.set_active(self.conf.align) align.connect('changed', change_align) lbl = ConfigLabel(_("_Align text:"), align) hb.pack_start(lbl, False, True, 0) hb.pack_start(align, False, True, 0) vb2.pack_start(hb, False, True, 0) frame = qltk.Frame(label=_("Text"), child=vb2) frame.set_border_width(6) vb.pack_start(frame, False, True, 0) # Colors t = Gtk.Table(2, 2) t.set_col_spacings(6) t.set_row_spacings(3) b = Gtk.ColorButton(rgba=Gdk.RGBA(*map(__floattocol, self.conf.text))) l = Label(_("_Text:")) l.set_mnemonic_widget(b) l.set_use_underline(True) t.attach(l, 0, 1, 0, 1, xoptions=Gtk.AttachOptions.FILL) t.attach(b, 1, 2, 0, 1) b.connect('color-set', set_text) b = Gtk.ColorButton(color=Gdk.Color(*map(__floattocol, self.conf.fill[0:3]))) b.set_use_alpha(True) b.set_alpha(__floattocol(self.conf.fill[3])) b.connect('color-set', set_fill) l = Label(_("_Fill:")) l.set_mnemonic_widget(b) l.set_use_underline(True) t.attach(l, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL) t.attach(b, 1, 2, 1, 2) f = qltk.Frame(label=_("Colors"), child=t) f.set_border_width(6) vb.pack_start(f, False, False, 0) # Effects vb2 = Gtk.VBox(spacing=3) hb = Gtk.HBox(spacing=6) toggles = [ ("_Shadows", self.conf.shadow[0], change_shadow), ("_Outline", self.conf.outline[0], change_outline), ("Rou_nded Corners", self.conf.corners - 1, change_rounded), ] for (label, current, callback) in toggles: checkb = Gtk.CheckButton(label, use_underline=True) checkb.set_active(current != -1) checkb.connect("toggled", callback) hb.pack_start(checkb, True, True, 0) vb2.pack_start(hb, True, True, 0) hb = Gtk.HBox(spacing=6) timeout = Gtk.SpinButton(adjustment= Gtk.Adjustment(self.conf.delay / 1000.0, 0, 60, 0.1, 1.0, 0), climb_rate=0.1, digits=1) timeout.set_numeric(True) timeout.connect('value-changed', change_delay) l1 = ConfigLabel("_Delay:", timeout) hb.pack_start(l1, False, True, 0) hb.pack_start(timeout, False, True, 0) vb2.pack_start(hb, False, True, 0) frame = qltk.Frame(label=_("Effects"), child=vb2) frame.set_border_width(6) vb.pack_start(frame, False, True, 0) string = qltk.Button(_("Ed_it Display"), Gtk.STOCK_EDIT) string.connect('clicked', edit_string) vb.pack_start(string, False, True, 0) return vb class conf(object): # position of window 0--1 horizontal pos_x = 0.5 # position of window 0--1 vertical pos_y = 0.0 # never any closer to the screen edge than this margin = 50 # text/cover this far apart, from edge border = 20 # take this many seconds to fade in or out fadetime = 0.3 # wait this many milliseconds between steps ms = 40 # wait this many milliseconds before hiding delay = 2500 # monitor to display OSD on monitor = 0 # Font font = "Sans 22" # main font color. Alpha is ignored. text = (0.9, 0.9, 0.9, 0.0) # align text: 0 (left), 1 (center), 2 (right) align = 1 # rounded corner radius, 0 for angled corners corners = 0 # color,alpha or (-1.0,0.0,0.0,0.0) - surrounds text and cover outline = (-1.0, 0.0, 0.0, 0.2) # color,alpha or (-1.0,0.0,0.0) - shadows outline for text and cover shadow = (-1.0, 0.0, 0.0, 0.1) # color,alpha or None - fills rectangular area fill = (0.25, 0.25, 0.25, 0.5) # color,alpha or (-1.0,0.0,0.0,0.5) - borders the whole OSD bcolor = (0.0, 0.0, 0.0, 0.2) # song information to use - like in main window string = (r"<album|\<b\><album>\</b\><discnumber| - Disc " r"""<discnumber>><part| - \<b\><part>\</b\>><tracknumber| - <tracknumber>> >\<span weight='bold' size='large'\><title>\</span\> - <~length><version| \<small\>\<i\><version>\</i\>\</small\>><~people|" by <~people>>""") def __init__(self): self.__current_window = None def str_to_tuple(s): lst = map(float, s.split()) while len(lst) < 4: lst.append(0.0) return tuple(lst) config_map = [ ('text', str_to_tuple), ('fill', str_to_tuple), ('shadow', str_to_tuple), ('outline', str_to_tuple), ('bcolor', str_to_tuple), ('corners', float), ('font', None), ('align', int), ('delay', int), ('monitor', int), ('pos_y', float), ('string', None), ] for key, getconv in config_map: try: default = getattr(self.conf, key) except AttributeError: print_d("Unknown config item '%s'" % key) try: value = self.config_get(key, default) # This should never happen now that we default, but still.. if value is None: continue except (config.Error, ValueError): print_d("Couldn't find config item %s" % key) continue try: if getconv is not None: value = getconv(value) except Exception as err: print_d("Error parsing config for %s (%s) - defaulting to %r" % (key, err, default)) # Replace the invalid value if default is not None: self.config_set(key, default) else: setattr(self.conf, key, value) # for rapid debugging and for the preview def plugin_single_song(self, song): self.plugin_on_song_started(song) def plugin_on_song_started(self, song): if self.__current_window is not None: if self.__current_window.is_composited(): self.__current_window.fade_out() else: self.__current_window.hide() self.__current_window.destroy() if song is None: self.__current_window = None return window = OSDWindow(self.conf, song) window.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) window.connect('button-press-event', self.__buttonpress) window.connect('fade-finished', self.__fade_finished) self.__current_window = window window.set_opacity(0.0) window.show() window.fade_in() def start_fade_out(self, window): window.fade_out() return False def __buttonpress(self, window, event): window.hide() if self.__current_window is window: self.__current_window = None window.destroy() def __fade_finished(self, window, fade_in): if fade_in: GLib.timeout_add(self.conf.delay, self.start_fade_out, window) else: window.hide() if self.__current_window is window: self.__current_window = None # Delay destroy - apparently the hide does not quite register if # the destroy is done immediately. The compiz animation plugin # then sometimes triggers and causes undesirable effects while the # popup should already be invisible. GLib.timeout_add(1000, window.destroy) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/searchprovider.py����������������������������������������������������0000644�0001750�0001750�00000012354�12173213426�022315� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2013 Christoph Reiter <reiter.christoph@gmail.com> # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """ For this plugin to work Gnome Shell needs this file: /usr/share/gnome-shell/search-providers/quodlibet-search-provider.ini with the following content: [Shell Search Provider] DesktopId=quodlibet.desktop BusName=net.sacredchao.QuodLibet.SearchProvider ObjectPath=/net/sacredchao/QuodLibet/SearchProvider Version=2 """ import os import dbus import dbus.service from quodlibet import app from quodlibet import util from quodlibet.util.dbusutils import dbus_unicode_validate from quodlibet.plugins.events import EventPlugin from quodlibet.parse import Query from quodlibet.plugins import PluginImportException def get_gs_provider_files(): """Return all installed search provider files for Gnome Shell""" ini_files = [] for d in util.xdg_get_system_data_dirs(): path = os.path.join(d, "gnome-shell", "search-providers") try: for entry in os.listdir(path): if entry.endswith(".ini"): ini_files.append(os.path.join(path, entry)) except EnvironmentError: pass return ini_files def check_ini_installed(): """Raise if no Gnome Shell ini file for Quod Libet is found""" quodlibet_installed = False for path in get_gs_provider_files(): try: with open(path, "rb") as handle: if SearchProvider.BUS_NAME in handle.read(): quodlibet_installed = True break except EnvironmentError: pass if not quodlibet_installed: raise PluginImportException( _("No Gnome Shell search provider for " "Quod Libet installed.")) class GnomeSearchProvider(EventPlugin): PLUGIN_ID = "searchprovider" PLUGIN_NAME = _("Gnome Search Provider") PLUGIN_DESC = _("Allow the Gnome Shell to search the library") PLUGIN_ICON = "gtk-connect" PLUGIN_VERSION = "0.1" def enabled(self): self.obj = SearchProvider() def disabled(self): self.obj.remove_from_connection() del self.obj import gc gc.collect() ENTRY_ICON = (". GThemedIcon audio-mpeg gnome-mime-audio-mpeg " "audio-x-generic") def get_song_id(song): return str(id(song)) def get_songs_for_ids(library, ids): songs = [] ids = set(ids) for song in library: song_id = get_song_id(song) if song_id in ids: songs.append(song) ids.discard(song_id) if not ids: break return songs class SearchProvider(dbus.service.Object): PATH = "/net/sacredchao/QuodLibet/SearchProvider" BUS_NAME = "net.sacredchao.QuodLibet.SearchProvider" IFACE = "org.gnome.Shell.SearchProvider2" def __init__(self): bus = dbus.SessionBus() name = dbus.service.BusName(self.BUS_NAME, bus) super(SearchProvider, self).__init__(name, self.PATH) @dbus.service.method(IFACE, in_signature="as", out_signature="as") def GetInitialResultSet(self, terms): if terms: query = Query("") for term in terms: query &= Query(term) songs = filter(query.search, app.library) else: songs = app.library.values() ids = [get_song_id(s) for s in songs] return ids @dbus.service.method(IFACE, in_signature="asas", out_signature="as") def GetSubsearchResultSet(self, previous_results, terms): query = Query("") for term in terms: query &= Query(term) songs = get_songs_for_ids(app.library, previous_results) ids = [get_song_id(s) for s in songs if query.search(s)] return ids @dbus.service.method(IFACE, in_signature="as", out_signature="aa{sv}") def GetResultMetas(self, identifiers): metas = [] for song in get_songs_for_ids(app.library, identifiers): name = song("title") description = song("~artist~title") song_id = get_song_id(song) meta = dbus.Dictionary({ "name": dbus_unicode_validate(name), "id": song_id, "description": dbus_unicode_validate(description), "gicon": ENTRY_ICON, }, signature="ss") metas.append(meta) return metas @dbus.service.method(IFACE, in_signature="sasu") def ActivateResult(self, identifier, terms, timestamp): try: app.window.browser.filter_text(" ".join(terms)) except NotImplementedError: pass songs = get_songs_for_ids(app.library, [identifier]) if not songs: return if app.player.go_to(songs[0], True): app.player.paused = False @dbus.service.method(IFACE, in_signature="asu") def LaunchSearch(self, terms, timestamp): try: app.window.browser.filter_text(" ".join(terms)) except NotImplementedError: pass else: app.present() # the plugin is useless without the ini file... check_ini_installed() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/jep118.py������������������������������������������������������������0000644�0001750�0001750�00000002747�12161032160�020301� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os from quodlibet import util from quodlibet.const import USERDIR from quodlibet.plugins.events import EventPlugin outfile = os.path.join(USERDIR, "jabber") format = """\ <tune xmlns='http://jabber.org/protocol/tune'> <artist>%s</artist> <title>%s %s %d %d """ class JEP118(EventPlugin): PLUGIN_ID = "JEP-118" PLUGIN_NAME = "JEP-118" PLUGIN_DESC = "Output a Jabber User Tunes file to ~/.quodlibet/jabber" PLUGIN_ICON = 'gtk-save' PLUGIN_VERSION = "0.13" def plugin_on_song_started(self, song): if song is None: try: f = file(outfile, "w") f.write("") except EnvironmentError: pass else: f.close() else: try: f = file(outfile, "wb") f.write(format % ( util.escape(song.comma("artist")), util.escape(song.comma("title")), util.escape(song.comma("album")), song("~#track", 0), song.get("~#length", 0))) except EnvironmentError: pass else: f.close() quodlibet-plugins-3.0.2/events/mediaserver.py0000644000175000017500000005403112173212464021602 0ustar lazkalazka00000000000000# Copyright 2012,2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import tempfile from gi.repository import Gtk, GdkPixbuf import dbus import dbus.service from quodlibet import app from quodlibet.plugins.events import EventPlugin from quodlibet.parse import Pattern from quodlibet.util.uri import URI from quodlibet.util.dbusutils import DBusIntrospectable, DBusProperty from quodlibet.util.dbusutils import dbus_unicode_validate as unival BASE_PATH = "/org/gnome/UPnP/MediaServer2" BUS_NAME = "org.gnome.UPnP.MediaServer2.QuodLibet" class MediaServer(EventPlugin): PLUGIN_ID = "mediaserver" PLUGIN_NAME = _("UPnP AV Media Server") PLUGIN_DESC = _("Exposes all albums to the Rygel UPnP Media Server " "through the MediaServer2 D-Bus interface") PLUGIN_ICON = Gtk.STOCK_CONNECT PLUGIN_VERSION = "0.1" def enabled(self): entry = EntryObject() albums = AlbumsObject(entry, app.library) song = SongObject(app.library, [albums]) icon = Icon(entry) self.objects = [entry, albums, song, icon] def disabled(self): for obj in self.objects: obj.remove_from_connection() for obj in self.objects: obj.destroy() del self.objects import gc gc.collect() class DBusPropertyFilter(DBusProperty): """Adds some methods to support the MediaContainer property filtering.""" def get_properties_for_filter(self, interface, filter_): props = self.get_properties(interface) if "*" not in filter_: props = [p for p in props if p[1] in filter_] return props def get_values(self, properties, path="/"): result = {} for iface, prop in properties: result[prop] = self.get_value(iface, prop, path) return result class MediaContainer(object): IFACE = "org.gnome.UPnP.MediaContainer2" ISPEC_PROP = """ """ ISPEC = """ """ def __init__(self, optional=tuple()): self.set_introspection(MediaContainer.IFACE, MediaContainer.ISPEC) props = ["ChildCount", "ItemCount", "ContainerCount", "Searchable"] props += list(optional) self.set_properties(MediaContainer.IFACE, MediaContainer.ISPEC_PROP, wl=props) self.implement_interface(MediaContainer.IFACE, MediaObject.IFACE) def emit_updated(self, path="/"): self.Updated(rel=path) @dbus.service.method(IFACE, in_signature="uuas", out_signature="aa{sv}", rel_path_keyword="path") def ListChildren(self, offset, max_, filter_, path): if self.SUPPORTS_MULTIPLE_OBJECT_PATHS: return self.list_children(offset, max_, filter_, path) return self.list_children(offset, max_, filter_) @dbus.service.method(IFACE, in_signature="uuas", out_signature="aa{sv}", rel_path_keyword="path") def ListContainers(self, offset, max_, filter_, path): if self.SUPPORTS_MULTIPLE_OBJECT_PATHS: return self.list_containers(offset, max_, filter_, path) return self.list_containers(offset, max_, filter_) @dbus.service.method(IFACE, in_signature="uuas", out_signature="aa{sv}", rel_path_keyword="path") def ListItems(self, offset, max_, filter_, path): if self.SUPPORTS_MULTIPLE_OBJECT_PATHS: return self.list_items(offset, max_, filter_, path) return self.list_items(offset, max_, filter_) @dbus.service.method(IFACE, in_signature="suuas", out_signature="aa{sv}", rel_path_keyword="path") def SearchObjects(self, query, offset, max_, filter_, path): return [] @dbus.service.signal(IFACE, rel_path_keyword="rel") def Updated(self, rel=""): pass class MediaObject(object): IFACE = "org.gnome.UPnP.MediaObject2" ISPEC = """ """ parent = None def __init__(self, parent=None): self.set_properties(MediaObject.IFACE, MediaObject.ISPEC) self.parent = parent or self class MediaItem(object): IFACE = "org.gnome.UPnP.MediaItem2" ISPEC = """ """ def __init__(self, optional=tuple()): props = ["URLs", "MIMEType"] + list(optional) self.set_properties(MediaItem.IFACE, MediaItem.ISPEC, wl=props) self.implement_interface(MediaItem.IFACE, MediaObject.IFACE) class EntryObject(MediaContainer, MediaObject, DBusPropertyFilter, DBusIntrospectable, dbus.service.Object): PATH = BASE_PATH + "/QuodLibet" DISPLAY_NAME = "@REALNAME@'s Quod Libet on @HOSTNAME@" def __init__(self): self.__sub = [] DBusIntrospectable.__init__(self) DBusPropertyFilter.__init__(self) MediaObject.__init__(self) MediaContainer.__init__(self, optional=["Icon"]) bus = dbus.SessionBus() name = dbus.service.BusName(BUS_NAME, bus) dbus.service.Object.__init__(self, bus, self.PATH, name) def get_property(self, interface, name): if interface == MediaContainer.IFACE: if name == "ChildCount": return len(self.__sub) elif name == "ItemCount": return 0 elif name == "ContainerCount": return len(self.__sub) elif name == "Searchable": return False elif name == "Icon": return Icon.PATH elif interface == MediaObject.IFACE: if name == "Parent": return self.parent.PATH elif name == "Type": return "container" elif name == "Path": return self.PATH elif name == "DisplayName": return self.DISPLAY_NAME def destroy(self): # break cycle del self.__sub del self.parent def register_child(self, child): self.__sub.append(child) self.emit_properties_changed(MediaContainer.IFACE, ["ChildCount", "ContainerCount"]) def list_containers(self, offset, max_, filter_): props = self.get_properties_for_filter(MediaContainer.IFACE, filter_) end = (max_ and offset + max_) or None result = [] for sub in self.__sub[offset:end]: result.append(sub.get_values(props)) return result list_children = list_containers def list_items(self, offset, max_, filter_): return [] SUPPORTED_SONG_PROPERTIES = ("Size", "Artist", "Album", "Date", "Genre", "Duration", "TrackNumber") class DummySongObject(MediaItem, MediaObject, DBusPropertyFilter, DBusIntrospectable): """ A dummy song object that is not exported on the bus, but supports the usual interfaces. You need to assign a real song before using it, and have to pass a path prefix. The path of the song is /org/gnome/UPnP/MediaServer2/Song//SongID This lets us reconstruct the original parent path: /org/gnome/UPnP/MediaServer2/ atm. a prefix can look like "Albums/123456" """ SUPPORTS_MULTIPLE_OBJECT_PATHS = False __pattern = Pattern( ".>. ") def __init__(self, parent): DBusIntrospectable.__init__(self) DBusPropertyFilter.__init__(self) MediaObject.__init__(self, parent) MediaItem.__init__(self, optional=SUPPORTED_SONG_PROPERTIES) def set_song(self, song, prefix): self.__song = song self.__prefix = prefix def get_property(self, interface, name): if interface == MediaObject.IFACE: if name == "Parent": return BASE_PATH + "/" + self.__prefix elif name == "Type": return "music" elif name == "Path": path = SongObject.PATH path += "/" + self.__prefix + "/" + str(id(self.__song)) return path elif name == "DisplayName": return unival(self.__song.comma("title")) elif interface == MediaItem.IFACE: if name == "URLs": return [self.__song("~uri")] elif name == "MIMEType": mimes = self.__song.mimes return mimes and mimes[0] elif name == "Size": return self.__song("~#filesize") elif name == "Artist": return unival(self.__song.comma("artist")) elif name == "Album": return unival(self.__song.comma("album")) elif name == "Date": return unival(self.__song.comma("date")) elif name == "Genre": return unival(self.__song.comma("genre")) elif name == "Duration": return self.__song("~#length") elif name == "TrackNumber": return self.__song("~#track", 0) class DummyAlbumObject(MediaContainer, MediaObject, DBusPropertyFilter, DBusIntrospectable): SUPPORTS_MULTIPLE_OBJECT_PATHS = False __pattern = Pattern("<albumartist|<~albumartist~album>|<~artist~album>>") def __init__(self, parent): DBusIntrospectable.__init__(self) DBusPropertyFilter.__init__(self) MediaObject.__init__(self, parent) MediaContainer.__init__(self) self.__song = DummySongObject(self) def get_dummy(self, song): self.__song.set_song(song, "Albums/" + str(id(self.__album))) return self.__song def set_album(self, album): self.__album = album self.PATH = self.parent.PATH + "/" + str(id(album)) def get_property(self, interface, name): if interface == MediaContainer.IFACE: if name == "ChildCount" or name == "ItemCount": return len(self.__album.songs) elif name == "ContainerCount": return 0 elif name == "Searchable": return False elif interface == MediaObject.IFACE: if name == "Parent": return self.parent.PATH elif name == "Type": return "container" elif name == "Path": return self.PATH elif name == "DisplayName": return unival(self.__pattern % self.__album) def list_containers(self, offset, max_, filter_): return [] def list_items(self, offset, max_, filter_): songs = sorted(self.__album.songs, key=lambda s: s.sort_key) dummy = self.get_dummy(None) props = dummy.get_properties_for_filter(MediaItem.IFACE, filter_) end = (max_ and offset + max_) or None result = [] for song in songs[offset:end]: result.append(self.get_dummy(song).get_values(props)) return result list_children = list_items class SongObject(MediaItem, MediaObject, DBusProperty, DBusIntrospectable, dbus.service.FallbackObject): PATH = BASE_PATH + "/Song" def __init__(self, library, users): DBusIntrospectable.__init__(self) DBusProperty.__init__(self) MediaObject.__init__(self, None) MediaItem.__init__(self, optional=SUPPORTED_SONG_PROPERTIES) bus = dbus.SessionBus() self.ref = dbus.service.BusName(BUS_NAME, bus) dbus.service.FallbackObject.__init__(self, bus, self.PATH) self.__library = library self.__map = dict((id(v), v) for v in self.__library.itervalues()) self.__reverse = dict((v, k) for k, v in self.__map.iteritems()) self.__song = DummySongObject(self) self.__users = users signals = [ ("changed", self.__songs_changed), ("removed", self.__songs_removed), ("added", self.__songs_added), ] self.__sigs = map(lambda (s, f): self.__library.connect(s, f), signals) def __songs_changed(self, lib, songs): # We don't know what changed, so get all properties props = [p[1] for p in self.get_properties(MediaItem.IFACE)] for song in songs: song_id = str(id(song)) # https://code.google.com/p/quodlibet/issues/detail?id=1127 # XXX: Something is emitting wrong changed events.. # ignore song_ids we don't know for now if song_id not in self.__map: continue for user in self.__users: # ask the user for the prefix whith which the song is used prefix = user.get_prefix(song) path = "/" + prefix + "/" + song_id self.emit_properties_changed(MediaItem.IFACE, props, path) def __songs_added(self, lib, songs): for song in songs: new_id = id(song) self.__map[new_id] = song self.__reverse[song] = new_id def __songs_removed(self, lib, songs): for song in songs: del self.__map[self.__reverse[song]] del self.__reverse[song] def destroy(self): map(self.__library.disconnect, self.__sigs) def get_dummy(self, song, prefix): self.__song.set_song(song, prefix) return self.__song def get_property(self, interface, name, path): # extract the prefix prefix, song_id = path[1:].rsplit("/", 1) song = self.__map[int(song_id)] return self.get_dummy(song, prefix).get_property(interface, name) class AlbumsObject(MediaContainer, MediaObject, DBusPropertyFilter, DBusIntrospectable, dbus.service.FallbackObject): PATH = BASE_PATH + "/Albums" DISPLAY_NAME = "Albums" def __init__(self, parent, library): DBusIntrospectable.__init__(self) DBusPropertyFilter.__init__(self) MediaObject.__init__(self, parent) MediaContainer.__init__(self) bus = dbus.SessionBus() self.ref = dbus.service.BusName(BUS_NAME, bus) dbus.service.FallbackObject.__init__(self, bus, self.PATH) parent.register_child(self) self.__library = library.albums self.__library.load() self.__map = dict((id(v), v) for v in self.__library.itervalues()) self.__reverse = dict((v, k) for k, v in self.__map.iteritems()) signals = [ ("changed", self.__albums_changed), ("removed", self.__albums_removed), ("added", self.__albums_added), ] self.__sigs = map(lambda (s, f): self.__library.connect(s, f), signals) self.__dummy = DummyAlbumObject(self) def get_dummy(self, album): self.__dummy.set_album(album) return self.__dummy def get_path_dummy(self, path): return self.get_dummy(self.__map[int(path[1:])]) def __albums_changed(self, lib, albums): for album in albums: rel_path = "/" + str(id(album)) self.emit_updated(rel_path) self.emit_properties_changed( MediaContainer.IFACE, ["ChildCount", "ItemCount", "DisplayName"], rel_path) def __albums_added(self, lib, albums): for album in albums: new_id = id(album) self.__map[new_id] = album self.__reverse[album] = new_id self.emit_updated() self.emit_properties_changed(MediaContainer.IFACE, ["ChildCount", "ContainerCount"]) def __albums_removed(self, lib, albums): for album in albums: del self.__map[self.__reverse[album]] del self.__reverse[album] self.emit_updated() self.emit_properties_changed(MediaContainer.IFACE, ["ChildCount", "ContainerCount"]) def get_prefix(self, song): album = self.__library[song.album_key] return "Albums/" + str(id(album)) def destroy(self): map(self.__library.disconnect, self.__sigs) def __get_albums_property(self, interface, name): if interface == MediaContainer.IFACE: if name == "ChildCount": return len(self.__library) elif name == "ItemCount": return 0 elif name == "ContainerCount": return len(self.__library) elif name == "Searchable": return False elif interface == MediaObject.IFACE: if name == "Parent": return self.parent.PATH elif name == "Type": return "container" elif name == "Path": return self.PATH elif name == "DisplayName": return self.DISPLAY_NAME def get_property(self, interface, name, path): if path == "/": return self.__get_albums_property(interface, name) return self.get_path_dummy(path).get_property(interface, name) def __list_albums(self, offset, max_, filter_): props = self.get_properties_for_filter(MediaContainer.IFACE, filter_) albums = sorted(self.__library, key=lambda a: a.sort) end = (max_ and offset + max_) or None result = [] for album in albums[offset:end]: result.append(self.get_dummy(album).get_values(props)) return result def list_containers(self, offset, max_, filter_, path): if path == "/": return self.__list_albums(offset, max_, filter_) return [] def list_items(self, offset, max_, filter_, path): if path != "/": return self.get_path_dummy(path).list_items(offset, max_, filter_) return [] def list_children(self, offset, max_, filter_, path): if path == "/": return self.__list_albums(offset, max_, filter_) return self.get_path_dummy(path).list_children(offset, max_, filter_) class Icon(MediaItem, MediaObject, DBusProperty, DBusIntrospectable, dbus.service.Object): PATH = BASE_PATH + "/Icon" SIZE = 160 def __init__(self, parent): DBusIntrospectable.__init__(self) DBusProperty.__init__(self) MediaObject.__init__(self, parent=parent) MediaItem.__init__(self, optional=["Height", "Width", "ColorDepth"]) bus = dbus.SessionBus() name = dbus.service.BusName(BUS_NAME, bus) dbus.service.Object.__init__(self, bus, self.PATH, name) # https://bugzilla.gnome.org/show_bug.cgi?id=669677 self.implement_interface("org.gnome.UPnP.MediaItem1", MediaItem.IFACE) # load into a pixbuf theme = Gtk.IconTheme.get_default() pixbuf = theme.load_icon("quodlibet", Icon.SIZE, 0) # make sure the size is right pixbuf = pixbuf.scale_simple(Icon.SIZE, Icon.SIZE, GdkPixbuf.InterpType.BILINEAR) self.__depth = pixbuf.get_bits_per_sample() # save and keep reference self.__f = f = tempfile.NamedTemporaryFile() pixbuf.savev(f.name, "png", [], []) def get_property(self, interface, name): if interface == MediaObject.IFACE: if name == "Parent": return EntryObject.PATH elif name == "Type": return "image" elif name == "Path": return Icon.PATH elif name == "DisplayName": return "I'm an icon \o/" elif interface == MediaItem.IFACE: if name == "URLs": return [URI.frompath(self.__f.name)] elif name == "MIMEType": return "image/png" elif name == "Width" or name == "Height": return Icon.SIZE elif name == "ColorDepth": return self.__depth def destroy(self): pass �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/screensaver.py�������������������������������������������������������0000644�0001750�0001750�00000003554�12173212464�021620� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2011 Christoph Reiter <christoph.reiter@gmx.at> # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. from gi.repository import Gtk import dbus from quodlibet import app from quodlibet.plugins.events import EventPlugin class ScreensaverPause(EventPlugin): PLUGIN_ID = "screensaver_pause" PLUGIN_NAME = _("Screensaver Pause") PLUGIN_DESC = _("Pause while the GNOME screensaver is active.") PLUGIN_ICON = Gtk.STOCK_MEDIA_PAUSE PLUGIN_VERSION = "0.2" DBUS_NAME = "org.gnome.ScreenSaver" DBUS_INTERFACE = "org.gnome.ScreenSaver" DBUS_PATH = "/org/gnome/ScreenSaver" __was_paused = False __interface = None def __screensaver_changed(self, active): if active: self.__was_paused = app.player.paused app.player.paused = True elif not self.__was_paused: app.player.paused = False def __remove_interface(self): if self.__interface: self.__sig.remove() self.__interface = None def __owner_changed(self, owner): if not owner: self.__remove_interface() elif not self.__interface: bus = dbus.SessionBus() obj = bus.get_object(self.DBUS_NAME, self.DBUS_PATH) iface = dbus.Interface(obj, self.DBUS_INTERFACE) self.__sig = iface.connect_to_signal("ActiveChanged", self.__screensaver_changed) self.__interface = iface def enabled(self): bus = dbus.SessionBus() self.__watch = bus.watch_name_owner(self.DBUS_NAME, self.__owner_changed) def disabled(self): self.__watch.cancel() self.__remove_interface() ����������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/events/qlscrobbler.py�������������������������������������������������������0000644�0001750�0001750�00000051110�12173212464�021601� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# QLScrobbler: an Audioscrobbler client plugin for Quod Libet. # version 0.11 # (C) 2005-2012 by Joshua Kwan <joshk@triplehelix.org>, # Joe Wreschnig <piman@sacredchao.net>, # Franz Pletyz <fpletz@franz-pletz.org>, # Nicholas J. Michalek <djphazer@gmail.com>, # Steven Robertson <steven@strobe.cc> # Nick Boultbee <nick.boultbee@gmail.com> # Licensed under GPLv2. See Quod Libet's COPYING for more information. from httplib import HTTPException import cPickle as pickle import os import threading import time import urllib import urllib2 from gi.repository import Gtk, GLib try: from hashlib import md5 except ImportError: from md5 import md5 import quodlibet from quodlibet import config, const, app, parse, util, qltk from quodlibet.plugins.events import EventPlugin from quodlibet.plugins import PluginConfigMixin from quodlibet.qltk.entry import ValidatingEntry, UndoEntry from quodlibet.qltk.msg import Message from quodlibet.util.dprint import print_d SERVICES = { 'Last.fm': 'http://post.audioscrobbler.com/', 'Libre.fm': 'http://turtle.libre.fm/' } DEFAULT_SERVICE = 'Last.fm' DEFAULT_TITLEPAT = '<title><version| (<version>)>' DEFAULT_ARTISTPAT = '<artist|<artist>|<composer|<composer>|<performer>>>' def config_get(key, default=''): """Returns value for 'key' from config. If key is missing *or empty*, return default.""" try: return (config.get("plugins", "scrobbler_%s" % key) or default) except config.Error: return default class QLSubmitQueue(PluginConfigMixin): """Manages the submit queue for scrobbles. Works independently of the QLScrobbler plugin being enabled; other plugins may use submit() to queue songs for scrobbling. """ CLIENT = "qlb" PROTOCOL_VERSION = "1.2" DUMP = os.path.join(const.USERDIR, "scrobbler_cache") # This must be the kept the same as `QLScrobbler` CONFIG_SECTION = "scrobbler" # These objects are shared across instances, to allow other plugins to # queue scrobbles in future versions of QL queue = [] changed_event = threading.Event() def config_get_url(self): """Gets the URL for the currently configured service. This logic was used often enough to be split out from generic config""" # TODO: share this between the classes better service = self.config_get('service', DEFAULT_SERVICE) if service in SERVICES: return SERVICES[service] else: return self.config_get('url') def set_nowplaying(self, song): """Send a Now Playing notification.""" formatted = self._format_song(song) if not formatted or self.nowplaying_song == formatted: return self.nowplaying_song = formatted self.nowplaying_sent = False self.changed() def submit(self, song, timestamp=0): """Submit a song. If 'timestamp' is 0, the current time will be used.""" formatted = self._format_song(song) if formatted is None: return if timestamp > 0: formatted['i'] = str(timestamp) elif timestamp == 0: formatted['i'] = str(int(time.time())) else: # TODO: Forging timestamps for submission from PMPs return self.queue.append(formatted) self.changed() def _format_song(self, song): """Returns a dict with the keys formatted as required by spec.""" store = { "l": str(song.get("~#length", 0)), "n": str(song("~#track")), "b": song.comma("album"), "m": song("musicbrainz_trackid"), "t": self.titlepat.format(song), "a": self.artpat.format(song), } # Spec requires title and artist at minimum if not (store.get("a") and store.get("t")): return None return store def __init__(self): self.nowplaying_song = None self.nowplaying_sent = False self.sessionid = None self.broken = False self.username, self.password, self.base_url = ('', '', '') # These need to be set early for _format_song to work self.titlepat = parse.Pattern( self.config_get('titlepat', "") or DEFAULT_TITLEPAT) self.artpat = parse.Pattern( self.config_get('artistpat', "") or DEFAULT_ARTISTPAT) try: disk_queue_file = open(self.DUMP, 'r') disk_queue = pickle.load(disk_queue_file) disk_queue_file.close() os.unlink(self.DUMP) self.queue += disk_queue except Exception: pass @classmethod def dump_queue(klass): if klass.queue: try: disk_queue_file = open(klass.DUMP, 'w') pickle.dump(klass.queue, disk_queue_file) disk_queue_file.close() except IOError: pass return 0 def _check_config(self): user = self.config_get('username') passw = md5(self.config_get('password')).hexdigest() url = self.config_get_url() if not user or not passw or not url: if self.queue and not self.broken: self.quick_dialog("Please visit the Plugins window to set " "QLScrobbler up. Until then, songs will not be " "submitted.", Gtk.MessageType.INFO) self.broken = True elif (self.username, self.password, self.base_url) != (user, passw, url): self.username, self.password, self.base_url = (user, passw, url) self.broken = False self.handshake_sent = False self.offline = self.config_get_bool('offline') self.titlepat = parse.Pattern( self.config_get('titlepat', "") or DEFAULT_TITLEPAT) self.artpat = parse.Pattern( self.config_get('artistpat', "") or DEFAULT_ARTISTPAT) def changed(self): """Signal that settings or queue contents were changed.""" self._check_config() if not self.broken and not self.offline and (self.queue or (self.nowplaying_song and not self.nowplaying_sent)): self.changed_event.set() return self.changed_event.clear() def run(self): """Submit songs from the queue. Call from a daemon thread.""" # The spec calls for exponential backoff of failed handshakes, with a # minimum of 1m and maximum of 120m delay between attempts. self.handshake_sent = False self.handshake_event = threading.Event() self.handshake_event.set() self.handshake_delay = 1 self.failures = 0 while True: self.changed_event.wait() if not self.handshake_sent: self.handshake_event.wait() if self.send_handshake(): self.failures = 0 self.handshake_delay = 1 self.handshake_sent = True else: self.handshake_event.clear() self.handshake_delay = min(self.handshake_delay * 2, 120) GLib.timeout_add(self.handshake_delay * 60 * 1000, self.handshake_event.set) continue self.changed_event.wait() if self.queue: if self.send_submission(): self.failures = 0 else: self.failures += 1 if self.failures >= 3: self.handshake_sent = False elif self.nowplaying_song and not self.nowplaying_sent: self.send_nowplaying() self.nowplaying_sent = True else: # Nothing left to do; wait until something changes self.changed_event.clear() def send_handshake(self, show_dialog=False): # construct url stamp = int(time.time()) auth = md5(self.password + str(stamp)).hexdigest() url = "%s/?hs=true&p=%s&c=%s&v=%s&u=%s&a=%s&t=%d" % ( self.base_url, self.PROTOCOL_VERSION, self.CLIENT, QLScrobbler.PLUGIN_VERSION, self.username, auth, stamp) print_d("Sending handshake to service.") try: resp = urllib2.urlopen(url) except (IOError, HTTPException): if show_dialog: self.quick_dialog( "Could not contact service '%s'." % util.escape(self.base_url), Gtk.MessageType.ERROR) else: print_d("Could not contact service. Queueing submissions.") return False except ValueError: self.quick_dialog("Authentication failed: invalid URL.", Gtk.MessageType.ERROR) self.broken = True return False # check response lines = resp.read().rstrip().split("\n") status = lines.pop(0) print_d("Handshake status: %s" % status) if status == "OK": self.session_id, self.nowplaying_url, self.submit_url = lines self.handshake_sent = True print_d("Session ID: %s, NP URL: %s, Submit URL: %s" % ( self.session_id, self.nowplaying_url, self.submit_url)) return True elif status == "BADAUTH": self.quick_dialog("Authentication failed: Invalid username '%s' " "or bad password." % util.escape(self.username), Gtk.MessageType.ERROR) self.broken = True elif status == "BANNED": self.quick_dialog("Client is banned. Contact the author.", Gtk.MessageType.ERROR) self.broken = True elif status == "BADTIME": self.quick_dialog("Wrong system time. Submissions may fail until " "it is corrected.", Gtk.MessageType.ERROR) else: # "FAILED" self.quick_dialog(status, Gtk.MessageType.ERROR) self.changed() return False def _check_submit(self, url, data): data_str = urllib.urlencode(data) try: resp = urllib2.urlopen(url, data_str) except (IOError, HTTPException): print_d("Audioscrobbler server not responding, will try later.") return False resp_save = resp.read() status = resp_save.rstrip().split("\n")[0] print_d("Submission status: %s" % status) if status == "OK": return True elif status == "BADSESSION": self.handshake_sent = False return False else: return False def send_submission(self): data = {'s': self.session_id} to_submit = self.queue[:min(len(self.queue), 50)] for idx, song in enumerate(to_submit): for key, val in song.items(): data['%s[%d]' % (key, idx)] = val.encode('utf-8') data['o[%d]' % idx] = 'P' data['r[%d]' % idx] = '' print_d('Submitting song(s): %s' % ('\n\t'.join(['%s - %s' % (s['a'], s['t']) for s in to_submit]))) if self._check_submit(self.submit_url, data): del self.queue[:len(to_submit)] return True else: return False def send_nowplaying(self): data = {'s': self.session_id} for key, val in self.nowplaying_song.items(): data[key] = val.encode('utf-8') print_d('Now playing song: %s - %s' % (self.nowplaying_song['a'], self.nowplaying_song['t'])) return self._check_submit(self.nowplaying_url, data) def quick_dialog_helper(self, dialog_type, msg): dialog = Message(dialog_type, app.window, "QLScrobbler", msg) dialog.connect('response', lambda dia, resp: dia.destroy()) dialog.show() def quick_dialog(self, msg, dialog_type): GLib.idle_add(self.quick_dialog_helper, dialog_type, msg) class QLScrobbler(EventPlugin, PluginConfigMixin): PLUGIN_ID = "QLScrobbler" PLUGIN_NAME = _("AudioScrobbler Submission") PLUGIN_DESC = _("Audioscrobbler client for Last.fm, Libre.fm and other " "Audioscrobbler services.") PLUGIN_ICON = Gtk.STOCK_CONNECT PLUGIN_VERSION = "0.12" # Retain original config section CONFIG_SECTION = "scrobbler" def __init__(self): self.__enabled = False self.queue = QLSubmitQueue() queue_thread = threading.Thread(None, self.queue.run) queue_thread.setDaemon(True) queue_thread.start() self.start_time = 0 self.unpaused_time = 0 self.elapsed = 0 self.nowplaying = None self.exclude = self.config_get('exclude') # Set up exit hook to dump queue quodlibet.quit_add(0, self.queue.dump_queue) def config_get_url(self): """Gets the URL for the currently configured service. This logic was used often enough to be split out from generic config""" service = self.config_get('service', DEFAULT_SERVICE) if service in SERVICES: return SERVICES[service] else: return self.config_get('url') def plugin_on_song_ended(self, song, stopped): if song is None or not self.__enabled: return if self.unpaused_time > 0: self.elapsed += time.time() - self.unpaused_time # Spec: * don't submit when song length < 00:30 # * submit at end of playback (not in the middle, as with v1.1) # * submit if played for >= .5*length or >= 240s # we check 'elapsed' rather than 'length' to work around wrong ~#length if self.elapsed < 30: return if self.elapsed < 240 and self.elapsed <= .5 * song.get("~#length", 0): return print_d("Checking against filter %s" % self.exclude) if self.exclude and parse.Query(self.exclude).search(song): print_d("Not submitting: %s" % song("~artist~title")) return self.queue.submit(song, self.start_time) def song_excluded(self, song): if self.exclude and parse.Query(self.exclude).search(song): print_d("%s is excluded by %s" % (song("~artist~title"), self.exclude)) return True return False def send_nowplaying(self, song): if not self.song_excluded(song): self.queue.set_nowplaying(song) def plugin_on_song_started(self, song): if song is None: return self.start_time = int(time.time()) if app.player.paused: self.unpaused_time = 0 else: self.unpaused_time = time.time() self.elapsed = 0 if self.__enabled and not app.player.paused: self.send_nowplaying(song) else: self.nowplaying = song def plugin_on_paused(self): if self.unpaused_time > 0: self.elapsed += time.time() - self.unpaused_time self.unpaused_time = 0 def plugin_on_unpaused(self): self.unpaused_time = time.time() if self.__enabled and self.nowplaying: self.send_nowplaying(self.nowplaying) self.nowplaying = None def enabled(self): self.__enabled = True print_d("Plugin enabled - accepting new songs.") def disabled(self): self.__enabled = False print_d("Plugin disabled - not accepting any new songs.") def PluginPreferences(self, parent): def changed(entry, key): if entry.get_property('sensitive'): config.set("plugins", "scrobbler_" + key, entry.get_text()) def combo_changed(widget, urlent): service = widget.get_active_text() config.set("plugins", "scrobbler_service", service) urlent.set_sensitive((service not in SERVICES)) urlent.set_text(self.config_get_url()) def check_login(*args): queue = QLSubmitQueue() queue.changed() status = queue.send_handshake(show_dialog=True) if status: queue.quick_dialog("Authentication successful.", Gtk.MessageType.INFO) box = Gtk.VBox(spacing=12) # first frame table = Gtk.Table(5, 2) table.set_col_spacings(6) table.set_row_spacings(6) labels = [] label_names = [_("_Service:"), _("_URL:"), _("User_name:"), _("_Password:")] for idx, label in enumerate(map(Gtk.Label, label_names)): label.set_alignment(0.0, 0.5) label.set_use_underline(True) table.attach(label, 0, 1, idx, idx + 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) labels.append(label) row = 0 service_combo = Gtk.ComboBoxText() table.attach(service_combo, 1, 2, row, row + 1) cur_service = self.config_get('service') for idx, serv in enumerate(sorted(SERVICES.keys()) + ["Other..."]): service_combo.append_text(serv) if cur_service == serv: service_combo.set_active(idx) if service_combo.get_active() == -1: service_combo.set_active(0) labels[row].set_mnemonic_widget(service_combo) row += 1 # url entry = UndoEntry() entry.set_text(self.config_get('url')) entry.connect('changed', changed, 'url') service_combo.connect('changed', combo_changed, entry) service_combo.emit('changed') table.attach(entry, 1, 2, row, row + 1) labels[row].set_mnemonic_widget(entry) row += 1 # username entry = UndoEntry() entry.set_text(self.config_get('username')) entry.connect('changed', changed, 'username') table.attach(entry, 1, 2, row, row + 1) labels[row].set_mnemonic_widget(entry) row += 1 # password entry = UndoEntry() entry.set_text(self.config_get('password')) entry.set_visibility(False) entry.connect('changed', changed, 'password') table.attach(entry, 1, 2, row, row + 1) labels[row].set_mnemonic_widget(entry) row += 1 # verify data button = qltk.Button(_("_Verify account data"), Gtk.STOCK_INFO) button.connect('clicked', check_login) table.attach(button, 0, 2, 4, 5) box.pack_start(qltk.Frame(_("Account"), child=table), True, True, 0) # second frame table = Gtk.Table(4, 2) table.set_col_spacings(6) table.set_row_spacings(6) label_names = [_("_Artist pattern:"), _("_Title pattern:"), _("Exclude _filter:")] labels = [] for idx, label in enumerate(map(Gtk.Label, label_names)): label.set_alignment(0.0, 0.5) label.set_use_underline(True) table.attach(label, 0, 1, idx, idx + 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) labels.append(label) row = 0 # artist pattern entry = UndoEntry() entry.set_text(self.config_get('artistpat')) entry.connect('changed', changed, 'artistpat') table.attach(entry, 1, 2, row, row + 1) entry.set_tooltip_text(_("The pattern used to format " "the artist name for submission. Leave blank for default.")) labels[row].set_mnemonic_widget(entry) row += 1 # title pattern entry = UndoEntry() entry.set_text(self.config_get('titlepat')) entry.connect('changed', changed, 'titlepat') table.attach(entry, 1, 2, row, row + 1) entry.set_tooltip_text(_("The pattern used to format " "the title for submission. Leave blank for default.")) labels[row].set_mnemonic_widget(entry) row += 1 # exclude filter entry = ValidatingEntry(parse.Query.is_valid_color) entry.set_text(self.config_get('exclude')) entry.set_tooltip_text( _("Songs matching this filter will not be submitted.")) entry.connect('changed', changed, 'exclude') table.attach(entry, 1, 2, row, row + 1) labels[row].set_mnemonic_widget(entry) row += 1 # offline mode offline = self.ConfigCheckButton( _("_Offline mode (don't submit anything)"), 'scrobbler_offline') offline.set_active(self.config_get('offline') == "true") table.attach(offline, 0, 2, row, row + 1) box.pack_start(qltk.Frame(_("Submission"), child=table), True, True, 0) return box ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/������������������������������������������������������������������0000755�0001750�0001750�00000000000�12173213476�017435� 5����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/filterbrowser.py��������������������������������������������������0000644�0001750�0001750�00000001644�12173212464�022701� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from quodlibet import app from quodlibet import browsers from quodlibet.qltk.browser import LibraryBrowser from quodlibet.plugins.songsmenu import SongsMenuPlugin class FilterBrowser(SongsMenuPlugin): PLUGIN_ID = 'filterbrowser' PLUGIN_NAME = _('Filter on Directory') PLUGIN_DESC = _("Filter on directory in a new browser window.") PLUGIN_ICON = Gtk.STOCK_INDEX PLUGIN_VERSION = '0.1' def plugin_songs(self, songs): tag = "~dirname" values = [] for song in songs: values.extend(song.list(tag)) browser = LibraryBrowser(browsers.get("SearchBar"), app.library) browser.browser.filter(tag, set(values)) ��������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/wikipedia.py������������������������������������������������������0000644�0001750�0001750�00000003454�12173212464�021757� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Inigo Serna # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from urllib import quote from gi.repository import Gtk from quodlibet import config from quodlibet.util import website from quodlibet.plugins.songsmenu import SongsMenuPlugin WIKI_URL = "http://%s.wikipedia.org/wiki/" try: config.get("plugins", __name__) except: config.set("plugins", __name__, "en") class WikiSearch(object): PLUGIN_ICON = Gtk.STOCK_OPEN PLUGIN_VERSION = '0.14' def changed(self, e): config.set("plugins", __name__, e.get_text()) changed = classmethod(changed) def PluginPreferences(self, parent): hb = Gtk.HBox(spacing=3) hb.set_border_width(6) e = Gtk.Entry(max_length=2) e.set_width_chars(3) e.set_text(config.get('plugins', __name__)) e.connect('changed', self.changed) hb.pack_start(Gtk.Label("Search at http://"), False, True, 0) hb.pack_start(e, False, True, 0) hb.pack_start(Gtk.Label(".wikipedia.org"), False, True, 0) hb.show_all() return hb PluginPreferences = classmethod(PluginPreferences) def plugin_songs(self, songs): l = dict.fromkeys([song(self.k) for song in songs]).keys() for a in l: a = quote(str(a).title().replace(' ', '_')) website(WIKI_URL % config.get('plugins', __name__) + a) class WikiArtist(WikiSearch, SongsMenuPlugin): PLUGIN_ID = 'Search artist in Wikipedia' PLUGIN_NAME = _('Search artist in Wikipedia') k = 'artist' class WikiAlbum(WikiSearch, SongsMenuPlugin): PLUGIN_ID = 'Search album in Wikipedia' PLUGIN_NAME = _('Search album in Wikipedia') k = 'album' ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/website_search.py�������������������������������������������������0000644�0001750�0001750�00000014413�12173212464�022775� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- # Copyright 2011, 2012 Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from quodlibet import config, print_w, print_d, qltk from quodlibet.const import USERDIR from quodlibet.formats._audio import AudioFile from quodlibet.parse._pattern import Pattern from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.qltk.cbes import StandaloneEditor from quodlibet.qltk.x import SeparatorMenuItem from quodlibet.util import website from quodlibet.util.tags import STANDARD_TAGS, MACHINE_TAGS from urllib2 import quote import ConfigParser from gi.repository import Gtk import os from quodlibet.util.uri import URI class WebsiteSearch(SongsMenuPlugin): """Loads a browser with a URL designed to search on tags of the song. This may include a standard web search engine, eg Google, or a more specific site look-up. The URLs are customisable using tag patterns. """ PLUGIN_ICON = Gtk.STOCK_OPEN PLUGIN_ID = "Website Search" PLUGIN_NAME = _("Website Search") PLUGIN_DESC = _("Searches your choice of website using any song tags.") PLUGIN_VERSION = '0.3' # Here are some starters... # Sorry, PEP-8 : sometimes you're unrealistic DEFAULT_URL_PATS = [ ("Google song search", "http://google.com/search?q=<artist~title>"), ("Wikipedia (en) artist entry", "http://wikipedia.org/wiki/<albumartist|<albumartist>|<artist>>"), ("Musicbrainz album listing", "http://musicbrainz.org/<musicbrainz_albumid|release/" "<musicbrainz_albumid>|search?query=<album>&type=release>"), ("Discogs album search", "http://www.discogs.com/advanced_search?artist=" "<albumartist|<albumartist>|<artist>>&release_title=<album>"), ("ISOHunt FLAC album torrent search", "https://isohunt.com/torrents/?ihq=" "<albumartist|<albumartist>|<artist>>+<album>+flac"), ("The Pirate Bay torrent search", "http://thepiratebay.org/search/" "<albumartist|<albumartist>|<artist>> <album>/0/99/100") ] PATTERNS_FILE = os.path.join(USERDIR, 'lists', 'searchsites') @classmethod def cfg_get(cls, name, default=None): try: key = __name__ + "_" + name return config.get("plugins", key) except (ValueError, ConfigParser.Error): print_w("Config entry '%s' not found. Using '%s'" % (key, default,)) return default @classmethod def cfg_set(cls, name, value): key = __name__ + "_" + name config.set("plugins", key, value) def __set_site(self, name): self.chosen_site = name def get_url_pattern(self, key): """Gets the pattern for a given key""" return dict(self._url_pats).get(key, self.DEFAULT_URL_PATS[0][1]) @classmethod def edit_patterns(cls, button): def valid_uri(s): # TODO: some pattern validation too (that isn't slow) try: p = Pattern(s) u = URI(s) return (p and u.netloc and u.scheme in ["http", "https", "ftp", "file"]) except ValueError: return False win = StandaloneEditor(filename=cls.PATTERNS_FILE, title=_("Search URL patterns"), initial=cls.DEFAULT_URL_PATS, validator=valid_uri) win.show() @classmethod def PluginPreferences(cls, parent): hb = Gtk.HBox(spacing=3) hb.set_border_width(0) button = qltk.Button(_("Edit search URLs"), Gtk.STOCK_EDIT) button.set_tooltip_markup(_("Supports QL patterns\neg " "<tt>http://google.com?q=<artist~title></tt>")) button.connect("clicked", cls.edit_patterns) hb.pack_start(button, True, True, 0) hb.show_all() return hb def _get_saved_searches(self): filename = self.PATTERNS_FILE + ".saved" #print_d("Checking saved searches in %s..." % filename, context=self) self._url_pats = StandaloneEditor.load_values(filename) # Failing all else... if not len(self._url_pats): print_d("No saved searches found in %s. Using defaults." % filename, context=self) self._url_pats = self.DEFAULT_URL_PATS def __init__(self, *args, **kwargs): super(WebsiteSearch, self).__init__(*args, **kwargs) self.chosen_site = None self._url_pats = [] submenu = Gtk.Menu() self._get_saved_searches() for name, url_pat in self._url_pats: item = Gtk.MenuItem(name) item.connect_object('activate', self.__set_site, name) submenu.append(item) # Add link to editor config = Gtk.MenuItem(_("Configure searches...")) config.connect_object('activate', self.edit_patterns, config) submenu.append(SeparatorMenuItem()) submenu.append(config) if submenu.get_children(): self.set_submenu(submenu) else: self.set_sensitive(False) def plugin_songs(self, songs): # Check this is a launch, not a configure if self.chosen_site: url_pat = self.get_url_pattern(self.chosen_site) pat = Pattern(url_pat) urls = set() for song in songs: # Generate a sanitised AudioFile; allow through most tags subs = AudioFile() for k, v in song.items(): if k in (STANDARD_TAGS + MACHINE_TAGS): try: subs[k] = quote(unicode(v).encode('utf-8')) # Dodgy unicode problems except KeyError: print_d("Problem with %s tag value: %r" % (k, v)) url = str(pat.format(subs)) if not url: print_w("Couldn't build URL using \"%s\"." "Check your pattern?" % url_pat) return # Grr, set.add() should return boolean... if url not in urls: urls.add(url) website(url) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/reset.py����������������������������������������������������������0000644�0001750�0001750�00000005006�12173212464�021126� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from quodlibet import util, const, config from quodlibet.plugins.songsmenu import SongsMenuPlugin class ResetLibrary(SongsMenuPlugin): PLUGIN_ID = "Reset Library Data" PLUGIN_NAME = _("Reset Library Data") PLUGIN_VERSION = "1" PLUGIN_DESC = "Reset ratings, play counts, skip counts, and play times." PLUGIN_ICON = 'gtk-refresh' def plugin_song(self, song): for key in ["~#playcount", "~#skipcount", "~#lastplayed", "~#laststarted", "~#rating"]: if key in song: del song[key] class ResetRating(SongsMenuPlugin): PLUGIN_ID = "Reset Rating" PLUGIN_NAME = _("Reset Rating") PLUGIN_VERSION = "1" PLUGIN_DESC = _("Reset to the default rating " "and change the global default rating.") PLUGIN_ICON = 'gtk-clear' def plugin_song(self, song): if "~#rating" in song: del song["~#rating"] @classmethod def PluginPreferences(klass, window): vb2 = Gtk.VBox(spacing=3) hb = Gtk.HBox(spacing=3) lab = Gtk.Label(label=_("Default r_ating:")) lab.set_use_underline(True) hb.pack_start(lab, False, True, 0) def draw_rating(column, cell, model, it, data): i = model[it][0] text = "%0.2f\t%s" % (i, util.format_rating(i)) cell.set_property('text', text) def default_rating_changed(combo, model): it = combo.get_active_iter() if it is None: return default_rating = model[it][0] config.set("settings", "default_rating", default_rating) const.DEFAULT_RATING = default_rating model = Gtk.ListStore(float) combo = Gtk.ComboBox(model=model) cell = Gtk.CellRendererText() combo.pack_start(cell, True) for i in range(0, int(1.0 / util.RATING_PRECISION) + 1): i *= util.RATING_PRECISION it = model.append(row=[i]) if i == const.DEFAULT_RATING: combo.set_active_iter(it) combo.set_cell_data_func(cell, draw_rating, None) combo.connect('changed', default_rating_changed, model) hb.pack_start(combo, False, True, 0) lab.set_mnemonic_widget(combo) vb2.pack_start(hb, True, True, 0) return vb2 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/ape2id3.py��������������������������������������������������������0000644�0001750�0001750�00000002113�12161032160�021215� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import mutagen.apev2 from quodlibet.formats._apev2 import APEv2File from quodlibet.plugins.songsmenu import SongsMenuPlugin class APEv2toID3v2(SongsMenuPlugin): PLUGIN_ID = "APEv2 to ID3v2" PLUGIN_NAME = _("APEv2 to ID3v2") PLUGIN_DESC = ("Convert your APEv2 tags to ID3v2 tags. This will delete " "the APEv2 tags after conversion.") PLUGIN_ICON = 'gtk-convert' PLUGIN_VERSION = '0.2' def plugin_handles(self, songs): for song in songs: if not song.get("~filename", "").lower().endswith(".mp3"): return False return True def plugin_song(self, song): try: apesong = APEv2File(song["~filename"]) except: return # File doesn't have an APEv2 tag song.update(apesong) mutagen.apev2.delete(song["~filename"]) song._song.write() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/bookmarks.py������������������������������������������������������0000644�0001750�0001750�00000004736�12173212464�022005� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2006 Joe Wreschnig, 2010 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from quodlibet import app from quodlibet import qltk from quodlibet.qltk.bookmarks import EditBookmarks from quodlibet.qltk.x import SeparatorMenuItem from quodlibet.plugins.songsmenu import SongsMenuPlugin class Bookmarks(SongsMenuPlugin): PLUGIN_ID = "Go to Bookmark..." PLUGIN_NAME = _("Go to Bookmark...") PLUGIN_DESC = "List all bookmarks in the selected files." PLUGIN_ICON = Gtk.STOCK_JUMP_TO PLUGIN_VERSION = "0.4" def __init__(self, songs, *args, **kwargs): super(Bookmarks, self).__init__(songs, *args, **kwargs) self.__menu = Gtk.Menu() self.__menu.connect('map', self.__map, songs) self.__menu.connect('unmap', self.__unmap) self.set_submenu(self.__menu) class FakePlayer(object): def __init__(self, song): self.song = song def seek(self, time): if app.player.go_to(self.song._song, explicit=True): app.player.seek(time) get_position = lambda *x: 0 def __map(self, menu, songs): for song in songs: marks = song.bookmarks if marks: fake_player = self.FakePlayer(song) song_item = Gtk.MenuItem(song.comma("title")) song_menu = Gtk.Menu() song_item.set_submenu(song_menu) menu.append(song_item) items = qltk.bookmarks.MenuItems(marks, fake_player, True) map(song_menu.append, items) song_menu.append(SeparatorMenuItem()) i = qltk.MenuItem(_("_Edit Bookmarks..."), Gtk.STOCK_EDIT) def edit_bookmarks_cb(menu_item): window = EditBookmarks(self.plugin_window, app.library, fake_player) window.show() i.connect('activate', edit_bookmarks_cb) song_menu.append(i) if menu.get_active() is None: no_marks = Gtk.MenuItem(_("No Bookmarks")) no_marks.set_sensitive(False) menu.append(no_marks) menu.show_all() def __unmap(self, menu): map(self.__menu.remove, self.__menu.get_children()) def plugin_songs(self, songs): pass ����������������������������������quodlibet-plugins-3.0.2/songsmenu/duplicates.py�����������������������������������������������������0000644�0001750�0001750�00000041305�12173212464�022143� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# # Duplicates songs plugin. # # Copyright (C) 2012, 2011 Nick Boultbee # # Finds "duplicates" of songs selected by searching the library for # others with the same user-configurable "key", presenting a browser-like # dialog for further interaction with these. # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # import string import unicodedata from gi.repository import Gtk, Pango from quodlibet import app from quodlibet import print_d, print_w, util, qltk from quodlibet.plugins import PluginConfigMixin from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.qltk.ccb import ConfigCheckButton from quodlibet.qltk.edittags import AudioFileGroup from quodlibet.qltk.entry import UndoEntry from quodlibet.qltk.songsmenu import SongsMenu from quodlibet.qltk.views import RCMHintedTreeView class DuplicateSongsView(RCMHintedTreeView): """Allows full tree-like functionality on top of underlying features""" def get_selected_songs(self): selection = self.get_selection() if selection is None: return [] model, rows = selection.get_selected_rows() if not rows: return [] selected = [] for row in rows: row = model[row] if row.parent is None: for child in row.iterchildren(): selected.append(child[0]) else: selected.append(row[0]) return selected def Menu(self, library): songs = self.get_selected_songs() if not songs: return menu = SongsMenu( library, songs, delete=True, parent=self, plugins=False, devices=False, playlists=False) menu.show_all() return menu def __select_song(self, player, path, col): if len(path) == 1: if self.row_expanded(path): self.collapse_row(path) else: self.expand_row(path, False) else: songs = self.get_selected_songs() if songs and player.go_to(songs[0], True): player.paused = False else: print_w("Sorry, can't play song outside current list.") def _removed(self, library, songs): model = self.get_model() if not model: return for song in songs: row = model.find_row(song) if row: group_row = model.iter_parent(row.iter) print_d("Found parent group = %s" % group_row) model.remove(row.iter) num_kids = model.iter_n_children(group_row) if num_kids < Duplicates.MIN_GROUP_SIZE: print_d("Removing group %s" % group_row) model.remove(group_row) else: # print_w("Couldn't delete song %s" % song) pass def _added(self, library, songs): model = self.get_model() if not model: return for song in songs: key = Duplicates.get_key(song) model.add_to_existing_group(key, song) # TODO: handle creation of new groups based on songs that were # in original list but not as a duplicate def _changed(self, library, songs): model = self.get_model() if not model: # Keeps happening on next song - bug / race condition? return for song in songs: key = Duplicates.get_key(song) row = model.find_row(song) if row: print_d("Changed duplicated file \"%s\" (Row=%s)" % (song("~artist~title"), row)) parent = model.iter_parent(row.iter) old_key = model[parent][0] if old_key != key: print_d("Key changed from \"%s\" -> \"%s\"" % (old_key, key)) self._removed(library, [song]) self._added(library, [song]) else: # Still might be a displayable change print_d("Calling model.row_changed(%s, %s)..." % (row.path, row.iter)) model.row_changed(row.path, row.iter) else: model.add_to_existing_group(key, song) def __init__(self, model): super(DuplicateSongsView, self).__init__(model) self.connect_object('row-activated', self.__select_song, app.player) # Selecting multiple is a nice feature it turns out. self.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) # Handle signals propagated from the underlying library self.connected_library_sigs = [] SIGNAL_MAP = { 'removed': self._removed, 'added': self._added, 'changed': self._changed } for (sig, callback) in SIGNAL_MAP.items(): print_d("Listening to library.%s signals" % sig) self.connected_library_sigs.append( app.library.connect(sig, callback)) # And disconnect, or Bad Stuff happens. self.connect('destroy', self.on_destroy) def on_destroy(self, view): print_d("Disconnecting from library signals...") for sig in self.connected_library_sigs: app.library.disconnect(sig) class DuplicatesTreeModel(Gtk.TreeStore): """A tree store to model duplicated song information""" # Define columns to display (and how, in lieu of using qltk.browsers) def i(x): return x TAG_MAP = [ ("artist", i), ("title", i), ("album", i), ("~#length", lambda s: util.format_time(int(s))), ("~#filesize", lambda s: util.format_size(int(s))), ("~#bitrate", i), ("~filename", i)] # Now make a dict. This seems clunky. tag_functions = {} for t, f in TAG_MAP: tag_functions[t] = f @classmethod def group_value(cls, group, tag): """Gets a formatted aggregated value/dummy for a set of tag values""" try: group_val = group[tag].safenicestr() except KeyError: return "" else: try: group_val = cls.tag_functions[tag](group_val) except (ValueError, TypeError): pass return group_val.replace("\n", ", ") def find_row(self, song): """Returns the row in the model from song, or None""" for parent in self: for row in parent.iterchildren(): if row[0] == song: self.__iter = row.iter self.sourced = True return row return None def add_to_existing_group(self, key, song): """Tries to add a song to an existing group. Returns None if not able """ #print_d("Trying to add %s to group \"%s\"" % (song("~filename"), key)) for parent in self: if key == parent[0]: print_d("Found group", self) return self.append(parent.iter, self.__make_row(song)) # TODO: update group return None @classmethod def __make_row(cls, song): """Construct GTK row for a song, with all columns""" return [song] + [util.escape(str(f(song.comma(tag)))) for (tag, f) in cls.TAG_MAP] def add_group(self, key, songs): """Adds a new group, returning the row created""" group = AudioFileGroup(songs) # Add the group first. parent = self.append(None, [key] + [self.group_value(group, tag) for tag, f in self.TAG_MAP]) for s in songs: self.append(parent, self.__make_row(s)) def go_to(self, song, explicit=False): #print_d("Duplicates: told to go to %r" % song, context=self) self.__iter = None if isinstance(song, Gtk.TreeIter): self.__iter = song self.sourced = True elif not self.find_row(song): print_d("Failed to find song", context=self) return self.__iter def remove(self, itr): if self.__iter and self[itr].path == self[self.__iter].path: self.__iter = None super(DuplicatesTreeModel, self).remove(itr) def get(self): return [row[0] for row in self] @property def get_current(self): if self.__iter is None: return None elif self.is_empty(): return None else: return self[self.__iter][0] @property def get_current_path(self): if self.__iter is None: return None elif self.is_empty(): return None else: return self[self.__iter].path @property def get_current_iter(self): if self.__iter is None: return None elif self.is_empty(): return None else: return self.__iter def is_empty(self): return not len(self) def __init__(self): super(DuplicatesTreeModel, self).__init__( object, str, str, str, str, str, str, str) class DuplicateDialog(Gtk.Window): """Main dialog for browsing duplicate results""" def __quit(self, widget=None, response=None): if response == Gtk.ResponseType.OK or \ response == Gtk.ResponseType.CLOSE: print_d("Exiting plugin on user request...", self) self.finished = True self.destroy() return def __songs_popup_menu(self, songlist): path, col = songlist.get_cursor() menu = songlist.Menu(app.library) if menu is not None: return songlist.popup_menu(menu, 0, Gtk.get_current_event_time()) def __init__(self, model): songs_text = ngettext("%d duplicate group", "%d duplicate groups", len(model)) % len(model) super(DuplicateDialog, self).__init__() self.set_destroy_with_parent(True) self.set_title("Quod Libet - %s (%s)" % (Duplicates.PLUGIN_NAME, songs_text)) self.finished = False self.set_default_size(960, 480) self.set_border_width(6) swin = Gtk.ScrolledWindow() swin.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) swin.set_shadow_type(Gtk.ShadowType.IN) # Set up the browser view view = DuplicateSongsView(model) def cell_text(column, cell, model, iter_, index): text = model[iter_][index] cell.markup = text cell.set_property("markup", text) # Set up the columns for i, (tag, f) in enumerate(DuplicatesTreeModel.TAG_MAP): e = (Pango.EllipsizeMode.START if tag == '~filename' else Pango.EllipsizeMode.END) render = Gtk.CellRendererText() render.set_property("ellipsize", e) col = Gtk.TreeViewColumn(util.tag(tag), render) # Numeric columns are better smaller here. if tag.startswith("~#"): col.set_fixed_width(80) col.set_sizing(Gtk.TreeViewColumnSizing.FIXED) else: col.set_expand(True) col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) col.set_resizable(True) col.set_cell_data_func(render, cell_text, i + 1) view.append_column(col) view.connect('popup-menu', self.__songs_popup_menu) swin.add(view) # A basic information area hbox = Gtk.HBox(spacing=6) def expand_all(*args): model = view.get_model() for row in model: if view.row_expanded(row.path): for row in model: view.collapse_row(row.path) break else: for row in model: view.expand_row(row.path, False) expand = Gtk.Button(_("Collapse / Expand all")) expand.connect_object("clicked", expand_all, view) hbox.pack_start(expand, False, True, 0) label = Gtk.Label(label=_("Duplicate key expression is '%s'") % Duplicates.get_key_expression()) hbox.pack_start(label, True, True, 0) close = Gtk.Button(stock=Gtk.STOCK_CLOSE) close.connect('clicked', self.__quit) hbox.pack_start(close, False, True, 0) vbox = Gtk.VBox(spacing=6) vbox.pack_start(swin, True, True, 0) vbox.pack_start(hbox, False, True, 0) self.add(vbox) self.show_all() class Duplicates(SongsMenuPlugin, PluginConfigMixin): PLUGIN_ID = 'Duplicates' PLUGIN_NAME = _('Duplicates Browser') PLUGIN_DESC = _('Find and browse similarly tagged versions of songs.') PLUGIN_ICON = Gtk.STOCK_MEDIA_PLAY PLUGIN_VERSION = "0.7" MIN_GROUP_SIZE = 2 _CFG_KEY_KEY = "key_expression" __DEFAULT_KEY_VALUE = "~artist~title~version" _CFG_REMOVE_WHITESPACE = 'remove_whitespace' _CFG_REMOVE_DIACRITICS = 'remove_diacritics' _CFG_REMOVE_PUNCTUATION = 'remove_punctuation' _CFG_CASE_INSENSITIVE = 'case_insensitive' # Cached values key_expression = None __cfg_cache = {} # Faster than a speeding bullet __trans = string.maketrans("", "") @classmethod def get_key_expression(cls): if not cls.key_expression: cls.key_expression = ( cls.config_get(cls._CFG_KEY_KEY, cls.__DEFAULT_KEY_VALUE)) return cls.key_expression @classmethod def PluginPreferences(cls, window): def key_changed(entry): cls.key_expression = None cls.config_set(cls._CFG_KEY_KEY, entry.get_text().strip()) vb = Gtk.VBox(spacing=10) vb.set_border_width(0) hbox = Gtk.HBox(spacing=6) # TODO: construct a decent validator and use ValidatingEntry e = UndoEntry() e.set_text(cls.get_key_expression()) e.connect("changed", key_changed) e.set_tooltip_markup("Accepts QL tag expressions like " "<tt>~artist~title</tt> or <tt>musicbrainz_track_id</tt>") lbl = Gtk.Label(label=_("_Group duplicates by:")) lbl.set_mnemonic_widget(e) lbl.set_use_underline(True) hbox.pack_start(lbl, False, True, 0) hbox.pack_start(e, False, True, 0) frame = qltk.Frame(label=_("Duplicate Key"), child=hbox) vb.pack_start(frame, True, True, 0) # Matching Option toggles = [ (cls._CFG_REMOVE_WHITESPACE, _("Remove _Whitespace")), (cls._CFG_REMOVE_DIACRITICS, _("Remove _Diacritics")), (cls._CFG_REMOVE_PUNCTUATION, _("Remove _Punctuation")), (cls._CFG_CASE_INSENSITIVE, _("Case _Insensitive")), ] vb2 = Gtk.VBox(spacing=6) for key, label in toggles: ccb = ConfigCheckButton(label, 'plugins', cls._config_key(key)) ccb.set_active(cls.config_get_bool(key)) vb2.pack_start(ccb, True, True, 0) frame = qltk.Frame(label=_("Matching options"), child=vb2) vb.pack_start(frame, False, True, 0) vb.show_all() return vb @staticmethod def remove_accents(s): return filter(lambda c: not unicodedata.combining(c), unicodedata.normalize('NFKD', unicode(s))) @classmethod def get_key(cls, song): key = song(cls.get_key_expression()) if cls.config_get_bool(cls._CFG_REMOVE_DIACRITICS): key = cls.remove_accents(key) if cls.config_get_bool(cls._CFG_CASE_INSENSITIVE): key = key.lower() if cls.config_get_bool(cls._CFG_REMOVE_PUNCTUATION): key = str(key).translate(cls.__trans, string.punctuation) if cls.config_get_bool(cls._CFG_REMOVE_WHITESPACE): key = "_".join(key.split()) return key def plugin_songs(self, songs): model = DuplicatesTreeModel() self.__cfg_cache = {} # Index all songs by our custom key # TODO: make this cache-friendly print_d("Calculating duplicates...", self) groups = {} for song in songs: key = self.get_key(song) if key and key in groups: groups[key].add(song._song) elif key: groups[key] = set([song._song]) for song in app.library: key = self.get_key(song) if key in groups: groups[key].add(song) # Now display the grouped duplicates for (key, children) in groups.items(): if len(children) < self.MIN_GROUP_SIZE: continue # The parent (group) label model.add_group(key, children) dialog = DuplicateDialog(model) dialog.show() ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/filterall.py������������������������������������������������������0000644�0001750�0001750�00000006133�12173212464�021764� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.util.tags import MACHINE_TAGS from quodlibet.util import build_filter_query from quodlibet.qltk import Window class SelectionWindow(Window): def __init__(self, filters, browser, parent=None): super(SelectionWindow, self).__init__() self.set_border_width(10) self.set_title(FilterAll.PLUGIN_NAME) self.set_default_size(200, 250) self.set_transient_for(parent) model = Gtk.ListStore(bool, str, str) for key, value in sorted(filters.items()): model.append(row=[False, key, value]) toggle = Gtk.CellRendererToggle() toggle.connect("toggled", self.__toggeled, model, browser) text = Gtk.CellRendererText() toggle_column = Gtk.TreeViewColumn("", toggle, active=0) column = Gtk.TreeViewColumn("Tag", text, text=1) view = Gtk.TreeView(model) view.append_column(toggle_column) view.append_column(column) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.set_shadow_type(Gtk.ShadowType.IN) sw.add(view) buttons = Gtk.HButtonBox() buttons.set_spacing(6) buttons.set_layout(Gtk.ButtonBoxStyle.END) close = Gtk.Button(stock=Gtk.STOCK_CLOSE) close.connect('clicked', lambda *x: self.destroy()) buttons.pack_start(close, True, True, 0) box = Gtk.VBox(spacing=12) box.pack_start(sw, True, True, 0) box.pack_start(buttons, False, True, 0) self.add(box) self.show_all() def __filter(self, model, browser): selected = {} for row in model: sel, key, value = row if sel: selected[key] = value joined = ", ".join(sorted(selected.values())) if len(selected) >= 2: joined = "&(%s)" % joined browser.filter_text(joined) def __toggeled(self, render, path, model, browser): model[path][0] = not model[path][0] self.__filter(model, browser) class FilterAll(SongsMenuPlugin): PLUGIN_ID = "FilterAll" PLUGIN_NAME = _("Filter on any tag") PLUGIN_DESC = _("Create a search query based on " "tags of the selected songs") PLUGIN_ICON = 'gtk-index' def plugin_songs(self, songs): browser = self.plugin_window.browser if not browser.can_filter_text(): return keys = set() for song in songs: keys.update(song.realkeys()) keys.difference_update(MACHINE_TAGS) filters = {} for key in keys: values = set() for song in songs: values.update(song.list(key)) filters[key] = build_filter_query(key, values) SelectionWindow(filters, browser, parent=self.plugin_window) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/forcewrite.py�����������������������������������������������������0000644�0001750�0001750�00000001163�12161032160�022143� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from quodlibet.plugins.songsmenu import SongsMenuPlugin class ForceWrite(SongsMenuPlugin): PLUGIN_ID = "Force Write" PLUGIN_NAME = _("Force Write") PLUGIN_DESC = _("Save the files again. This will make sure play counts " "and ratings are up-to-date.") PLUGIN_ICON = 'gtk-save' PLUGIN_VERSION = "0.14" def plugin_song(self, song): song._needs_write = True �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/splitting.py������������������������������������������������������0000644�0001750�0001750�00000003517�12173213426�022025� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from quodlibet import util from quodlibet.plugins.songsmenu import SongsMenuPlugin class SplitTags(SongsMenuPlugin): PLUGIN_ID = "Split Tags" PLUGIN_NAME = _("Split Tags") PLUGIN_HINT = "Split out version and disc number" PLUGIN_DESC = ("Split the disc number from the album and the version " "from the title at the same time.") PLUGIN_ICON = 'gtk-find-and-replace' PLUGIN_VERSION = "0.13" def plugin_song(self, song): if ("title" in song and song.can_change("title") and song.can_change("version")): title, versions = util.split_title(song["title"]) if title: song["title"] = title if versions: song["version"] = "\n".join(versions) if ("album" in song and "discnumber" not in song and song.can_change("album") and song.can_change("discnumber")): album, disc = util.split_album(song["album"]) if album: song["album"] = album if disc: song["discnumber"] = disc class SplitAlbum(SongsMenuPlugin): PLUGIN_ID = "Split Album" PLUGIN_NAME = _("Split Album") PLUGIN_HINT = "Split out disc number" PLUGIN_ICON = 'gtk-find-and-replace' PLUGIN_VERSION = "0.13" def plugin_song(self, song): if ("album" in song and "discnumber" not in song and song.can_change("album") and song.can_change("discnumber")): album, disc = util.split_album(song["album"]) if album: song["album"] = album if disc: song["discnumber"] = disc ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/replaygain.py�����������������������������������������������������0000644�0001750�0001750�00000035652�12173212464�022151� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#! /usr/bin/env python # # ReplayGain Album Analysis using gstreamer rganalysis element # Copyright (C) 2005,2007,2009 Michael Urman # 2012 Nick Boultbee # 2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. # from gi.repository import Gtk from gi.repository import GObject from gi.repository import Pango from gi.repository import Gst from gi.repository import GLib from quodlibet.qltk.views import HintedTreeView from quodlibet.plugins.songsmenu import SongsMenuPlugin __all__ = ['ReplayGain'] def get_num_threads(): # multiprocessing is >= 2.6. # Default to 2 threads if cpu_count isn't implemented for the current arch # or multiprocessing isn't available try: import multiprocessing threads = multiprocessing.cpu_count() except (ImportError, NotImplementedError): threads = 2 return threads class RGAlbum(object): def __init__(self, rg_songs): self.songs = rg_songs self.gain = None self.peak = None @property def progress(self): all_ = 0.0 done = 0.0 for song in self.songs: all_ += song.length done += song.length * song.progress try: return max(min(done / all_, 1.0), 0.0) except ZeroDivisionError: return 0.0 @property def done(self): for song in self.songs: if not song.done: return False return True @property def title(self): if not self.songs: return "" return self.songs[0].song.comma('~artist~album') @property def error(self): for song in self.songs: if song.error: return True return False def write(self): # Don't write incomplete data if not self.done: return for song in self.songs: song._write(self.gain, self.peak) @classmethod def from_songs(self, songs): return RGAlbum([RGSong(s) for s in songs]) class RGSong(object): def __init__(self, song): self.song = song self.error = False self.gain = None self.peak = None self.progress = 0.0 self.done = False def _write(self, album_gain, album_peak): if self.error or not self.done: return song = self.song if self.gain is not None: song['replaygain_track_gain'] = '%.2f dB' % self.gain if self.peak is not None: song['replaygain_track_peak'] = '%.4f' % self.peak if album_gain is not None: song['replaygain_album_gain'] = '%.2f dB' % album_gain if album_peak is not None: song['replaygain_album_peak'] = '%.4f' % album_peak @property def title(self): return self.song('~tracknumber~title~version') @property def filename(self): return self.song("~filename") @property def length(self): return self.song("~#length") class ReplayGainPipeline(GObject.Object): __gsignals__ = { # done(self, album) 'done': (GObject.SignalFlags.RUN_LAST, None, (object,)), # update(self, album, song) 'update': (GObject.SignalFlags.RUN_LAST, None, (object, object,)), } def __init__(self): super(ReplayGainPipeline, self).__init__() self._current = None self._setup_pipe() def _setup_pipe(self): # gst pipeline for replay gain analysis: # filesrc!decodebin!audioconvert!audioresample!rganalysis!fakesink self.pipe = Gst.Pipeline() self.filesrc = Gst.ElementFactory.make("filesrc", "source") self.pipe.add(self.filesrc) self.decode = Gst.ElementFactory.make("decodebin", "decode") def new_decoded_pad(dbin, pad): pad.link(self.convert.get_static_pad("sink")) def removed_decoded_pad(dbin, pad): pad.unlink(self.convert.get_static_pad("sink")) def sort_decoders(decode, pad, caps, factories): def set_prio(x): i, f = x i = {"mad": -1, "mpg123audiodec": -2}.get(f.get_name(), i) return (i, f) return zip(*sorted(map(set_prio, enumerate(factories))))[1] self.decode.connect("autoplug-sort", sort_decoders) self.decode.connect("pad-added", new_decoded_pad) self.decode.connect("pad-removed", removed_decoded_pad) self.pipe.add(self.decode) self.filesrc.link(self.decode) self.convert = Gst.ElementFactory.make("audioconvert", "convert") self.pipe.add(self.convert) self.resample = Gst.ElementFactory.make("audioresample", "resample") self.pipe.add(self.resample) self.convert.link(self.resample) self.analysis = Gst.ElementFactory.make("rganalysis", "analysis") self.pipe.add(self.analysis) self.resample.link(self.analysis) self.sink = Gst.ElementFactory.make("fakesink", "sink") self.pipe.add(self.sink) self.analysis.link(self.sink) self.bus = bus = self.pipe.get_bus() bus.add_signal_watch() bus.connect("message", self._bus_message) def request_update(self): if not self._current: return ok, p = self.pipe.query_position(Gst.Format.TIME) if ok: length = self._current.length try: progress = float(p / Gst.SECOND) / length except ZeroDivisionError: progress = 0.0 progress = max(min(progress, 1.0), 0.0) self._current.progress = progress self._emit_update() def _emit_update(self): self.emit("update", self._album, self._current) def start(self, album): self._album = album self._songs = list(album.songs) self._done = [] self._next_song(first=True) def quit(self): self.bus.remove_signal_watch() self.pipe.set_state(Gst.State.NULL) def _next_song(self, first=False): if self._current: self._current.progress = 1.0 self._current.done = True self._emit_update() self._done.append(self._current) self._current = None if not self._songs: self.pipe.set_state(Gst.State.NULL) self.emit("done", self._album) return if first: self.analysis.set_property("num-tracks", len(self._songs)) else: self.analysis.set_locked_state(True) self.pipe.set_state(Gst.State.NULL) self._current = self._songs.pop(0) self.filesrc.set_property("location", self._current.filename) if not first: # flush, so the element takes new data after EOS pad = self.analysis.get_static_pad("src") pad.send_event(Gst.Event.new_flush_start()) pad.send_event(Gst.Event.new_flush_stop(True)) self.analysis.set_locked_state(False) self.pipe.set_state(Gst.State.PLAYING) def _bus_message(self, bus, message): if message.type == Gst.MessageType.TAG: tags = message.parse_tag() ok, value = tags.get_double(Gst.TAG_TRACK_GAIN) if ok: self._current.gain = value ok, value = tags.get_double(Gst.TAG_TRACK_PEAK) if ok: self._current.peak = value ok, value = tags.get_double(Gst.TAG_ALBUM_GAIN) if ok: self._album.gain = value ok, value = tags.get_double(Gst.TAG_ALBUM_PEAK) if ok: self._album.peak = value self._emit_update() elif message.type == Gst.MessageType.EOS: self._next_song() elif message.type == Gst.MessageType.ERROR: gerror, debug = message.parse_error() if gerror: print_e(gerror.message) print_e(debug) self._current.error = True self._next_song() class RGDialog(Gtk.Dialog): def __init__(self, albums, parent): super(RGDialog, self).__init__( title=_('ReplayGain Analyzer'), parent=parent, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK) ) self.set_default_size(500, 350) self.set_border_width(6) swin = Gtk.ScrolledWindow() swin.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) swin.set_shadow_type(Gtk.ShadowType.IN) self.vbox.pack_start(swin, True, True, 0) view = HintedTreeView() swin.add(view) def icon_cdf(column, cell, model, iter_, *args): item = model[iter_][0] if item.error: cell.set_property('stock-id', Gtk.STOCK_DIALOG_ERROR) else: cell.set_property('stock-id', None) column = Gtk.TreeViewColumn() column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) icon_render = Gtk.CellRendererPixbuf() column.pack_start(icon_render, True) column.set_cell_data_func(icon_render, icon_cdf) view.append_column(column) def track_cdf(column, cell, model, iter_, *args): item = model[iter_][0] cell.set_property('text', item.title) column = Gtk.TreeViewColumn(_("Track")) column.set_expand(True) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) track_render = Gtk.CellRendererText() track_render.set_property('ellipsize', Pango.EllipsizeMode.END) column.pack_start(track_render, True) column.set_cell_data_func(track_render, track_cdf) view.append_column(column) def progress_cdf(column, cell, model, iter_, *args): item = model[iter_][0] cell.set_property('value', int(item.progress * 100)) column = Gtk.TreeViewColumn(_("Progress")) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) progress_render = Gtk.CellRendererProgress() column.pack_start(progress_render, True) column.set_cell_data_func(progress_render, progress_cdf) view.append_column(column) def gain_cdf(column, cell, model, iter_, *args): item = model[iter_][0] if item.gain is None or not item.done: cell.set_property('text', "-") else: cell.set_property('text', "%.2f db" % item.gain) column = Gtk.TreeViewColumn(_("Gain")) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) gain_renderer = Gtk.CellRendererText() column.pack_start(gain_renderer, True) column.set_cell_data_func(gain_renderer, gain_cdf) view.append_column(column) def peak_cdf(column, cell, model, iter_, *args): item = model[iter_][0] if item.gain is None or not item.done: cell.set_property('text', "-") else: cell.set_property('text', "%.2f" % item.peak) column = Gtk.TreeViewColumn(_("Peak")) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) peak_renderer = Gtk.CellRendererText() column.pack_start(peak_renderer, True) column.set_cell_data_func(peak_renderer, peak_cdf) view.append_column(column) # create as many pipelines as threads self.pipes = [] for i in xrange(get_num_threads()): self.pipes.append(ReplayGainPipeline()) self._timeout = None self._sigs = {} self._done = [] self._todo = list([RGAlbum.from_songs(a) for a in albums]) self._count = len(self._todo) # fill the view self.model = model = Gtk.TreeStore(object) insert = model.insert for album in reversed(self._todo): base = insert(None, 0, row=[album]) for song in reversed(album.songs): insert(base, 0, row=[song]) view.set_model(model) if len(self._todo) == 1: view.expand_all() self.connect("destroy", self.__destroy) self.connect('response', self.__response) def start_analysis(self): self._timeout = GLib.idle_add(self.__request_update) # fill the pipelines for p in self.pipes: if not self._todo: break self._sigs[p] = [ p.connect("done", self.__done), p.connect("update", self.__update), ] p.start(self._todo.pop(0)) def __response(self, win, response): if response == Gtk.ResponseType.CANCEL: self.destroy() elif response == Gtk.ResponseType.OK: for album in self._done: album.write() self.destroy() def __destroy(self, *args): # shut down any active processing and clean up resources, timeouts if self._timeout: GLib.source_remove(self._timeout) for p in self.pipes: if p in self._sigs: for s in self._sigs.get(p, []): p.disconnect(s) p.quit() def __update(self, pipeline, album, song): for row in self.model: row_album = row[0] if row_album is album: self.model.row_changed(row.path, row.iter) for child in row.iterchildren(): row_song = child[0] if row_song is song: self.model.row_changed(child.path, child.iter) break break def __done(self, pipeline, album): self._done.append(album) if self._todo: pipeline.start(self._todo.pop(0)) for row in self.model: row_album = row[0] if row_album is album: self.model.row_changed(row.path, row.iter) break def __request_update(self): GLib.source_remove(self._timeout) # all done, stop if len(self._done) < self._count: for p in self.pipes: p.request_update() self._timeout = GLib.timeout_add(400, self.__request_update) return False class ReplayGain(SongsMenuPlugin): PLUGIN_ID = 'ReplayGain' PLUGIN_NAME = 'Replay Gain' PLUGIN_DESC = _('Analyzes ReplayGain with gstreamer, grouped by album') PLUGIN_ICON = Gtk.STOCK_MEDIA_PLAY def plugin_albums(self, albums): win = RGDialog(albums, parent=self.plugin_window) win.show_all() win.start_analysis() # plugin_done checks for metadata changes and opens the write dialog win.connect("destroy", self.__plugin_done) def __plugin_done(self, win): self.plugin_finish() if not Gst.Registry.get().find_plugin("replaygain"): __all__ = [] del ReplayGain raise ImportError("GStreamer replaygain plugin not found") ��������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/brainz.py���������������������������������������������������������0000644�0001750�0001750�00000045710�12173213426�021276� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# brainz.py - Quod Libet plugin to tag files from MusicBrainz automatically # Copyright 2005-2010 Joshua Kwan <joshk@triplehelix.org>, # Michael Ball <michael.ball@gmail.com>, # Steven Robertson <steven@strobe.cc> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os import re import threading import time from gi.repository import Gtk, GObject, Pango, GLib try: from musicbrainz2 import webservice as ws from musicbrainz2.utils import extractUuid except ImportError: from quodlibet import plugins if not hasattr(plugins, "PluginImportException"): raise raise plugins.PluginImportException( "Couldn't find python-musicbrainz2.") from quodlibet import config, util from quodlibet.qltk.ccb import ConfigCheckButton from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.qltk.views import HintedTreeView, MultiDragTreeView VARIOUS_ARTISTS_ARTISTID = '89ad4ac3-39f7-470e-963a-56509c546377' def get_artist(album): """Returns a single artist likely to be the MB AlbumArtist, or None.""" for tag in ["albumartist", "artist", "performer"]: names = set() for song in album: map(names.add, filter(lambda n: n, song.get(tag, "").split("\n"))) if len(names) == 1: return names.pop() elif len(names) > 1: return None return None def get_trackcount(album): """Returns the track count, hammered into submission.""" return max(max(map(lambda t: max(map(int, t.get('tracknumber', '0').split('/'))), album)), len(album)) # (;)) def config_get(key, default=''): return config.getboolean('plugins', 'brainz_' + key, default) def dialog_get_widget_for_stockid(dialog, stockid): for child in dialog.get_action_area().get_children(): if child.get_label() == stockid: return child class ResultTreeView(HintedTreeView, MultiDragTreeView): """The result treeview. The model only stores local tracks; info about remote results is pulled from self.remote_album.""" def __name_datafunc(self, col, cell, model, itr, data): song = model[itr][0] if song: cell.set_property('text', os.path.basename(song.get("~filename"))) else: cell.set_property('text', '') def __track_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self.remote_album): cell.set_property('text', '') else: cell.set_property('text', str(idx + 1)) def __title_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self.remote_album): cell.set_property('text', '') else: cell.set_property('text', self.remote_album[idx].title) def __artist_datafunc(self, col, cell, model, itr, data): idx = model.get_path(itr)[0] if idx >= len(self.remote_album) or not self.remote_album[idx].artist: cell.set_property('text', '') else: cell.set_property('text', self.remote_album[idx].artist.name) def __init__(self, album): self.album = album self.remote_album = [] self.model = Gtk.ListStore(object) map(self.model.append, zip(album)) super(ResultTreeView, self).__init__(self.model) self.set_headers_clickable(True) self.set_rules_hint(True) self.set_reorderable(True) self.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) cols = [ ('Filename', self.__name_datafunc, True), ('Track', self.__track_datafunc, False), ('Title', self.__title_datafunc, True), ('Artist', self.__artist_datafunc, True), ] for title, func, resize in cols: render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) col = Gtk.TreeViewColumn(title, render) col.set_cell_data_func(render, func) col.set_resizable(resize) col.set_expand(resize) self.append_column(col) def update_remote_album(self, remote_album): """Updates the TreeView, handling results with a different number of tracks than the album being tagged.""" for i in range(len(self.model), len(remote_album)): self.model.append((None, )) for i in range(len(self.model), len(remote_album), -1): if self.model[-1][0] is not None: break itr = self.model.get_iter_from_string(str(len(self.model) - 1)) self.model.remove(itr) self.remote_album = remote_album has_artists = bool(filter(lambda t: t.artist, remote_album)) col = self.get_column(3) # sometimes gets called after the treeview is already gone if not col: return col.set_visible(has_artists) self.columns_autosize() self.queue_draw() class ResultComboBox(Gtk.ComboBox): """Formatted picker for different Result entries.""" def __init__(self, model): super(ResultComboBox, self).__init__(model=model) render = Gtk.CellRendererText() render.set_fixed_height_from_font(2) def celldata(layout, cell, model, iter, data): release = model[iter][0] if not release: return date = release.getEarliestReleaseDate() if date: date = '%s, ' % date else: date = '' markup = "<b>%s</b>\n%s - %s%s tracks" % ( util.escape(release.title), util.escape(release.artist.name), date, release.tracksCount) cell.set_property('markup', markup) self.pack_start(render, True) self.set_cell_data_func(render, celldata, None) class ReleaseEventComboBox(Gtk.HBox): """A ComboBox for picking a release event.""" def __init__(self): super(ReleaseEventComboBox, self).__init__() self.model = Gtk.ListStore(object, str) self.combo = Gtk.ComboBox(model=self.model) render = Gtk.CellRendererText() self.combo.pack_start(render, True) self.combo.add_attribute(render, "markup", 1) self.combo.set_sensitive(False) self.label = Gtk.Label(label="_Release:", use_underline=True) self.label.set_use_underline(True) self.label.set_mnemonic_widget(self.combo) self.pack_start(self.label, False, True, 0) self.pack_start(self.combo, True, True, 0) def update(self, release): self.model.clear() events = release.getReleaseEvents() # The catalog number is the most important of these fields, as it's # the source for the 'labelid' tag, which we'll use until MB NGS is # up and running to deal with multi-disc albums properly. We sort to # find the earliest release with a catalog number. events.sort(key=lambda e: (bool(not e.getCatalogNumber()), e.getDate() or '9999-12-31')) for rel_event in events: text = '%s %s: <b>%s</b> <i>(%s)</i>' % ( rel_event.getDate() or '', rel_event.getLabel() or '', rel_event.getCatalogNumber(), rel_event.getCountry()) self.model.append((rel_event, text)) if len(events) > 0: self.combo.set_active(0) self.combo.set_sensitive((len(events) > 0)) text = ngettext("%d _release:", "%d _releases:", len(events)) self.label.set_text(text % len(events)) self.label.set_use_underline(True) def get_release_event(self): itr = self.combo.get_active_iter() if itr: return self.model[itr][0] else: return None class QueryThread: """Daemon thread which does HTTP retries and avoids flooding.""" def __init__(self): self.running = True self.queue = [] thread = threading.Thread(target=self.__run) thread.daemon = True thread.start() def add(self, callback, func, *args, **kwargs): """Add a func to be evaluated in a background thread. Callback will be called with the result from the main thread.""" self.queue.append((callback, func, args, kwargs)) def stop(self): """Stop the background thread.""" self.running = False def __run(self): while self.running: if self.queue: callback, func, args, kwargs = self.queue.pop(0) try: res = func(*args, **kwargs) except: time.sleep(2) try: res = func(*args, **kwargs) except: res = None GLib.idle_add(callback, res) time.sleep(1) class SearchWindow(Gtk.Dialog): def __save(self, widget=None, response=None): """Writes values to Song objects.""" self._qthread.stop() if response != Gtk.ResponseType.ACCEPT: self.destroy() return album = self.current_release shared = {} shared['album'] = album.title if config_get('split_disc', True): m = re.match(r'(.*) \(disc (.*?)\)$', album.title) if m: shared['album'] = m.group(1) disc = m.group(2).split(': ', 1) shared['discnumber'] = disc[0] if len(disc) > 1: shared['discsubtitle'] = disc[1] relevt = self.release_combo.get_release_event() shared['date'] = relevt and relevt.getDate() or '' if shared['date'] and config_get('year_only', False): shared['date'] = shared['date'].split('-')[0] if config_get('labelid', True): if relevt and relevt.getCatalogNumber(): shared['labelid'] = relevt.getCatalogNumber() if not album.isSingleArtistRelease(): if (config_get('albumartist', True) and extractUuid(album.artist.id) != VARIOUS_ARTISTS_ARTISTID): shared['albumartist'] = album.artist.name if config_get('artist_sort', False) and \ album.artist.sortName != album.artist.name: shared['albumartistsort'] = album.artist.sortName if config_get('standard', True): shared['musicbrainz_albumartistid'] = extractUuid(album.artist.id) shared['musicbrainz_albumid'] = extractUuid(album.id) for idx, (song, ) in enumerate(self.result_treeview.model): if song is None: continue song.update(shared) if idx >= len(album.tracks): continue track = album.tracks[idx] song['title'] = track.title song['tracknumber'] = '%d/%d' % (idx + 1, max(len(album.tracks), len(self.result_treeview.model))) if config_get('standard', True): song['musicbrainz_trackid'] = extractUuid(track.id) if album.isSingleArtistRelease() or not track.artist: song['artist'] = album.artist.name if config_get('artist_sort', False) and \ album.artist.sortName != album.artist.name: song['artistsort'] = album.artist.sortName else: song['artist'] = track.artist.name if config_get('artist_sort', False) and \ track.artist.sortName != track.artist.name: song['artistsort'] = track.artist.sortName if config_get('standard', True): song['musicbrainz_artistid'] = extractUuid(track.artist.id) if config_get('split_feat', False): feats = re.findall(r' \(feat\. (.*?)\)', track.title) if feats: feat = [] for value in feats: values = value.split(', ') if len(values) > 1: values += values.pop().split(' & ') feat += values song['performer'] = '\n'.join(feat) song['title'] = re.sub(r' \(feat\. .*?\)', '', track.title) self.destroy() def __do_query(self, *args): """Search for album using the query text.""" query = self.search_query.get_text() if not query: self.result_label.set_markup("<b>Please enter a query.</b>") self.search_button.set_sensitive(True) return self.result_label.set_markup("<i>Searching...</i>") filt = ws.ReleaseFilter(query=query) self._qthread.add(self.__process_results, self._query.getReleases, filt) def __process_results(self, results): """Callback for search query completion.""" self._resultlist.clear() self.search_button.set_sensitive(True) if results is None: self.result_label.set_text("Error encountered. Please retry.") self.search_button.set_sensitive(True) return for release in map(lambda r: r.release, results): self._resultlist.append((release, )) if len(results) > 0 and self.result_combo.get_active() == -1: self.result_label.set_markup("<i>Loading result...</i>") self.result_combo.set_active(0) else: self.result_label.set_markup("No results found.") def __result_changed(self, combo): """Called when a release is chosen from the result combo.""" idx = combo.get_active() if idx == -1: return rel_id = self._resultlist[idx][0].id if rel_id in self._releasecache: self.__update_results(self._releasecache[rel_id]) else: self.result_label.set_markup("<i>Loading result...</i>") inc = ws.ReleaseIncludes( artist=True, releaseEvents=True, tracks=True) self._qthread.add(self.__update_result, self._query.getReleaseById, rel_id, inc) def __update_result(self, release): """Callback for release detail download from result combo.""" num_results = len(self._resultlist) text = ngettext("Found %d result.", "Found %d results.", num_results) self.result_label.set_text(text % num_results) # issue 973: search can return invalid (or removed) ReleaseIDs if release is None: return self._releasecache.setdefault(extractUuid(release.id), release) self.result_treeview.update_remote_album(release.tracks) self.current_release = release self.release_combo.update(release) save_button = dialog_get_widget_for_stockid(self, Gtk.STOCK_SAVE) save_button.set_sensitive(True) def __init__(self, album, cache): self.album = album self._query = ws.Query() self._resultlist = Gtk.ListStore(GObject.TYPE_PYOBJECT) self._releasecache = cache self._qthread = QueryThread() self.current_release = None super(SearchWindow, self).__init__("MusicBrainz lookup", buttons=( Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)) self.set_default_size(650, 500) self.set_border_width(5) save_button = dialog_get_widget_for_stockid(self, Gtk.STOCK_SAVE) save_button.set_sensitive(False) vb = Gtk.VBox() vb.set_spacing(8) hb = Gtk.HBox() hb.set_spacing(8) sq = self.search_query = Gtk.Entry() sq.connect('activate', self.__do_query) alb = '"%s"' % album[0].comma("album").replace('"', '') art = get_artist(album) if art: alb = '%s AND artist:"%s"' % (alb, art.replace('"', '')) sq.set_text('%s AND tracks:%d' % (alb, get_trackcount(album))) lbl = Gtk.Label(label="_Query:") lbl.set_use_underline(True) lbl.set_mnemonic_widget(sq) stb = self.search_button = Gtk.Button('S_earch', use_underline=True) stb.connect('clicked', self.__do_query) hb.pack_start(lbl, False, True, 0) hb.pack_start(sq, True, True, 0) hb.pack_start(stb, False, True, 0) vb.pack_start(hb, False, True, 0) self.result_combo = ResultComboBox(self._resultlist) self.result_combo.connect('changed', self.__result_changed) vb.pack_start(self.result_combo, False, True, 0) rhb = Gtk.HBox() rl = Gtk.Label() rl.set_markup("Results <i>(drag to reorder)</i>") rl.set_alignment(0, 0.5) rhb.pack_start(rl, False, True, 0) rl = self.result_label = Gtk.Label(label="") rhb.pack_end(rl, False, True, 0) vb.pack_start(rhb, False, True, 0) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS) rtv = self.result_treeview = ResultTreeView(self.album) rtv.set_border_width(8) sw.add(rtv) vb.pack_start(sw, True, True, 0) hb = Gtk.HBox() hb.set_spacing(8) self.release_combo = ReleaseEventComboBox() vb.pack_start(self.release_combo, False, True, 0) self.get_content_area().pack_start(vb, True, True, 0) self.connect('response', self.__save) stb.emit('clicked') self.show_all() class MyBrainz(SongsMenuPlugin): PLUGIN_ID = "MusicBrainz lookup" PLUGIN_NAME = "MusicBrainz Lookup" PLUGIN_ICON = Gtk.STOCK_CDROM PLUGIN_DESC = 'Retag an album based on a MusicBrainz search.' PLUGIN_VERSION = '0.5' cache = {} def plugin_albums(self, albums): for album in albums: discs = {} for song in album: discnum = int(song.get('discnumber', '1').split('/')[0]) discs.setdefault(discnum, []).append(song) for disc in discs.values(): SearchWindow(disc, self.cache).run() @classmethod def PluginPreferences(self, win): items = [ ('split_disc', 'Split _disc from album', True), ('split_feat', 'Split _featured performers from track', False), ('year_only', 'Only use year for "date" tag', False), ('albumartist', 'Write "_albumartist" when needed', True), ('artist_sort', 'Write sort tags for artist names', False), ('standard', 'Write _standard MusicBrainz tags', True), ('labelid', 'Write _labelid tag (fixes multi-disc albums)', True), ] vb = Gtk.VBox() vb.set_spacing(8) for key, label, default in items: ccb = ConfigCheckButton(label, 'plugins', 'brainz_' + key) ccb.set_active(config_get(key, default)) vb.pack_start(ccb, True, True, 0) return vb ��������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/albumart.py�������������������������������������������������������0000644�0001750�0001750�00000115214�12173213426�021615� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- # Copyright 2005-2011 By: # Eduardo Gonzalez, Niklas Janlert, Christoph Reiter, Antonio Riva, # Aymeric Mansoux, Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation # Sat, 01 Aug 2009 13:19:31 by Christoph Reiter <christoph.reiter@gmx.at> # - Fix coverparadise by handling bad HTML better # - Use AppEngine webapp proxy (by wm_eddie) for Amazon # - Increase search limit to 7 (from 5) # - Treeview hints and DND # - Some cleanup and version bump -> 0.5.1 # Wed Mar 04 09:11:28 2009 by Christoph Reiter <christoph.reiter@gmx.at> # - Nearly complete rewrite # - search engines: darktown, coverparadise, amazon (no aws, because # there was no search limit which would cause endless searching for # common terms and loosing a dependency is always good) and discogs # - new: open with GIMP, image zooming mode, absolutely no UI freezes, # enable/disable search engines # - Bumped version number to 0.5 # Wed May 21 21:16:48 EDT 2008 by <wm.eddie@gmail.com> # - Some cleanup # - Added to SVN # - Bumped version number to 0.41 # Tue 2008-05-13 19:40:12 (+0200) by <wxcover@users.sourceforge.net> # - Added walmart, darktown and buy.com cover searching. # - Few fixes # - Updated version number (0.25 -> 0.4) # Mon 2008-05-05 14:54:27 (-0400) # - Updated for new Amazon API by Jeremy Cantrell <jmcantrell@gmail.com> import os import time import threading import gzip import urllib import urllib2 from HTMLParser import HTMLParser, HTMLParseError from cStringIO import StringIO from xml.dom import minidom from gi.repository import Gtk, Pango, GLib, Gdk, GdkPixbuf from quodlibet import util, qltk, config, print_w, app from quodlibet.qltk.views import AllTreeView from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.parse import Pattern USER_AGENT = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13) " \ "Gecko/20101210 Iceweasel/3.6.13 (like Firefox/3.6.13)" def get_encoding_from_socket(socket): content_type = socket.headers.get("Content-Type", "") p = map(str.strip, map(str.lower, content_type.split(";"))) enc = [t.split("=")[-1].strip() for t in p if t.startswith("charset")] return (enc and enc[0]) or "utf-8" def get_url(url, post={}, get={}): post_params = urllib.urlencode(post) get_params = urllib.urlencode(get) if get: get_params = '?' + get_params # add post, get data and headers url = '%s%s' % (url, get_params) if post_params: request = urllib2.Request(url, post_params) else: request = urllib2.Request(url) # for discogs request.add_header('Accept-Encoding', 'gzip') request.add_header('User-Agent', USER_AGENT) url_sock = urllib2.urlopen(request) enc = get_encoding_from_socket(url_sock) # unzip the response if needed data = url_sock.read() if url_sock.headers.get("content-encoding", "") == "gzip": data = gzip.GzipFile(fileobj=StringIO(data)).read() url_sock.close() return data, enc def get_encoding(url): request = urllib2.Request(url) request.add_header('Accept-Encoding', 'gzip') request.add_header('User-Agent', USER_AGENT) url_sock = urllib2.urlopen(request) return get_encoding_from_socket(url_sock) class BasicHTMLParser(HTMLParser, object): """Basic Parser, stores all tags in a 3 tuple with tagname, attrs and data between the starttags. Ignores nesting but gives a consistent structure. All in all an ugly hack.""" encoding = "utf-8" def __init__(self): super(BasicHTMLParser, self).__init__() self.data = [] self.__buffer = [] #to make the crappy HTMLParser ignore more stuff self.CDATA_CONTENT_ELEMENTS = () def parse_url(self, url, post={}, get={}): """Will read the data and parse it into the data variable. A tag will be ['tagname', {all attributes}, 'data until the next tag'] Only starttags are handled/used.""" self.data = [] text, self.encoding = get_url(url, post, get) text = text.decode(self.encoding, 'replace') #strip script tags/content. HTMLParser doesn't handle them well text = "".join([p.split("script>")[-1] for p in text.split("<script")]) try: self.feed(text) self.close() except HTMLParseError: pass def handle_starttag(self, tag, attrs): if self.__buffer: self.__buffer.append('') self.data.append(self.__buffer) self.__buffer = [] self.__buffer = [tag, dict(attrs)] def handle_data(self, data): if self.__buffer: self.__buffer.append(data) self.data.append(self.__buffer) self.__buffer = [] else: self.data.append(['', {}, data]) class CoverParadiseParser(BasicHTMLParser): """A class for searching covers from coverparadise.to""" ROOT_URL = 'http://coverparadise.to' def start(self, query, limit=10): """Start the search and return a list of covers""" if isinstance(query, str): query = query.decode("utf-8") # site only takes 3+ chars if len(query) < 3: return [] query = query.encode(get_encoding(self.ROOT_URL)) # parse the first page self.__parse_search_list(query) # get the max number of offsets and the step size max_offset = -1 step_size = 0 for i, (tag, attr, data) in enumerate(self.data): if "SimpleSearchPage" in attr.get("href", ""): offset = int(attr["href"].split(",")[1].strip("' ")) if offset > max_offset: step_size = step_size or (offset - max_offset) max_offset = offset if max_offset == -1: # if there is no offset, this is a single result page covers = self.__extract_from_single() else: # otherwise parse it as a list for each page covers = self.__extract_from_list() for offset in range(step_size, max_offset + 1, step_size): if len(covers) >= limit: break self.__parse_search_list(query, offset) covers.extend(self.__extract_from_list()) del self.data return covers def __parse_search_list(self, query, offset=0): post = {"SearchString": query, "Page": offset, "Sektion": "2", } self.parse_url(self.ROOT_URL + '/?Module=SimpleSearch', post=post) def __extract_from_single(self): covers = [] cover = None for i, (tag, attr, data) in enumerate(self.data): data = data.strip() if attr.get("class", "") == "ThumbDetails": cover = {"source": self.ROOT_URL} if cover: if attr.get("href"): cover["cover"] = self.ROOT_URL + attr["href"] if attr.get("src") and "thumbnail" not in cover: cover["thumbnail"] = attr["src"] if "front" not in attr.get("alt").lower(): cover = None continue if attr.get("title"): cover["name"] = attr["title"] if tag == "br": if data.endswith("px"): cover["resolution"] = data.strip("@ ") elif data.lower().endswith("b"): cover["size"] = data.strip("@ ") if len(cover.keys()) >= 6: covers.append(cover) cover = None return covers def __extract_from_list(self): covers = [] cover = None old_data = "" last_entry = "" for i, (tag, attr, data) in enumerate(self.data): data = data.strip() if "ViewEntry" in attr.get("href", "") and \ attr.get("href") != last_entry: cover = {"source": self.ROOT_URL} last_entry = attr.get("href") if cover: if attr.get("src") and "thumbnail" not in cover: cover["thumbnail"] = attr["src"] uid = attr["src"].rsplit("/")[-1].split(".")[0] url = self.ROOT_URL + "/res/exe/GetElement.php?ID=" + uid cover["cover"] = url if data and "name" not in cover: cover["name"] = data if "dimension" in old_data.lower() and data: cover["resolution"] = data if "filesize" in old_data.lower() and data: cover["size"] = data if len(cover.keys()) >= 6: covers.append(cover) cover = None old_data = data return covers class DiscogsParser(object): """A class for searching covers from discogs.com""" def __init__(self): self.api_key = 'e404383a2a' self.url = 'http://www.discogs.com' self.cover_list = [] self.limit = 0 self.limit_count = 0 def __get_search_page(self, page, query): """Returns the XML dom of a search result page. Starts with 1.""" search_url = self.url + '/search' search_paras = {} search_paras['type'] = 'releases' search_paras['q'] = query search_paras['f'] = 'xml' search_paras['api_key'] = self.api_key search_paras['page'] = page data, enc = get_url(search_url, get=search_paras) dom = minidom.parseString(data) return dom def __parse_list(self, dom): """Returns a list with the album name and the uri. Since the naming of releases in the specific release pages seems complex.. use the one from the search result page.""" list = [] results = dom.getElementsByTagName('result') for result in results: uri_tag = result.getElementsByTagName('uri')[0] uri = uri_tag.firstChild.data name = result.getElementsByTagName('title')[0].firstChild.data list.append((uri, name)) return list def __parse_release(self, url, name): """Parse the release page and add the cover to the list.""" if len(self.cover_list) >= self.limit: return rel_paras = {} rel_paras['api_key'] = self.api_key rel_paras['f'] = 'xml' data, enc = get_url(url, get=rel_paras) dom = minidom.parseString(data) imgs = dom.getElementsByTagName('image') cover = {} for img in imgs: if img.getAttribute('type') == 'primary': cover['cover'] = img.getAttribute('uri') width = img.getAttribute('width') height = img.getAttribute('height') cover['resolution'] = '%s x %s px' % (width, height) cover['thumbnail'] = img.getAttribute('uri150') cover['name'] = name cover['size'] = get_size_of_url(cover['cover']) cover['source'] = self.url break if cover and len(self.cover_list) < self.limit: self.cover_list.append(cover) def start(self, query, limit=10): """Start the search and return the covers""" self.limit = limit self.limit_count = 0 self.cover_list = [] page = 1 limit_stop = False while 1: dom = self.__get_search_page(page, query) result = dom.getElementsByTagName('searchresults') if not result: break #all = number of all results, end = last result number on the page all = int(result[0].getAttribute('numResults')) end = int(result[0].getAttribute('end')) urls = self.__parse_list(dom) thread_list = [] for url, name in urls: self.limit_count += 1 thr = threading.Thread(target=self.__parse_release, args=(url, name)) thr.setDaemon(True) thr.start() thread_list.append(thr) #Don't search forever if there are many entries with no image #In the default case of limit=10 this will prevent searching #the second result page... if self.limit_count >= self.limit * 2: limit_stop = True break for thread in thread_list: thread.join() if end >= all or limit_stop: break page += 1 return self.cover_list class AmazonParser(object): """A class for searching covers from amazon""" def __init__(self): self.page_count = 0 self.covers = [] self.limit = 0 def __parse_page(self, page, query): """Gets all item tags and calls the item parsing function for each""" #Amazon now requires that all requests be signed. #I have built a webapp on AppEngine for this purpose. -- wm_eddie #url = 'http://webservices.amazon.com/onca/xml' url = 'http://qlwebservices.appspot.com/onca/xml' parameters = {} parameters['Service'] = 'AWSECommerceService' parameters['AWSAccessKeyId'] = '0RKH4ZH1JCFZHMND91G2' # Now Ignored. parameters['Operation'] = 'ItemSearch' parameters['ResponseGroup'] = 'Images,Small' parameters['SearchIndex'] = 'Music' parameters['Keywords'] = query parameters['ItemPage'] = page # This specifies where the money goes and needed since 1.11.2011 # (What a good reason to break API..) # ...so use the gnome.org one parameters['AssociateTag'] = 'gnomestore-20' data, enc = get_url(url, get=parameters) dom = minidom.parseString(data) pages = dom.getElementsByTagName('TotalPages') if pages: self.page_count = int(pages[0].firstChild.data) items = dom.getElementsByTagName('Item') for item in items: self.__parse_item(item) if len(self.covers) >= self.limit: break def __parse_item(self, item): """Extract all information and add the covers to the list.""" large = item.getElementsByTagName('LargeImage') small = item.getElementsByTagName('SmallImage') title = item.getElementsByTagName('Title') if large and small and title: cover = {} artist = item.getElementsByTagName('Artist') creator = item.getElementsByTagName('Creator') text = '' if artist: text = artist[0].firstChild.data elif creator: if len(creator) > 1: text = ', '.join([i.firstChild.data for i in creator]) else: text = creator[0].firstChild.data title_text = title[0].firstChild.data if len(text) and len(title_text): text += ' - ' cover['name'] = text + title_text url_tag = small[0].getElementsByTagName('URL')[0] cover['thumbnail'] = url_tag.firstChild.data url_tag = large[0].getElementsByTagName('URL')[0] cover['cover'] = url_tag.firstChild.data #Since we don't know the size, use the one from the HTML header. cover['size'] = get_size_of_url(cover['cover']) h_tag = large[0].getElementsByTagName('Height')[0] height = h_tag.firstChild.data w_tag = large[0].getElementsByTagName('Width')[0] width = w_tag.firstChild.data cover['resolution'] = '%s x %s px' % (width, height) cover['source'] = 'http://www.amazon.com' self.covers.append(cover) def start(self, query, limit=10): """Start the search and returns the covers""" self.page_count = 0 self.covers = [] self.limit = limit self.__parse_page(1, query) if len(self.covers) < limit: for page in xrange(2, self.page_count + 1): self.__parse_page(page, query) if len(self.covers) >= limit: break return self.covers class CoverArea(Gtk.VBox): """The image display and saving part.""" def __init__(self, parent, song): super(CoverArea, self).__init__() self.song = song self.connect('destroy', self.__save_config) self.dirname = song("~dirname") self.main_win = parent self.data_cache = [] self.current_data = None self.current_pixbuf = None self.image = Gtk.Image() self.button = Gtk.Button(stock=Gtk.STOCK_SAVE) self.button.set_sensitive(False) self.button.connect('clicked', self.__save) close_button = Gtk.Button(stock=Gtk.STOCK_CLOSE) close_button.connect('clicked', lambda x: self.main_win.destroy()) self.window_fit = Gtk.CheckButton(_('Fit image to _window'), use_underline=True) self.window_fit.connect('toggled', self.__scale_pixbuf) self.name_combo = Gtk.ComboBoxText() self.cmd = qltk.entry.ValidatingEntry(util.iscommand) #both labels label_open = Gtk.Label(label=_('_Program:')) label_open.set_use_underline(True) label_open.set_mnemonic_widget(self.cmd) label_open.set_justify(Gtk.Justification.LEFT) self.open_check = Gtk.CheckButton(_('_Edit image after saving'), use_underline=True) label_name = Gtk.Label(label=_('File_name:'), use_underline=True) label_name.set_use_underline(True) label_name.set_mnemonic_widget(self.name_combo) label_name.set_justify(Gtk.Justification.LEFT) # set all stuff from the config self.window_fit.set_active(cfg_get('fit', True)) self.open_check.set_active(cfg_get('edit', False)) self.cmd.set_text(cfg_get('edit_cmd', 'gimp')) #create the filename combo box fn_list = ['cover.jpg', 'folder.jpg', '.folder.jpg'] # Issue 374 - add dynamic file names artist = song("artist") alartist = song("albumartist") album = song("album") labelid = song("labelid") if album: fn_list.append("<album>.jpg") if alartist: fn_list.append("<albumartist> - <album>.jpg") else: fn_list.append("<artist> - <album>.jpg") else: print_w("No album for \"%s\". Could be difficult finding art..." % song("~filename")) title = song("title") if title and artist: fn_list.append("<artist> - <title>.jpg") if (labelid): fn_list.append("<labelid>.jpg") set_fn = cfg_get('fn', fn_list[0]) for i, fn in enumerate(fn_list): self.name_combo.append_text(fn) if fn == set_fn: self.name_combo.set_active(i) if self.name_combo.get_active() < 0: self.name_combo.set_active(0) table = Gtk.Table(rows=2, columns=2, homogeneous=False) table.set_row_spacing(0, 5) table.set_row_spacing(1, 5) table.set_col_spacing(0, 5) table.set_col_spacing(1, 5) table.attach(label_open, 0, 1, 0, 1) table.attach(label_name, 0, 1, 1, 2) table.attach(self.cmd, 1, 2, 0, 1) table.attach(self.name_combo, 1, 2, 1, 2) self.scrolled = Gtk.ScrolledWindow() self.scrolled.add_with_viewport(self.image) self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) bbox = Gtk.HButtonBox() bbox.set_spacing(6) bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.button, True, True, 0) bbox.pack_start(close_button, True, True, 0) bb_align = Gtk.Alignment.new(0, 1, 1, 0) bb_align.set_property('right-padding', 6) bb_align.add(bbox) main_hbox = Gtk.HBox() main_hbox.pack_start(table, False, True, 6) main_hbox.pack_start(bb_align, True, True, 0) top_hbox = Gtk.HBox() top_hbox.pack_start(self.open_check, True, True, 0) top_hbox.pack_start(self.window_fit, False, True, 0) main_vbox = Gtk.VBox() main_vbox.pack_start(top_hbox, True, True, 2) main_vbox.pack_start(main_hbox, True, True, 0) self.pack_start(self.scrolled, True, True, 0) self.pack_start(main_vbox, False, True, 5) # 5 MB image cache size self.max_cache_size = 1024 * 1024 * 5 #for managing fast selection switches of covers.. self.stop_loading = False self.loading = False self.current_job = 0 def __save(self, *data): """save the cover, spawn the program to edit it if selected""" filename = self.name_combo.get_active_text() # Allow support for filename patterns pattern = Pattern(filename) filename = util.fsencode(pattern.format(self.song)) file_path = os.path.join(self.dirname, filename) if os.path.exists(file_path) and not qltk.ConfirmAction(None, _('File exists'), _('The file <b>%s</b> already exists.' '\n\nOverwrite?') % util.escape(filename)).run(): return try: f = open(file_path, 'wb') f.write(self.current_data) f.close() except IOError: qltk.ErrorMessage(None, _('Saving failed'), _('Unable to save "%s".') % file_path).run() else: if self.open_check.get_active(): try: util.spawn([self.cmd.get_text(), file_path]) except: pass app.window.emit("artwork-changed", [self.song]) self.main_win.destroy() def __save_config(self, widget): cfg_set('fit', self.window_fit.get_active()) cfg_set('edit', self.open_check.get_active()) cfg_set('edit_cmd', self.cmd.get_text()) cfg_set('fn', self.name_combo.get_active_text()) def __update(self, loader, *data): """update the picture while it is loading""" if self.stop_loading: return pixbuf = loader.get_pixbuf() GLib.idle_add(self.image.set_from_pixbuf, pixbuf) def __scale_pixbuf(self, *data): if not self.current_pixbuf: return pixbuf = self.current_pixbuf if self.window_fit.get_active(): pb_width = pixbuf.get_width() pb_height = pixbuf.get_height() alloc = self.scrolled.get_allocation() width = alloc.width height = alloc.height if pb_width > width or pb_height > height: pb_ratio = float(pb_width) / pb_height win_ratio = float(width) / height if pb_ratio > win_ratio: scale_w = width scale_h = int(width / pb_ratio) else: scale_w = int(height * pb_ratio) scale_h = height #the size is wrong if the window is about to close if scale_w <= 0 or scale_h <= 0: return thr = threading.Thread( target=self.__scale_async, args=(pixbuf, scale_w, scale_h)) thr.setDaemon(True) thr.start() else: self.image.set_from_pixbuf(pixbuf) else: self.image.set_from_pixbuf(pixbuf) def __scale_async(self, pixbuf, w, h): pixbuf = pixbuf.scale_simple(w, h, GdkPixbuf.InterpType.BILINEAR) GLib.idle_add(self.image.set_from_pixbuf, pixbuf) def __close(self, loader, *data): if self.stop_loading: return self.current_pixbuf = loader.get_pixbuf() GLib.idle_add(self.__scale_pixbuf) def set_cover(self, url): thr = threading.Thread(target=self.__set_async, args=(url,)) thr.setDaemon(True) thr.start() def __set_async(self, url): """manages various stuff like fast switching of covers (aborting old HTTP requests), managing the image cache etc.""" self.current_job += 1 job = self.current_job self.stop_loading = True while self.loading: time.sleep(0.05) self.stop_loading = False if job != self.current_job: return self.loading = True GLib.idle_add(self.button.set_sensitive, False) self.current_pixbuf = None pbloader = GdkPixbuf.PixbufLoader() pbloader.connect('closed', self.__close) #look for cached images raw_data = None for entry in self.data_cache: if entry[0] == url: raw_data = entry[1] break if not raw_data: pbloader.connect('area-updated', self.__update) data_store = StringIO() try: request = urllib2.Request(url) request.add_header('User-Agent', USER_AGENT) url_sock = urllib2.urlopen(request) except urllib2.HTTPError: print_w(_("[albumart] HTTP Error: %s") % url) else: while not self.stop_loading: tmp = url_sock.read(1024 * 10) if not tmp: break pbloader.write(tmp) data_store.write(tmp) url_sock.close() if not self.stop_loading: raw_data = data_store.getvalue() self.data_cache.insert(0, (url, raw_data)) while 1: cache_sizes = [len(data[1]) for data in self.data_cache] if sum(cache_sizes) > self.max_cache_size: del self.data_cache[-1] else: break data_store.close() else: #sleep for fast switching of cached images time.sleep(0.05) if not self.stop_loading: pbloader.write(raw_data) try: pbloader.close() except GLib.GError: pass self.current_data = raw_data if not self.stop_loading: GLib.idle_add(self.button.set_sensitive, True) self.loading = False class AlbumArtWindow(qltk.Window): """The main window including the search list""" def __init__(self, songs): super(AlbumArtWindow, self).__init__() self.image_cache = [] self.image_cache_size = 10 self.search_lock = False self.set_title(_('Album Art Downloader')) self.set_icon_name(Gtk.STOCK_FIND) self.set_default_size(800, 550) image = CoverArea(self, songs[0]) self.liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, object) self.treeview = treeview = AllTreeView(self.liststore) self.treeview.set_headers_visible(False) self.treeview.set_rules_hint(True) targets = [("text/uri-list", 0, 0)] targets = [Gtk.TargetEntry.new(*t) for t in targets] treeview.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY) treeselection = self.treeview.get_selection() treeselection.set_mode(Gtk.SelectionMode.SINGLE) treeselection.connect('changed', self.__select_callback, image) self.treeview.connect("drag-data-get", self.__drag_data_get, treeselection) rend_pix = Gtk.CellRendererPixbuf() img_col = Gtk.TreeViewColumn('Thumb') img_col.pack_start(rend_pix, False) img_col.add_attribute(rend_pix, 'pixbuf', 0) treeview.append_column(img_col) rend_pix.set_property('xpad', 2) rend_pix.set_property('ypad', 2) rend_pix.set_property('width', 56) rend_pix.set_property('height', 56) def escape_data(data): for rep in ('\n', '\t', '\r', '\v'): data = data.replace(rep, ' ') return util.escape(' '.join(data.split())) def cell_data(column, cell, model, iter, data): cover = model[iter][1] esc = escape_data txt = '<b><i>%s</i></b>' % esc(cover['name']) txt += _('\n<small>from <i>%s</i></small>') % esc(cover['source']) if 'resolution' in cover: txt += _('\nResolution: <i>%s</i>') % esc(cover['resolution']) if 'size' in cover: txt += _('\nSize: <i>%s</i>') % esc(cover['size']) cell.markup = txt cell.set_property('markup', cell.markup) rend = Gtk.CellRendererText() rend.set_property('ellipsize', Pango.EllipsizeMode.END) info_col = Gtk.TreeViewColumn('Info', rend) info_col.set_cell_data_func(rend, cell_data) treeview.append_column(info_col) sw_list = Gtk.ScrolledWindow() sw_list.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw_list.set_shadow_type(Gtk.ShadowType.IN) sw_list.add(treeview) self.search_field = Gtk.Entry() self.search_button = Gtk.Button(stock=Gtk.STOCK_FIND) self.search_button.connect('clicked', self.start_search) self.search_field.connect('activate', self.start_search) widget_space = 5 search_hbox = Gtk.HBox(False, widget_space) search_hbox.pack_start(self.search_field, True, True, 0) search_hbox.pack_start(self.search_button, False, True, 0) self.progress = Gtk.ProgressBar() left_vbox = Gtk.VBox(False, widget_space) left_vbox.pack_start(search_hbox, False, True, 0) left_vbox.pack_start(sw_list, True, True, 0) hpaned = Gtk.HPaned() hpaned.set_border_width(widget_space) hpaned.pack1(left_vbox) hpaned.pack2(image) hpaned.set_position(275) self.add(hpaned) self.show_all() left_vbox.pack_start(self.progress, False, True, 0) if songs[0]('albumartist'): text = songs[0]('albumartist') else: text = songs[0]('artist') text += ' - ' + songs[0]('album') self.set_text(text) self.start_search() def __drag_data_get(self, view, ctx, sel, tid, etime, treeselection): model, iter = treeselection.get_selected() if not iter: return cover = model.get_value(iter, 1) sel.set_uris([cover['cover']]) def start_search(self, *data): """Start the search using the text from the text entry""" global engines, config_eng_prefix text = self.search_field.get_text() if not text or self.search_lock: return self.search_lock = True self.search_button.set_sensitive(False) self.progress.set_fraction(0) self.progress.set_text(_('Searching...')) self.progress.show() self.liststore.clear() search = CoverSearch(self.__search_callback) for eng in engines: if cfg_get(config_eng_prefix + eng['config_id'], True): search.add_engine(eng['class'], eng['replace']) search.start(text) #focus the list self.treeview.grab_focus() def set_text(self, text): """set the text and move the cursor to the end""" self.search_field.set_text(text) self.search_field.emit('move-cursor', Gtk.MovementStep.BUFFER_ENDS, 0, False) def __select_callback(self, selection, image): model, iter = selection.get_selected() if not iter: return cover = model.get_value(iter, 1) image.set_cover(cover['cover']) def __add_cover_to_list(self, cover): try: pbloader = GdkPixbuf.PixbufLoader() pbloader.write(get_url(cover['thumbnail'])[0]) pbloader.close() size = 48 pixbuf = pbloader.get_pixbuf().scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR) thumb = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, size + 2, size + 2) thumb.fill(0x000000ff) pixbuf.copy_area(0, 0, size, size, thumb, 1, 1) except (GLib.GError, IOError): pass else: def append(data): self.liststore.append(data) GLib.idle_add(append, [thumb, cover]) def __search_callback(self, covers, progress): for cover in covers: self.__add_cover_to_list(cover) if self.progress.get_fraction() < progress: self.progress.set_fraction(progress) if progress >= 1: self.progress.set_text(_('Done')) GLib.timeout_add(700, self.progress.hide) self.search_button.set_sensitive(True) self.search_lock = False class CoverSearch(object): """Class for glueing the search eninges together. No UI stuff.""" def __init__(self, callback): self.engine_list = [] self.callback = callback self.finished = 0 self.overall_limit = 7 def add_engine(self, engine, query_replace): """Adds a new search engine, query_replace is the string with witch all special characters get replaced""" self.engine_list.append((engine, query_replace)) def start(self, query): """Start search. The callback function will be called after each of the search engines has finished.""" for engine, replace in self.engine_list: thr = threading.Thread(target=self.__search_thread, args=(engine, query, replace)) thr.setDaemon(True) thr.start() #tell the other side that we are finished if there is nothing to do. if not len(self.engine_list): GLib.idle_add(self.callback, [], 1) def __search_thread(self, engine, query, replace): """Creates searching threads which call the callback function after they are finished""" clean_query = self.__cleanup_query(query, replace) result = [] try: result = engine().start(clean_query, self.overall_limit) except Exception: print_w("[AlbumArt] %s: %r" % (engine.__name__, query)) util.print_exc() self.finished += 1 #progress is between 0..1 progress = float(self.finished) / len(self.engine_list) GLib.idle_add(self.callback, result, progress) def __cleanup_query(self, query, replace): """split up at '-', remove some chars, only keep the longest words.. more false positives but much better results""" query = query.lower() if query.startswith("the "): query = query[4:] split = query.split('-') replace_str = ('+', '&', ',', '.', '!', '´', '\'', ':', ' and ', '(', ')') new_query = '' for part in split: for stri in replace_str: part = part.replace(stri, replace) p_split = part.split() p_split.sort(lambda x, y: len(y) - len(x)) p_split = p_split[:max(len(p_split) / 4, max(4 - len(p_split), 2))] new_query += ' '.join(p_split) + ' ' return new_query.rstrip() #------------------------------------------------------------------------------ def cfg_get(key, default): try: if type(default) == bool: value = config.getboolean('plugins', "cover_" + key) else: value = config.get('plugins', "cover_" + key) try: return type(default)(value) except ValueError: return default except (config.Error, AttributeError): return default config_eng_prefix = 'engine_' #------------------------------------------------------------------------------ def cfg_set(key, value): if type(value) == bool: value = str(bool(value)).lower() config.set('plugins', "cover_" + key, value) #------------------------------------------------------------------------------ def get_size_of_url(url): request = urllib2.Request(url) request.add_header('Accept-Encoding', 'gzip') request.add_header('User-Agent', USER_AGENT) url_sock = urllib2.urlopen(request) size = url_sock.headers.get('content-length') url_sock.close() if size: size = int(size) / 1024.0 if size < 1024: return '%.2f KB' % size else: return '%.2f MB' % size / 1024 else: return '' #------------------------------------------------------------------------------ engines = [] #------- eng = {} eng['class'] = CoverParadiseParser eng['url'] = 'http://www.coverparadise.to/' eng['replace'] = '*' eng['config_id'] = 'coverparadise' engines.append(eng) #------- eng = {} eng['class'] = AmazonParser eng['url'] = 'http://www.amazon.com/' eng['replace'] = ' ' eng['config_id'] = 'amazon' engines.append(eng) #------- #eng = {} #eng['class'] = DiscogsParser #eng['url'] = 'http://www.discogs.com/' #eng['replace'] = ' ' #eng['config_id'] = 'discogs' #engines.append(eng) #------------------------------------------------------------------------------ def change_config(checkb, id): global config_eng_prefix cfg_set(config_eng_prefix + id, checkb.get_active()) class DownloadAlbumArt(SongsMenuPlugin): PLUGIN_ID = 'Download Album art' PLUGIN_NAME = _('Download Album Art') PLUGIN_DESC = _('Download album covers from various websites') PLUGIN_ICON = Gtk.STOCK_FIND PLUGIN_VERSION = '0.5.1' def PluginPreferences(klass, window): global engines, change_config, config_eng_prefix table = Gtk.Table(len(engines), 2) table.set_col_spacings(6) table.set_row_spacings(6) frame = qltk.Frame(_("Sources"), child=table) for i, eng in enumerate(sorted(engines, key=lambda x: x["url"])): check = Gtk.CheckButton(eng['config_id'].title()) table.attach(check, 0, 1, i, i + 1) checked = cfg_get(config_eng_prefix + eng['config_id'], True) check.set_active(checked) check.connect('toggled', change_config, eng['config_id']) button = Gtk.Button(eng['url']) button.connect('clicked', lambda s: util.website(s.get_label())) table.attach(button, 1, 2, i, i + 1, xoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) return frame PluginPreferences = classmethod(PluginPreferences) def plugin_album(self, songs): return AlbumArtWindow(songs) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/fingerprint.py����������������������������������������������������0000644�0001750�0001750�00000065242�12173212464�022343� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2011,2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk, GObject, Gst, Pango, GLib import threading import urllib import urllib2 import StringIO import gzip from xml.dom.minidom import parseString from quodlibet import config from quodlibet import util from quodlibet.qltk import Button, Window, Frame from quodlibet.qltk.entry import UndoEntry from quodlibet.qltk.msg import ErrorMessage from quodlibet.plugins.songsmenu import SongsMenuPlugin if not Gst.ElementFactory.find("chromaprint"): from quodlibet import plugins raise plugins.PluginImportException("Couldn't find gst-chromaprint.") def get_num_threads(): # multiprocessing is >= 2.6. # Default to 2 threads if cpu_count isn't implemented for the current arch # or multiprocessing isn't available try: import multiprocessing threads = multiprocessing.cpu_count() except (ImportError, NotImplementedError): threads = 2 return threads class FingerPrintPipeline(threading.Thread): def __init__(self, pool, song, ofa): super(FingerPrintPipeline, self).__init__() self.daemon = True self.__pool = pool self.__song = song self.__cv = threading.Condition() self.__shutdown = False self.__ofa = ofa self.__fingerprints = {} self.__todo = [] self.start() def run(self): # pipeline pipe = Gst.Pipeline() # decode part filesrc = Gst.ElementFactory.make("filesrc", None) pipe.add(filesrc) decode = Gst.ElementFactory.make("decodebin", None) pipe.add(decode) Gst.Element.link(filesrc, decode) # convert to right format convert = Gst.ElementFactory.make("audioconvert", None) resample = Gst.ElementFactory.make("audioresample", None) pipe.add(convert) pipe.add(resample) Gst.Element.link(convert, resample) # ffdec_mp3 got disabled in gstreamer # (for a reason they don't remember), reenable it.. # http://cgit.freedesktop.org/gstreamer/gst-ffmpeg/commit/ # ?id=2de5aaf22d6762450857d644e815d858bc0cce65 ffdec_mp3 = Gst.ElementFactory.find("ffdec_mp3") if ffdec_mp3: ffdec_mp3.set_rank(Gst.Rank.MARGINAL) # decodebin creates pad, we link it decode.connect_object("pad-added", self.__new_decoded_pad, convert) decode.connect("autoplug-sort", self.__sort_decoders) chroma_src = resample use_ofa = self.__ofa and Gst.ElementFactory.find("ofa") if use_ofa: # create a tee and one queue for chroma tee = Gst.ElementFactory.make("tee", None) chroma_queue = Gst.ElementFactory.make("queue", None) pipe.add(tee) pipe.add(chroma_queue) Gst.Element.link(resample, tee) Gst.Element.link(tee, chroma_queue) chroma_src = chroma_queue ofa_queue = Gst.ElementFactory.make("queue", None) ofa = Gst.ElementFactory.make("ofa", None) fake = Gst.ElementFactory.make("fakesink", None) pipe.add(ofa_queue) pipe.add(ofa) pipe.add(fake) Gst.Element.link(tee, ofa_queue) Gst.Element.link(ofa_queue, ofa) Gst.Element.link(ofa, fake) self.__todo.append(ofa) chroma = Gst.ElementFactory.make("chromaprint", None) fake2 = Gst.ElementFactory.make("fakesink", None) pipe.add(chroma) pipe.add(fake2) Gst.Element.link(chroma_src, chroma) Gst.Element.link(chroma, fake2) self.__todo.append(chroma) filesrc.set_property("location", self.__song["~filename"]) # bus bus = pipe.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect("sync-message", self.__bus_message, chroma, use_ofa and ofa) # get it started self.__cv.acquire() pipe.set_state(Gst.State.PLAYING) result = pipe.get_state(timeout=Gst.SECOND / 2)[0] if result == Gst.StateChangeReturn.FAILURE: # something failed, error message kicks in before, so check # for shutdown if not self.__shutdown: self.__shutdown = True GLib.idle_add(self.__pool._callback, self.__song, None, "Error", self) elif not self.__shutdown: # GStreamer probably knows song durations better than we do. # (and it's more precise for PUID lookup) # In case this fails, we insert the mutagen value later # (this only works in active playing state) ok, d = pipe.query_duration(Gst.Format.TIME) if ok: self.__fingerprints["length"] = d / Gst.MSECOND self.__cv.wait() self.__cv.release() # clean up bus.remove_signal_watch() pipe.set_state(Gst.State.NULL) # we need to make sure the state change has finished, before # we can return and hand it over to the python GC pipe.get_state(timeout=Gst.SECOND / 2) def stop(self): self.__shutdown = True self.__cv.acquire() self.__cv.notify() self.__cv.release() def __sort_decoders(self, decode, pad, caps, factories): # mad is the default decoder with GST_RANK_SECONDARY # flump3dec also is GST_RANK_SECONDARY, is slower than mad, # but wins because of its name, ffdec_mp3 is faster but had some # stability problems (which all seem resolved by now and we call # this >= 0.10.31 anyway). Finally there is mpg123 # (http://gst.homeunix.net/) which is even faster but not in the # GStreamer core (FIXME: re-evaluate if it gets merged) # # Example (atom CPU) 248 sec song: # mpg123: 3.5s / ffdec_mp3: 5.5s / mad: 7.2s / flump3dec: 13.3s def set_prio(x): i, f = x i = { "mad": -1, "ffdec_mp3": -2, "mpg123audiodec": -3 }.get(f.get_name(), i) return (i, f) return zip(*sorted(map(set_prio, enumerate(factories))))[1] def __new_decoded_pad(self, convert, pad, *args): pad.link(convert.get_static_pad("sink")) def __bus_message(self, bus, message, chroma, ofa): error = None if message.type == Gst.MessageType.TAG: tags = message.parse_tag() ok, value = tags.get_string("chromaprint-fingerprint") if ok: if chroma in self.__todo: self.__todo.remove(chroma) self.__fingerprints["chromaprint"] = value ok, value = tags.get_string("ofa-fingerprint") if ok: if ofa in self.__todo: self.__todo.remove(ofa) self.__fingerprints["ofa"] = value elif message.type == Gst.MessageType.EOS: error = "EOS" elif message.type == Gst.MessageType.ERROR: error = str(message.parse_error()[0]) if not self.__shutdown and (not self.__todo or error): GLib.idle_add(self.__pool._callback, self.__song, self.__fingerprints, error, self) self.__shutdown = True self.__cv.acquire() self.__cv.notify() self.__cv.release() class FingerPrintThreadPool(GObject.GObject): __gsignals__ = { "fingerprint-done": ( GObject.SignalFlags.RUN_LAST, None, (object, object)), "fingerprint-started": ( GObject.SignalFlags.RUN_LAST, None, (object,)), "fingerprint-error": ( GObject.SignalFlags.RUN_LAST, None, (object, object)), } def __init__(self, max_workers): super(FingerPrintThreadPool, self).__init__() self.__threads = [] self.__queued = [] self.__max_workers = max_workers self.__stopped = False def push(self, song, ofa=False): self.__stopped = False if len(self.__threads) < self.__max_workers: self.__threads.append(FingerPrintPipeline(self, song, ofa)) self.emit("fingerprint-started", song) else: self.__queued.append((song, ofa)) def stop(self): self.__stopped = True for thread in self.__threads: thread.stop() for thread in self.__threads: thread.join() def _callback(self, song, result, error, thread): # make sure everythin is gone before starting new ones. thread.join() self.__threads.remove(thread) if self.__stopped: return if not error: self.emit("fingerprint-done", song, result) else: self.emit("fingerprint-error", song, error) if self.__queued: song, ofa = self.__queued.pop(0) self.__threads.append(FingerPrintPipeline(self, song, ofa)) self.emit("fingerprint-started", song) class MusicDNSThread(threading.Thread): INTERVAL = 1500 URL = "http://ofa.musicdns.org/ofa/1/track" # The anonymous keys give me quota errors. So use the picard one. # I hope that's ok.. #API_KEY = "57aae6071e74345f69143baa210bda87" # anonymous #API_KEY = "e4230822bede81ef71cde723db743e27" # anonymous API_KEY = "0736ac2cd889ef77f26f6b5e3fb8a09c" # mb picard def __init__(self, fingerprints, progress_cb, callback): super(MusicDNSThread, self).__init__() self.__callback = callback self.__fingerprints = fingerprints self.__stopped = False self.__progress_cb = progress_cb self.__sem = threading.Semaphore() self.start() def __get_puid(self, fingerprint, duration): """Returns a PUID for the given libofa fingerprint and duration in milliseconds or None if something fails""" values = { "cid": self.API_KEY, "cvr": "Quod Libet", "fpt": fingerprint, "dur": str(duration), # msecs "brt": "", "fmt": "", "art": "", "ttl": "", "alb": "", "tnm": "", "gnr": "", "yrr": "", } # querying takes about 0.9 secs here, FYI data = urllib.urlencode(values) req = urllib2.Request(self.URL, data) error = None try: response = urllib2.urlopen(req) except urllib2.HTTPError, e: error = "urllib error, code: " + str(e.code) except: error = "urllib error" else: xml = response.read() try: dom = parseString(xml) except: error = "xml error" else: puids = dom.getElementsByTagName("puid") if puids and puids[0].hasAttribute("id"): return puids[0].getAttribute("id") if error: print_w("[fingerprint] " + _("MusicDNS lookup failed: ") + error) def run(self): self.__sem.release() GLib.timeout_add(self.INTERVAL, self.__inc_sem) items = [(s, d) for s, d in self.__fingerprints.iteritems() if "ofa" in d] for i, (song, data) in enumerate(items): self.__sem.acquire() if self.__stopped: return puid = self.__get_puid(data["ofa"], data["length"]) if puid: data["puid"] = puid GLib.idle_add(self.__progress_cb, song, float(i + 1) / len(items)) GLib.idle_add(self.__callback, self) # stop sem increment self.__stopped = True def __inc_sem(self): self.__sem.release() # repeat increment until stopped return not self.__stopped def stop(self): self.__stopped = True self.__sem.release() class AcoustidSubmissionThread(threading.Thread): INTERVAL = 1500 URL = "http://api.acoustid.org/v2/submit" APP_KEY = "C6IduH7D" SONGS_PER_SUBMISSION = 50 # be gentle :) def __init__(self, fingerprints, invalid, progress_cb, callback): super(AcoustidSubmissionThread, self).__init__() self.__callback = callback self.__fingerprints = fingerprints self.__invalid = invalid self.__stopped = False self.__progress_cb = progress_cb self.__sem = threading.Semaphore() self.__done = 0 self.start() def __send(self, urldata): self.__sem.acquire() if self.__stopped: return self.__done += len(urldata) basedata = urllib.urlencode({ "format": "xml", "client": self.APP_KEY, "user": get_api_key(), }) urldata = "&".join([basedata] + map(urllib.urlencode, urldata)) obj = StringIO.StringIO() gzip.GzipFile(fileobj=obj, mode="wb").write(urldata) urldata = obj.getvalue() headers = { "Content-Encoding": "gzip", "Content-type": "application/x-www-form-urlencoded" } req = urllib2.Request(self.URL, urldata, headers) error = None try: response = urllib2.urlopen(req) except urllib2.HTTPError, e: error = "urllib error, code: " + str(e.code) except: error = "urllib error" else: xml = response.read() try: dom = parseString(xml) except: error = "xml error" else: status = dom.getElementsByTagName("status") if not status or not status[0].childNodes or not \ status[0].childNodes[0].nodeValue == "ok": error = "response status error" if error: print_w("[fingerprint] " + _("Submission failed: ") + error) # emit progress GLib.idle_add(self.__progress_cb, float(self.__done) / len(self.__fingerprints)) def run(self): self.__sem.release() GLib.timeout_add(self.INTERVAL, self.__inc_sem) urldata = [] for i, (song, data) in enumerate(self.__fingerprints.iteritems()): if song in self.__invalid: continue track = { "duration": int(round(data["length"] / 1000)), "fingerprint": data["chromaprint"], "bitrate": song("~#bitrate"), "fileformat": song("~format"), "mbid": song("musicbrainz_trackid"), "puid": data.get("puid", "") or song("puid"), "artist": song.list("artist"), "album": song("album"), "albumartist": song("albumartist"), "year": song("~year"), "trackno": song("~#track"), "discno": song("~#disc"), } tuples = [] for key, value in track.iteritems(): # this also dismisses 0.. which should be ok here. if not value: continue # the postfixes don't have to start at a specific point, # they just need to be different and numbers key += ".%d" % i if isinstance(value, list): for val in value: tuples.append((key, val)) else: tuples.append((key, value)) urldata.append(tuples) if len(urldata) >= self.SONGS_PER_SUBMISSION: self.__send(urldata) urldata = [] if self.__stopped: return if urldata: self.__send(urldata) GLib.idle_add(self.__callback, self) # stop sem increment self.__stopped = True def __inc_sem(self): self.__sem.release() # repeat increment until stopped return not self.__stopped def stop(self): self.__stopped = True self.__sem.release() class FingerprintDialog(Window): def __init__(self, songs): super(FingerprintDialog, self).__init__() self.set_border_width(12) self.set_title(_("Submit Acoustic Fingerprints")) self.set_default_size(300, 0) outer_box = Gtk.VBox(spacing=12) box = Gtk.VBox(spacing=6) self.__label = label = Gtk.Label() label.set_markup("<b>%s</b>" % _("Generating fingerprints:")) label.set_alignment(0, 0.5) box.pack_start(label, False, True, 0) self.__bar = bar = Gtk.ProgressBar() self.__set_fraction(0) box.pack_start(bar, False, True, 0) self.__label_song = label_song = Gtk.Label() label_song.set_alignment(0, 0.5) label_song.set_ellipsize(Pango.EllipsizeMode.MIDDLE) box.pack_start(label_song, False, True, 0) self.__stats = stats = Gtk.Label() stats.set_alignment(0, 0.5) expand = Gtk.Expander.new_with_mnemonic(_("_Details")) align = Gtk.Alignment.new(0.0, 0.0, 1.0, 1.0) align.set_padding(6, 0, 6, 0) expand.add(align) align.add(stats) def expand_cb(expand, *args): self.resize(self.get_size()[0], 1) stats.connect("unmap", expand_cb) box.pack_start(expand, False, False, 0) self.__fp_results = {} self.__fp_done = 0 self.__songs = songs self.__musicdns_thread = None self.__acoustid_thread = None self.__invalid_songs = set() self.__mbids = self.__puids = self.__meta = 0 for song in self.__songs: got_puid = bool(song("puid")) got_mbid = bool(song("musicbrainz_trackid")) got_meta = bool(song("artist") and song.get("title") and song("album")) if not got_puid and not got_mbid and not got_meta: self.__invalid_songs.add(song) self.__puids += got_puid self.__mbids += got_mbid self.__meta += got_meta self.__update_stats() pool = FingerPrintThreadPool(get_num_threads()) bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.set_spacing(6) self.__submit = submit = Button(_("_Submit"), Gtk.STOCK_APPLY) submit.set_sensitive(False) submit.connect('clicked', self.__submit_cb) cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL) cancel.connect_object('clicked', self.__cancel_cb, pool) bbox.pack_start(submit, True, True, 0) bbox.pack_start(cancel, True, True, 0) outer_box.pack_start(box, False, True, 0) outer_box.pack_start(bbox, False, True, 0) pool.connect('fingerprint-done', self.__fp_done_cb) pool.connect('fingerprint-error', self.__fp_error_cb) pool.connect('fingerprint-started', self.__fp_started_cb) for song in songs: option = get_puid_lookup() if option == "no_mbid": ofa = not song("musicbrainz_trackid") and not song("puid") elif option == "always": ofa = not song("puid") else: ofa = False pool.push(song, ofa=ofa) self.connect_object('delete-event', self.__cancel_cb, pool) self.add(outer_box) self.show_all() def __update_stats(self): all = len(self.__songs) to_send = all - len(self.__invalid_songs) valid_fp = len(self.__fp_results) text = _("Songs either need a <i><b>musicbrainz_trackid</b></i>, " "a <i><b>puid</b></i>\nor <i><b>artist</b></i> / " "<i><b>title</b></i> / <i><b>album</b></i> tags to get submitted.") text += _("\n\n<i>Fingerprints:</i> %d/%d") % (valid_fp, all) text += _("\n<i>Songs with MBIDs:</i> %d/%d") % (self.__mbids, all) text += _("\n<i>Songs with PUIDs:</i> %d/%d") % (self.__puids, all) text += _("\n<i>Songs with sufficient tags:</i> %d/%d") % ( self.__meta, all) text += _("\n<i>Songs to submit:</i> %d/%d") % (to_send, all) self.__stats.set_markup(text) def __filter_results(self): """Returns a copy of all results which are suitable for sending""" to_send = {} for song, data in self.__fp_results.iteritems(): artist = song("artist") title = song.get("title", "") # title falls back to filename album = song("album") puid = song("puid") or data.get("puid", "") mbid = song("musicbrainz_trackid") if mbid or puid or (artist and title and album): to_send[song] = data return to_send def __set_fraction(self, progress): self.__bar.set_fraction(progress) self.__bar.set_text("%d%%" % round(progress * 100)) def __set_fp_fraction(self): self.__fp_done += 1 frac = self.__fp_done / float(len(self.__songs)) self.__set_fraction(frac) if self.__fp_done == len(self.__songs): GLib.timeout_add(500, self.__start_puid) def __fp_started_cb(self, pool, song): # increase by an amount smaller than one song, so that the user can # see some progress from the beginning. self.__set_fraction(0.5 / len(self.__songs) + self.__bar.get_fraction()) self.__label_song.set_text(song("~filename")) def __fp_done_cb(self, pool, song, result): # fill in song duration if gstreamer failed result.setdefault("length", song("~#length") * 1000) self.__fp_results[song] = result self.__set_fp_fraction() self.__update_stats() def __fp_error_cb(self, pool, song, error): print_w("[fingerprint] " + error) self.__invalid_songs.add(song) self.__set_fp_fraction() self.__update_stats() def __start_puid(self): for song, data in self.__fp_results.iteritems(): if "ofa" in data: self.__label.set_markup("<b>%s</b>" % _("Looking up PUIDs:")) self.__set_fraction(0) self.__musicdns_thread = MusicDNSThread(self.__fp_results, self.__puid_update, self.__puid_done) break else: self.__submit.set_sensitive(True) def __show_final_stats(self): all = len(self.__songs) to_send = all - len(self.__invalid_songs) self.__label_song.set_text( _("Done. %d/%d songs to submit.") % (to_send, all)) def __puid_done(self, thread): thread.join() self.__set_fraction(1.0) self.__show_final_stats() self.__submit.set_sensitive(True) def __puid_update(self, song, progress): self.__label_song.set_text(song("~filename")) self.__set_fraction(progress) if song in self.__fp_results and "puid" in self.__fp_results[song]: self.__puids += 1 self.__invalid_songs.discard(song) self.__update_stats() def __cancel_cb(self, pool, *args): self.destroy() def idle_cancel(): pool.stop() if self.__musicdns_thread: self.__musicdns_thread.stop() if self.__acoustid_thread: self.__acoustid_thread.stop() # pool.stop can block a short time because the CV might be locked # during starting the pipeline -> idle_add -> no GUI blocking GLib.idle_add(idle_cancel) def __submit_cb(self, *args): self.__submit.set_sensitive(False) self.__label.set_markup("<b>%s</b>" % _("Submitting Fingerprints:")) self.__set_fraction(0) self.__acoustid_thread = AcoustidSubmissionThread( self.__fp_results, self.__invalid_songs, self.__acoustid_update, self.__acoustid_done) def __acoustid_update(self, progress): self.__set_fraction(progress) self.__label_song.set_text(_("Submitting...")) def __acoustid_done(self, thread): thread.join() self.__set_fraction(1.0) self.__show_final_stats() GLib.timeout_add(500, self.destroy) def get_api_key(): return config.get("plugins", "fingerprint_acoustid_api_key", "") def get_puid_lookup(): return config.get("plugins", "fingerprint_puid_lookup", "no_mbid") class AcoustidSubmit(SongsMenuPlugin): PLUGIN_ID = "AcoustidSubmit" PLUGIN_NAME = _("Submit Acoustic Fingerprints") PLUGIN_DESC = _("Generates acoustic fingerprints using chromaprint and " "libofa and submits them to 'acoustid.org'") PLUGIN_ICON = Gtk.STOCK_CONNECT PLUGIN_VERSION = "0.1" def plugin_songs(self, songs): if not get_api_key(): ErrorMessage(self, _("API Key Missing"), _("You have to specify an Acoustid.org API key in the plugin " "preferences before you can submit fingerprints.")).run() else: FingerprintDialog(songs) @classmethod def PluginPreferences(self, win): box = Gtk.VBox(spacing=12) # api key section def key_changed(entry, *args): config.set("plugins", "fingerprint_acoustid_api_key", entry.get_text()) button = Button(_("Request API key"), Gtk.STOCK_NETWORK) button.connect("clicked", lambda s: util.website("https://acoustid.org/api-key")) key_box = Gtk.HBox(spacing=6) entry = UndoEntry() entry.set_text(get_api_key()) entry.connect("changed", key_changed) label = Gtk.Label(label=_("API _key:")) label.set_use_underline(True) label.set_mnemonic_widget(entry) key_box.pack_start(label, False, True, 0) key_box.pack_start(entry, True, True, 0) key_box.pack_start(button, False, True, 0) box.pack_start(Frame(_("Acoustid Web Service"), child=key_box), True, True, 0) # puid lookup section puid_box = Gtk.VBox(spacing=6) options = [ ("no_mbid", _("If <i>_musicbrainz__trackid</i> is missing")), ("always", _("_Always")), ("never", _("_Never")), ] def config_changed(radio, value): if radio.get_active(): config.set("plugins", "fingerprint_puid_lookup", value) start_value = get_puid_lookup() radio = None for value, text in options: radio = Gtk.RadioButton(group=radio, label=text, use_underline=True) radio.get_child().set_use_markup(True) radio.set_active(value == start_value) radio.connect("toggled", config_changed, value) puid_box.pack_start(radio, True, True, 0) box.pack_start(Frame(_("PUID Lookup"), child=puid_box), True, True, 0) return box ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/lastfmsync.py�����������������������������������������������������0000644�0001750�0001750�00000024500�12173212464�022167� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2010 Steven Robertson # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os import shelve import urllib import urllib2 import time from datetime import date from threading import Thread from gi.repository import Gtk, GLib from quodlibet import const, config, util, qltk from quodlibet.qltk.entry import UndoEntry from quodlibet.plugins.songsmenu import SongsMenuPlugin try: import json except ImportError: import simplejson as json API_KEY = "f536cdadb4c2aec75ae15e2b719cb3a1" def log(msg): print_d('[lastfmsync] %s' % msg) def apicall(method, **kwargs): """Performs Last.fm API call.""" real_args = { 'api_key': API_KEY, 'format': 'json', 'method': method, } real_args.update(kwargs) url = ''.join(["http://ws.audioscrobbler.com/2.0/?", urllib.urlencode(real_args)]) log(url) uobj = urllib2.urlopen(url) resp = json.load(uobj) if 'error' in resp: errmsg = 'Last.fm API error: %s' % resp.get('message', '') log(errmsg) raise EnvironmentError(resp['error'], errmsg) return resp def config_get(key, default=None): return config.get('plugins', 'lastfmsync_%s' % key, default) class LastFMSyncCache(object): """Stores the Last.fm charts for a particular user.""" def __init__(self, username): self.username = username self.lastupdated = None self.charts = {} self.songs = {} def update_charts(self, progress=None): """Updates Last.fm charts for the given user. Returns True if an update was attempted, False otherwise. progress is a callback func (msg, frac) that will be called to update a UI. 'frac' may be None to indicate no change should be made. If the function returns False, this thread will stop early.""" def prog(msg, frac): if progress: if not progress(msg, frac): # this gets caught later raise ValueError() try: # Last.fm updates their charts weekly; we only poll for new # charts if it's been more than a day since the last poll now = time.time() if not self.lastupdated or self.lastupdated + (24 * 60 * 60) < now: prog("Updating chart list.", 0) resp = apicall('user.getweeklychartlist', user=self.username) charts = resp['weeklychartlist']['chart'] for chart in charts: # Charts keys are 2-tuple (from_timestamp, to_timestamp); # values are whether we still need to fetch the chart fro, to = map(lambda s: int(chart[s]), ('from', 'to')) self.charts.setdefault((fro, to), True) self.lastupdated = now elif not filter(None, self.charts.values()): # No charts to fetch, no update scheduled. prog(_("Already up-to-date."), 1.) return False new_charts = filter(lambda k: self.charts[k], self.charts.keys()) for idx, (fro, to) in enumerate(sorted(new_charts)): chart_week = date.fromtimestamp(fro).isoformat() prog(_("Fetching chart for week of %s.") % chart_week, (idx + 1.) / (len(new_charts) + 2.)) args = {'user': self.username, 'from': fro, 'to': to} try: resp = apicall('user.getweeklytrackchart', **args) except urllib2.HTTPError, err: msg = "HTTP error %d, retrying in %d seconds." log(msg % (err.code, 15)) for i in range(15, 0, -1): time.sleep(1) prog(msg % (err.code, i), None) resp = apicall('user.getweeklytrackchart', **args) try: tracks = resp['weeklytrackchart']['track'] except KeyError: tracks = [] # Delightfully, the API JSON frontend unboxes 1-element lists. if isinstance(tracks, dict): tracks = [tracks] for track in tracks: self._update_stats(track, fro, to) self.charts[(fro, to)] = False prog(_("Sync complete."), 1.) except ValueError: # this is probably from prog() pass except Exception: util.print_exc() prog(_("Error during sync"), None) return False return True def _update_stats(self, track, chart_fro, chart_to): """Updates a single track's stats. 'track' is as returned by API; 'chart_fro' and 'chart_to' are the chart's timestamp range.""" # we try track mbid, (artist mbid, name), (artist name, name) as keys keys = [] if track['mbid']: keys.append(track['mbid']) for artist in (track['artist']['mbid'], track['artist']['#text']): if artist: keys.append((artist.lower(), track['name'].lower())) stats = filter(None, map(self.songs.get, keys)) if stats: # Not sure if last.fm ever changes their tag values, but this # should map all changed values to the same object correctly plays = max(map(lambda d: d.get('playcount', 0), stats)) last = max(map(lambda d: d.get('lastplayed', 0), stats)) added = max(map(lambda d: d.get('added', chart_to), stats)) stats = stats[0] stats.update( {'playcount': plays, 'lastplayed': last, 'added': added}) else: stats = {'playcount': 0, 'lastplayed': 0, 'added': chart_to} stats['playcount'] = stats['playcount'] + int(track['playcount']) stats['lastplayed'] = max(stats['lastplayed'], chart_fro) stats['added'] = min(stats['added'], chart_to) for key in keys: self.songs[key] = stats def update_songs(self, songs): """Updates each SongFile in songs from the cache.""" for song in songs: keys = [] if 'musicbrainz_trackid' in song: keys.append(song['musicbrainz_trackid'].lower()) if 'musiscbrainz_artistid' in song: keys.append((song['musicbrainz_artistid'].lower(), song.get('title', '').lower())) keys.append((song.get('artist', '').lower(), song.get('title', '').lower())) stats = filter(None, map(self.songs.get, keys)) if not stats: continue stats = stats[0] playcount = max(song.get('~#playcount', 0), stats['playcount']) if playcount != 0: song['~#playcount'] = playcount lastplayed = max(song.get('~#lastplayed', 0), stats['lastplayed']) if lastplayed != 0: song['~#lastplayed'] = lastplayed song['~#added'] = min(song['~#added'], stats['added']) class LastFMSyncWindow(Gtk.Dialog): def __init__(self, parent): super(LastFMSyncWindow, self).__init__( _("Last.fm Sync"), parent, buttons=( Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)) self.set_border_width(5) self.set_default_size(300, 100) vbox = Gtk.VBox() vbox.set_spacing(12) self.progbar = Gtk.ProgressBar() vbox.pack_start(self.progbar, False, True, 0) self.status = Gtk.Label(label="") vbox.pack_start(self.status, True, True, 0) self.get_content_area().pack_start(vbox, True, True, 0) self.set_response_sensitive(Gtk.ResponseType.ACCEPT, False) self.show_all() def progress(self, message, fraction): self.status.set_text(message) if fraction is not None: self.progbar.set_fraction(fraction) self.progbar.set_text("%2.1f%%" % (fraction * 100)) if fraction == 1: self.set_response_sensitive(Gtk.ResponseType.ACCEPT, True) class LastFMSync(SongsMenuPlugin): PLUGIN_ID = "Last.fm Sync" PLUGIN_NAME = _("Last.fm Sync") PLUGIN_DESC = ("Update your library's statistics from your " "Last.fm profile.") PLUGIN_ICON = 'gtk-refresh' PLUGIN_VERSION = '0.1' CACHE_PATH = os.path.join(const.USERDIR, "lastfmsync.db") def runner(self, cache): changed = True try: changed = cache.update_charts(self.progress) except: pass if changed: self.cache_shelf[cache.username] = cache self.cache_shelf.close() def progress(self, msg, frac): if self.running: GLib.idle_add(self.dialog.progress, msg, frac) return True else: return False def plugin_songs(self, songs): self.cache_shelf = shelve.open(self.CACHE_PATH) user = config_get('username', '') try: cache = self.cache_shelf.setdefault(user, LastFMSyncCache(user)) except Exception: # unpickle can fail in many ways. this is just cache, so ignore cache = self.cache_shelf[user] = LastFMSyncCache(user) self.dialog = LastFMSyncWindow(self.plugin_window) self.running = True thread = Thread(target=self.runner, args=(cache,)) thread.daemon = True thread.start() resp = self.dialog.run() if resp == Gtk.ResponseType.ACCEPT: cache.update_songs(songs) self.running = False self.dialog.destroy() @classmethod def PluginPreferences(klass, win): def entry_changed(entry): config.set('plugins', 'lastfmsync_username', entry.get_text()) label = Gtk.Label(label=_("_Username:"), use_underline=True) entry = UndoEntry() entry.set_text(config_get('username', '')) entry.connect('changed', entry_changed) label.set_mnemonic_widget(entry) hbox = Gtk.HBox() hbox.set_spacing(6) hbox.pack_start(label, False, True, 0) hbox.pack_start(entry, True, True, 0) return qltk.Frame(_("Account"), child=hbox) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/html.py�����������������������������������������������������������0000644�0001750�0001750�00000004445�12173212464�020756� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Eduardo Gonzalez # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from quodlibet import config from quodlibet.util import tag, escape from quodlibet.plugins.songsmenu import SongsMenuPlugin HTML = '''<?xml version="1.0" encoding="UTF-8"?> <html> <head><title>Quod Libet Playlist

My Quod Libet Playlist


%(headers)s %(songs)s
''' def to_html(songs): cols = config.get_columns() cols_s = "" for col in cols: cols_s += '%s' % tag(col) songs_s = "" for song in songs: s = '' for col in cols: col = {"~#rating": "~rating", "~#length": "~length"}.get( col, col) s += '\n%s' % ( escape(unicode(song.comma(col))) or ' ') s += '' songs_s += s return HTML % {'headers': cols_s, 'songs': songs_s} class ExportToHTML(SongsMenuPlugin): PLUGIN_ID = "Export to HTML" PLUGIN_NAME = _("Export to HTML") PLUGIN_DESC = _("Export the selected song list to HTML.") PLUGIN_ICON = Gtk.STOCK_CONVERT PLUGIN_VERSION = "0.17" def plugin_songs(self, songs): if not songs: return chooser = Gtk.FileChooserDialog( title="Export to HTML", action=Gtk.FileChooserAction.SAVE, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) chooser.set_default_response(Gtk.ResponseType.ACCEPT) resp = chooser.run() if resp != Gtk.ResponseType.ACCEPT: chooser.destroy() return fn = chooser.get_filename() chooser.destroy() with open(fn, "wb") as f: f.write(to_html(songs).encode("utf-8")) quodlibet-plugins-3.0.2/songsmenu/openwith.py0000644000175000017500000000347012173213426021643 0ustar lazkalazka00000000000000from gi.repository import Gtk from quodlibet import util from quodlibet.plugins.songsmenu import SongsMenuPlugin class Command(object): FILES, URIS, FOLDERS = range(3) def __init__(self, title, command, type): self.title = title self.command = command self.type = type def exists(self): return util.iscommand(self.command.split()[0]) def run(self, songs): if self.type == self.FOLDERS: files = [song("~dirname") for song in songs] elif self.type == self.FILES: files = [song("~filename") for song in songs] elif self.type == self.URIS: files = [song("~uri") for song in songs] files = dict.fromkeys(files).keys() util.spawn(self.command.split() + files) class SendTo(SongsMenuPlugin): PLUGIN_ID = 'SendTo' PLUGIN_NAME = _('Send To...') PLUGIN_DESC = _("Generic file-opening plugin.") PLUGIN_ICON = Gtk.STOCK_EXECUTE PLUGIN_VERSION = '1' commands = [ Command("K3B", "k3b --audiocd", Command.FILES), Command("Thunar", "thunar", Command.FOLDERS), ] def __init__(self, *args, **kwargs): super(SendTo, self).__init__(*args, **kwargs) self.command = None submenu = Gtk.Menu() for command in self.commands: item = Gtk.MenuItem(command.title) if not command.exists(): item.set_sensitive(False) else: item.connect_object('activate', self.__set, command) submenu.append(item) if submenu.get_children(): self.set_submenu(submenu) else: self.set_sensitive(False) def __set(self, command): self.command = command def plugin_songs(self, songs): if self.command: self.command.run(songs) quodlibet-plugins-3.0.2/songsmenu/custom_commands.py0000644000175000017500000001564612173212464023212 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2012 Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk import os import re from quodlibet.const import USERDIR from quodlibet import qltk from quodlibet import util from quodlibet.parse._pattern import Pattern from quodlibet.plugins import PluginConfigMixin from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.qltk.data_editors import JSONBasedEditor from quodlibet.qltk.x import SeparatorMenuItem from quodlibet.qltk import ErrorMessage from quodlibet.util.dprint import print_w, print_d, print_e from quodlibet.util.json_data import JSONObject, JSONObjectDict class Command(JSONObject): """ Wraps an arbitrary shell command and its argument pattern. Serialises as JSON for some editability """ @property def data(self): return JSONObject._data(self, ["name", "command", "pattern", "unique", "max_args"]) def __init__(self, name=None, command=None, pattern="<~filename>", unique=False, max_args=10000, warn_threshold=100): JSONObject.__init__(self, name) self.command = str(command or "") self.pattern = str(pattern) self.unique = bool(unique) self.max_args = max_args self.__pat = Pattern(self.pattern) self.warn_threshold = warn_threshold def run(self, songs): """ Runs this command on `songs`, splitting into multiple calls if necessary """ args = [] for song in songs: arg = str(self.__pat.format(song)) if not arg: print_w("Couldn't build shell command using \"%s\"." "Check your pattern?" % self.pattern) break if not self.unique: args.append(arg) elif arg not in args: args.append(arg) max = int((self.max_args or 10000)) com_words = self.command.split(" ") while args: print_d("Running %s with %d substituted arg(s) (of %d%s total)..." % (self.command, min(max, len(args)), len(args), " unique" if self.unique else "")) util.spawn(com_words + args[:max]) args = args[max:] def __str__(self): return "Command= {command} {pattern}".format(**dict(self.data)) class CustomCommands(SongsMenuPlugin, PluginConfigMixin): PLUGIN_ICON = Gtk.STOCK_OPEN PLUGIN_ID = "CustomCommands" PLUGIN_NAME = _("Custom Commands") PLUGIN_DESC = _("Runs custom commands (in batches if required) on songs " "using any of their tags.") PLUGIN_VERSION = '1.0' _TUPLE_DEF = "\s*\('([^']*)'%s\)" % ("(?:,\s*'([^']*)')?" * 5) _TUPLE_REGEX = re.compile(_TUPLE_DEF) # Here are some starters... DEFAULT_COMS = [ Command("Compress files", "file-roller -d", "<~filename>"), Command("K3B", "k3b --audiocd", "<~filename>"), Command("Browse folders (Thunar)", "thunar", "<~dirname>", unique=True, max_args=50, warn_threshold=20), Command(name="Flash notification", command="notify-send" " -i /usr/share/icons/hicolor/scalable/apps/quodlibet.svg", pattern="<~rating> \"<version| (<version>)>\"" "<~people| by <~people>>" "<album|, from <album><discnumber| : disk <discnumber>>" "<~length| (<~length>)>", unique=False, max_args=1, warn_threshold=10), Command("Fix MP3 VBR with mp3val", "mp3val -f", "<~filename>", unique=True, max_args=1), ] COMS_FILE = os.path.join(USERDIR, 'lists', 'customcommands.json') def __set_pat(self, name): self.com_index = name def get_data(self, key): """Gets the pattern for a given key""" try: return self.commands[key] except (KeyError, TypeError): print_d("Invalid key %s" % key) return None @classmethod def edit_patterns(cls, button): cls.commands = cls._get_saved_searches() win = JSONBasedEditor(Command, cls.commands, filename=cls.COMS_FILE, title=_("Edit Custom Commands")) win.show() @classmethod def PluginPreferences(cls, parent): hb = Gtk.HBox(spacing=3) hb.set_border_width(0) button = qltk.Button(_("Edit Custom Commands") + "...", Gtk.STOCK_EDIT) button.set_tooltip_markup(util.escape(_("Supports QL patterns\neg " "<tt>stat <~filename></tt>"))) button.connect("clicked", cls.edit_patterns) hb.pack_start(button, True, True, 0) hb.show_all() return hb @classmethod def _get_saved_searches(cls): filename = cls.COMS_FILE print_d("Loading saved commands from '%s'..." % filename) coms = None try: with open(filename) as f: coms = JSONObjectDict.from_json(Command, f.read()) except (IOError, ValueError), e: print_w("Couldn't open saved commands (%s)" % e) # Failing all else... if not coms: print_d("No commands found in %s. Using defaults." % filename) coms = dict([(c.name, c) for c in cls.DEFAULT_COMS]) print_d("Commands = %s" % coms) return coms def __init__(self, *args, **kwargs): super(CustomCommands, self).__init__(*args, **kwargs) self.com_index = None self.unique_only = False self.commands = {} submenu = Gtk.Menu() self.commands = self._get_saved_searches() for (name, c) in self.commands.items(): item = Gtk.MenuItem(name) item.connect_object('activate', self.__set_pat, name) submenu.append(item) # Add link to editor config = Gtk.MenuItem(_("Edit Custom Commands") + "...") config.connect_object('activate', self.edit_patterns, config) submenu.append(SeparatorMenuItem()) submenu.append(config) if submenu.get_children(): self.set_submenu(submenu) else: self.set_sensitive(False) def plugin_songs(self, songs): # Check this is a launch, not a configure if self.com_index: com = self.get_data(self.com_index) print_d("Running %s" % com) try: com.run(songs) except Exception, err: print_e("Couldn't run command %s: %s %s at" % (com.name, type(err), err)) ErrorMessage( self.plugin_window, _("Unable to run custom command %s" % util.escape(self.com_index)), util.escape(str(err))).run() ������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/cddb.py�����������������������������������������������������������0000644�0001750�0001750�00000022223�12173213426�020677� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os from os import path import CDDB from gi.repository import Gtk from quodlibet.qltk import ErrorMessage, ConfirmAction, Message from quodlibet.const import VERSION from quodlibet.util import tag, escape, expanduser from quodlibet.plugins.songsmenu import SongsMenuPlugin CDDB.proto = 6 # utf8 instead of latin1 CLIENTINFO = {'client_name': "quodlibet", 'client_version': VERSION} class AskAction(ConfirmAction): """A message dialog that asks a yes/no question.""" def __init__(self, *args, **kwargs): kwargs["buttons"] = Gtk.ButtonsType.YES_NO Message.__init__(self, Gtk.MessageType.QUESTION, *args, **kwargs) def sumdigits(n): return sum(map(long, str(n))) def calculate_discid(album): lengths = [song.get('~#length', 0) for song in album] total_time = 0 offsets = [] for length in lengths: offsets.append(total_time) total_time += length checksum = sum(map(sumdigits, offsets)) discid = ((checksum % 0xff) << 24) | (total_time << 8) | len(album) return [discid, len(album)] + [75 * o for o in offsets] + [total_time] def query(category, discid, xcode='utf8:utf8'): discinfo = {} tracktitles = {} dump = path.join(expanduser("~"), '.cddb', category, discid) try: for line in file(dump): if line.startswith("TTITLE"): track, title = line.split("=", 1) try: track = int(track[6:]) except (ValueError): pass else: tracktitles[track] = \ title.decode('utf-8', 'replace').strip() elif line.startswith("DGENRE"): discinfo['genre'] = line.split('=', 1)[1].strip() elif line.startswith("DTITLE"): dtitle = line.split('=', 1)[1].strip().split(' / ', 1) if len(dtitle) == 2: discinfo['artist'], discinfo['title'] = dtitle else: discinfo['title'] = dtitle[0].strip() elif line.startswith("DYEAR"): discinfo['year'] = line.split('=', 1)[1].strip() except EnvironmentError: pass else: return discinfo, tracktitles read, info = CDDB.read(category, discid, **CLIENTINFO) if read != 210: return None try: os.makedirs(path.join(expanduser("~"), '.cddb')) except EnvironmentError: pass try: save = file(dump, 'w') keys = info.keys() keys.sort() for key in keys: print>>save, "%s=%s" % (key, info[key]) save.close() except EnvironmentError: pass xf, xt = xcode.split(':') for key, value in info.iteritems(): try: value = value.decode('utf-8', 'replace').strip().encode( xf, 'replace').decode(xt, 'replace') except AttributeError: pass if key.startswith('TTITLE'): try: tracktitles[int(key[6:])] = value except ValueError: pass elif key == 'DGENRE': discinfo['genre'] = value elif key == 'DTITLE': dtitle = value.strip().split(' / ', 1) if len(dtitle) == 2: discinfo['artist'], discinfo['title'] = dtitle else: discinfo['title'] = dtitle[0].strip() elif key == 'DYEAR': discinfo['year'] = value return discinfo, tracktitles def make_info_label((disc, track), album, discid): message = [] if 'artist' in disc: message.append('%s:\t<b>%s</b>' % ( tag("artist"), escape(disc['artist']))) if 'title' in disc: message.append('%s:\t<b>%s</b>' % ( tag("album"), escape(disc['title']))) if 'year' in disc: message.append('%s:\t<b>%s</b>' % (tag("date"), escape(disc['year']))) if 'genre' in disc: message.append('%s:\t<b>%s</b>' % ( tag("genre"), escape(disc['genre']))) if discid: message.append('%s:\t<b>%s</b>' % (tag("CDDB ID"), escape(discid))) message.append('\n<u>%s</u>' % _('Track List')) keys = track.keys() keys.sort() for key in keys: message.append(' <b>%d.</b> %s' % (key + 1, escape(track[key].encode('utf-8')))) return '\n'.join(message) class CDDBLookup(SongsMenuPlugin): PLUGIN_ID = 'CDDB lookup' PLUGIN_NAME = _('CDDB Lookup') PLUGIN_DESC = 'Look up album information in FreeDB (requires CDDB.py)' PLUGIN_ICON = 'gtk-cdrom' def plugin_album(self, album): discid = calculate_discid(album) try: stat, discs = CDDB.query(discid, **CLIENTINFO) except IOError: ErrorMessage(None, _("Timeout"), _( "Query could not be executed, connection timed out")).run() return if stat in (200, 211): xcode = 'utf8:utf8' dlg = Gtk.Dialog(_('Select an album')) dlg.set_border_width(6) dlg.set_has_separator(False) dlg.set_resizable(False) dlg.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK) dlg.vbox.set_spacing(6) dlg.set_default_response(Gtk.ResponseType.OK) model = Gtk.ListStore(str, str, str, str, str, str) for disc in discs: model.append( [disc[s] for s in ('title', 'category', 'disc_id')] * 2) box = Gtk.ComboBox(model) box.set_active(0) for i in range(3): crt = Gtk.CellRendererText() box.pack_start(crt, True, True, 0) box.set_attributes(crt, text=i) discinfo = Gtk.Label() crosscode = Gtk.ListStore(str) crosscode.append(['utf8:utf8']) crosscode.append(['latin1:latin2']) crosscode.append(['latin1:cp1251']) crosscode.append(['latin1:sjis']) crosscode.append(['latin1:euc-jp']) cbo = Gtk.ComboBoxEntry(crosscode, column=0) cbo.set_active(0) def update_discinfo(combo): xcode = cbo.get_child().get_text() model = combo.get_model() t, c, d, title, cat, discid = model[box.get_active()] info = query(cat, discid, xcode=xcode) discinfo.set_markup( make_info_label(info, album, discs[0]['disc_id'])) def crosscode_cddbinfo(combo): try: xf, xt = combo.get_child().get_text().split(':') for row in model: for show, store in zip(range(0, 3), range(3, 6)): row[show] = row[store].encode( xf, 'replace').decode(xt, 'replace') except: for row in model: for show, store in zip(range(0, 3), range(3, 6)): row[show] = row[store] update_discinfo(box) cbo.connect('changed', crosscode_cddbinfo) box.connect('changed', update_discinfo) update_discinfo(box) dlg.vbox.pack_start(Gtk.Label( _("Select the album you wish to retrieve.", True, True, 0))) dlg.vbox.pack_start(box, True, True, 0) dlg.vbox.pack_start(discinfo, True, True, 0) dlg.vbox.pack_start(cbo, True, True, 0) dlg.vbox.show_all() resp = dlg.run() xcode = cbo.get_child().get_text() if resp == Gtk.ResponseType.OK: t, c, d, title, cat, discid = model[box.get_active()] (disc, track) = query(cat, discid, xcode=xcode) keys = track.keys() keys.sort() for key, song in zip(keys, album): if 'artist' in disc: song['artist'] = disc['artist'] if 'title' in disc: song['album'] = disc['title'] if 'year' in disc: song['date'] = disc['year'] if 'genre' in disc: song['genre'] = disc['genre'] s = track[key].split("/") if len(s) == 2: song['artist'] = s[0] song['title'] = s[1] else: song['title'] = track[key] song['tracknumber'] = '%d/%d' % (key + 1, len(album)) dlg.destroy() else: n = len(album) albumname = album[0]('album') if not albumname: albumname = ngettext('%d track', '%d tracks', n) % n ErrorMessage(None, _("CDDB lookup failed (%s)" % stat), ngettext("%(title)s and %(count)d more...", "%(title)s and %(count)d more...", n - 1) % { 'title': album[0]('~basename'), 'count': n - 1}).run() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/clearerrors.py����������������������������������������������������0000644�0001750�0001750�00000001160�12161032160�022312� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from quodlibet.plugins.songsmenu import SongsMenuPlugin class ForceWrite(SongsMenuPlugin): PLUGIN_ID = "ClearErrors" PLUGIN_NAME = _("Clear Errors") PLUGIN_DESC = _("Clears the ~errors tag from all selected files.") PLUGIN_ICON = 'gtk-clear' PLUGIN_VERSION = "1" def plugin_song(self, song): try: del(song["~errors"]) except KeyError: pass ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/playlist.py�������������������������������������������������������0000644�0001750�0001750�00000012714�12173212464�021651� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2009 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation # # The Unofficial M3U and PLS Specification (Winamp): # http://forums.winamp.com/showthread.php?threadid=65772 import os from gi.repository import Gtk from quodlibet import util, qltk from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.const import HOME as lastfolder if hasattr(os.path, 'relpath'): relpath = os.path.relpath else: # relpath taken from posixpath in Python 2.7 def relpath(path, start=os.path.curdir): """Return a relative version of a path""" if not path: raise ValueError("no path specified") start_list = os.path.abspath(start).split(os.path.sep) path_list = os.path.abspath(path).split(os.path.sep) # Work out how much of the filepath is shared by start and path. i = len(os.path.commonprefix([start_list, path_list])) rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:] if not rel_list: return os.path.curdir return os.path.join(*rel_list) class PlaylistExport(SongsMenuPlugin): PLUGIN_ID = 'Playlist Export' PLUGIN_NAME = _('Playlist Export') PLUGIN_DESC = _('Export songs to M3U or PLS playlists.') PLUGIN_ICON = 'gtk-save' PLUGIN_VERSION = '0.1' def plugin_songs(self, songs): global lastfolder dialog = Gtk.FileChooserDialog(self.PLUGIN_NAME, None, Gtk.FileChooserAction.SAVE, (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) dialog.set_default_response(Gtk.ResponseType.OK) ffilter = Gtk.FileFilter() ffilter.set_name("m3u") ffilter.add_mime_type("audio/x-mpegurl") ffilter.add_pattern("*.m3u") dialog.add_filter(ffilter) ffilter = Gtk.FileFilter() ffilter.set_name("pls") ffilter.add_mime_type("audio/x-scpls") ffilter.add_pattern("*.pls") dialog.add_filter(ffilter) dialog.set_current_folder(lastfolder) diag_cont = dialog.get_child() hbox_path = Gtk.HBox() combo_path = Gtk.ComboBoxText() hbox_path.pack_end(combo_path, False, False, 6) diag_cont.pack_start(hbox_path, False, False, 0) diag_cont.show_all() map(combo_path.append_text, [_("Relative path"), _("Absolute path")]) combo_path.set_active(0) response = dialog.run() if response == Gtk.ResponseType.OK: file_path = dialog.get_filename() dir_path = os.path.dirname(file_path) file_format = dialog.get_filter().get_name() extension = "." + file_format if not file_path.endswith(extension): file_path += extension if os.path.exists(file_path) and not qltk.ConfirmAction( None, _('File exists'), _('The file <b>%s</b> already exists.\n\nOverwrite?') % util.escape(file_path)).run(): dialog.destroy() return relative = combo_path.get_active() == 0 files = self.__get_files(songs, dir_path, relative) if file_format == "m3u": self.__m3u_export(file_path, files) elif file_format == "pls": self.__pls_export(file_path, files) lastfolder = dir_path dialog.destroy() def __get_files(self, songs, dir_path, relative=False): files = [] for song in songs: f = {} if "~uri" in song: f['path'] = song('~filename') f['title'] = song("title") f['length'] = -1 else: path = song('~filename') if relative: path = relpath(path, dir_path) f['path'] = path f['title'] = "%s - %s" % ( song('~people').replace("\n", ", "), song('~title~version')) f['length'] = song('~#length') files.append(f) return files def __file_error(self, file_path): qltk.ErrorMessage( None, _("Unable to export playlist"), _("Writing to <b>%s</b> failed.") % util.escape(file_path)).run() def __m3u_export(self, file_path, files): try: fhandler = open(file_path, "w") except IOError: self.__file_error(file_path) else: text = "#EXTM3U\n" for f in files: text += "#EXTINF:%d,%s\n" % (f['length'], f['title']) text += f['path'] + "\n" fhandler.write(text.encode("utf-8")) fhandler.close() def __pls_export(self, file_path, files): try: fhandler = open(file_path, "w") except IOError: self.__file_error(file_path) else: text = "[playlist]\n" for num, f in enumerate(files): num += 1 text += "File%d=%s\n" % (num, f['path']) text += "Title%d=%s\n" % (num, f['title']) text += "Length%d=%s\n" % (num, f['length']) text += "NumberOfEntries=%d\n" % len(files) text += "Version=2\n" fhandler.write(text.encode("utf-8")) fhandler.close() ����������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/importexport.py���������������������������������������������������0000644�0001750�0001750�00000012505�12173212464�022562� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from os.path import splitext, extsep, dirname from quodlibet import app from quodlibet.qltk import ErrorMessage from quodlibet.const import HOME as lastfolder from quodlibet.plugins.songsmenu import SongsMenuPlugin __all__ = ['Export', 'Import'] def filechooser(save, title): chooser = Gtk.FileChooserDialog( title=(save and "Export %s Metadata to ..." or "Import %s Metadata from ...") % title, action=(save and Gtk.FileChooserAction.SAVE or Gtk.FileChooserAction.OPEN), buttons=(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT, Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT)) for name, pattern in [('Tag files (*.tags)', '*.tags'), ('All Files', '*')]: filter = Gtk.FileFilter() filter.set_name(name) filter.add_pattern(pattern) chooser.add_filter(filter) chooser.set_current_folder(lastfolder) chooser.set_default_response(Gtk.ResponseType.ACCEPT) return chooser class Export(SongsMenuPlugin): PLUGIN_ID = "ExportMeta" PLUGIN_NAME = _("Export Metadata") PLUGIN_ICON = 'gtk-save' def plugin_album(self, songs): songs.sort(lambda a, b: cmp(a('~#track'), b('~#track')) or cmp(a('~basename'), b('~basename')) or cmp(a, b)) chooser = filechooser(save=True, title=songs[0]('album')) resp = chooser.run() fn = chooser.get_filename() chooser.destroy() if resp != Gtk.ResponseType.ACCEPT: return base, ext = splitext(fn) if not ext: fn = extsep.join([fn, 'tags']) global lastfolder lastfolder = dirname(fn) out = open(fn, 'w') for song in songs: print>>out, str(song('~basename')) keys = song.keys() keys.sort() for key in keys: if key.startswith('~'): continue for val in song.list(key): print>>out, '%s=%s' % (key, val.encode('utf-8')) print>>out class Import(SongsMenuPlugin): PLUGIN_ID = "ImportMeta" PLUGIN_NAME = _("Import Metadata") PLUGIN_ICON = 'gtk-open' # Note: the usage of plugin_album here is sometimes NOT what you want. It # supports fixing up tags on several already-known albums just by walking # them via the plugin system and just selecting a new .tags; this mimics # export of several albums. # # However if one of the songs in your album is different from the rest # (e.g. # one isn't tagged, or only one is) it will be passed in as two different # invocations, neither of which has the right size. If you find yourself in # that scenario a lot more than the previous one, change this to # def plugin_songs(self, songs): # and comment out the songs.sort line for safety. def plugin_album(self, songs): songs.sort(lambda a, b: cmp(a('~#track'), b('~#track')) or cmp(a('~basename'), b('~basename')) or cmp(a, b)) chooser = filechooser(save=False, title=songs[0]('album')) box = Gtk.HBox() rename = Gtk.CheckButton("Rename Files") rename.set_active(False) box.pack_start(rename, True, True, 0) append = Gtk.CheckButton("Append Metadata") append.set_active(True) box.pack_start(append, True, True, 0) box.show_all() chooser.set_extra_widget(box) resp = chooser.run() append = append.get_active() rename = rename.get_active() fn = chooser.get_filename() chooser.destroy() if resp != Gtk.ResponseType.ACCEPT: return global lastfolder lastfolder = dirname(fn) metadata = [] names = [] index = 0 for line in open(fn, 'rU'): if index == len(metadata): names.append(line[:line.rfind('.')]) metadata.append({}) elif line == '\n': index = len(metadata) else: key, value = line[:-1].split('=', 1) value = value.decode('utf-8') try: metadata[index][key].append(value) except KeyError: metadata[index][key] = [value] if not (len(songs) == len(metadata) == len(names)): ErrorMessage(None, "Songs mismatch", "There are %(select)d songs selected, but %(meta)d " "songs in the file. Aborting." % dict(select=len(songs), meta=len(metadata))).run() return for song, meta, name in zip(songs, metadata, names): for key, values in meta.iteritems(): if append and key in song: values = song.list(key) + values song[key] = '\n'.join(values) if rename: origname = song['~filename'] newname = name + origname[origname.rfind('.'):] app.library.rename(origname, newname) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/makesorttags.py���������������������������������������������������0000644�0001750�0001750�00000002551�12161032160�022500� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2008 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from quodlibet.plugins.songsmenu import SongsMenuPlugin def artist_to_sort(artist): try: rest, last = artist.rsplit(" ", 1) except ValueError: return None else: return ", ".join([last, rest]) def album_to_sort(album): try: first, rest = album.split(" ", 1) except ValueError: return None else: if first.lower() in ["a", "the"]: return ", ".join([rest, first]) class MakeSortTags(SongsMenuPlugin): PLUGIN_ID = "SortTags" PLUGIN_NAME = _("Create Sort Tags") PLUGIN_DESC = _("Convert album and artist names to sort names, poorly.") PLUGIN_ICON = 'gtk-edit' PLUGIN_VERSION = "1" def plugin_song(self, song): for tag in ["album"]: values = filter(None, map(album_to_sort, song.list(tag))) if values and (tag + "sort") not in song: song[tag + "sort"] = "\n".join(values) for tag in ["artist", "albumartist", "performer"]: values = filter(None, map(artist_to_sort, song.list(tag))) if values and (tag + "sort") not in song: song[tag + "sort"] = "\n".join(values) �������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/browsefolders.py��������������������������������������������������0000644�0001750�0001750�00000013406�12173212464�022667� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 Christoph Reiter, Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import subprocess from gi.repository import Gtk try: import dbus except ImportError: class FakeDbus(object): def __getattribute__(self, name): if name == "DBusException": return Exception raise Exception dbus = FakeDbus() from quodlibet.plugins.songsmenu import SongsMenuPlugin from quodlibet.util.uri import URI from quodlibet.qltk.msg import ErrorMessage from quodlibet.util.dprint import print_d def get_startup_id(): from quodlibet import app app_name = type(app.window).__name__ return "%s_TIME%d" % (app_name, Gtk.get_current_event_time()) # http://www.freedesktop.org/wiki/Specifications/file-manager-interface FDO_PATH = "/org/freedesktop/FileManager1" FDO_NAME = "org.freedesktop.FileManager1" FDO_IFACE = "org.freedesktop.FileManager1" def browse_folders_fdo(songs): bus = dbus.SessionBus() bus_object = bus.get_object(FDO_NAME, FDO_PATH) bus_iface = dbus.Interface(bus_object, dbus_interface=FDO_IFACE) uris = map(URI.frompath, set([s("~dirname") for s in songs])) bus_iface.ShowFolders(uris, get_startup_id()) def browse_files_fdo(songs): bus = dbus.SessionBus() bus_object = bus.get_object(FDO_NAME, FDO_PATH) bus_iface = dbus.Interface(bus_object, dbus_interface=FDO_IFACE) uris = [s("~uri") for s in songs] bus_iface.ShowItems(uris, get_startup_id()) # http://git.xfce.org/xfce/thunar/tree/thunar/thunar-dbus-service-infos.xml XFCE_PATH = "/org/xfce/FileManager" XFCE_NAME = "org.xfce.FileManager" XFCE_IFACE = "org.xfce.FileManager" def browse_folders_thunar(songs, display=""): bus = dbus.SessionBus() bus_object = bus.get_object(XFCE_NAME, XFCE_PATH) bus_iface = dbus.Interface(bus_object, dbus_interface=XFCE_IFACE) uris = map(URI.frompath, set([s("~dirname") for s in songs])) for uri in uris: bus_iface.DisplayFolder(uri, display, get_startup_id()) def browse_files_thunar(songs, display=""): bus = dbus.SessionBus() bus_object = bus.get_object(XFCE_NAME, XFCE_PATH) bus_iface = dbus.Interface(bus_object, dbus_interface=XFCE_IFACE) for song in songs: dirname = song("~dirname") basename = song("~basename") bus_iface.DisplayFolderAndSelect(URI.frompath(dirname), basename, display, get_startup_id()) def browse_folders_gnome_open(songs): dirs = list(set([s("~dirname") for s in songs])) for dir_ in dirs: if subprocess.call(["gnome-open", dir_]) != 0: raise EnvironmentError def browse_folders_xdg_open(songs): dirs = list(set([s("~dirname") for s in songs])) for dir_ in dirs: if subprocess.call(["xdg-open", dir_]) != 0: raise EnvironmentError # http://support.microsoft.com/kb/152457 def browse_folders_win_explorer(songs): dirs = list(set([s("~dirname") for s in songs])) for dir_ in dirs: # FIXME: returns always 1 under XP, but if the # executable isn't found it will raise OSError anyway subprocess.call(["Explorer", "/root,", dir_]) def browse_files_win_explorer(songs): for song in songs: subprocess.call(["Explorer", "/select,", song("~filename")]) class HandlingMixin(object): def plugin_handles(self, songs): # By default, any single song being a file is good enough for song in songs: if song.is_file: return True return False def handle(self, songs): """ Uses the first successful handler in callable list `_HANDLERS` to handle `songs` Returns False if none could be used """ if not hasattr(self, "_HANDLERS"): return False for handler in self._HANDLERS: name = handler.__name__ try: print_d("Trying %r..." % name) handler(songs) except (dbus.DBusException, EnvironmentError): print_d("...failed.") # TODO: caching of failures (re-order list maybe) else: print_d("...success!") return True print_d("No handlers could be used.") return False class BrowseFolders(SongsMenuPlugin, HandlingMixin): PLUGIN_ID = 'Browse Folders' PLUGIN_NAME = _('Browse Folders') PLUGIN_DESC = "View the songs' folders in a file manager" PLUGIN_ICON = Gtk.STOCK_OPEN PLUGIN_VERSION = '1.1' _HANDLERS = [browse_folders_fdo, browse_folders_thunar, browse_folders_xdg_open, browse_folders_gnome_open, browse_folders_win_explorer] def plugin_songs(self, songs): songs = [s for s in songs if s.is_file] print_d("Trying to browse folders...") if not self.handle(songs): ErrorMessage(self.plugin_window, _("Unable to open folders"), _("No program available to open folders.")).run() class BrowseFiles(SongsMenuPlugin, HandlingMixin): PLUGIN_ID = 'Browse Files' PLUGIN_NAME = _('Show File') PLUGIN_DESC = "View the song's file in a file manager" PLUGIN_ICON = Gtk.STOCK_OPEN PLUGIN_VERSION = '1.1' _HANDLERS = [browse_files_fdo, browse_files_thunar, browse_files_win_explorer] def plugin_single_song(self, song): songs = [s for s in [song] if s.is_file] print_d("Trying to browse files...") if not self.handle(songs): ErrorMessage(self.plugin_window, _("Unable to browse files"), _("No program available to browse files.")).run() ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/ifp.py������������������������������������������������������������0000644�0001750�0001750�00000004115�12173213426�020561� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2004-2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation import os from gi.repository import Gtk from quodlibet import util, qltk from quodlibet.plugins.songsmenu import SongsMenuPlugin class IFPUpload(SongsMenuPlugin): PLUGIN_ID = "Send to iFP" PLUGIN_NAME = _("Send to iFP") PLUGIN_DESC = _("Upload songs to an iRiver iFP device.") PLUGIN_VERSION = "0.12" PLUGIN_ICON = Gtk.STOCK_CONVERT def plugin_songs(self, songs): if os.system("ifp typestring"): qltk.ErrorMessage( None, "No iFP device found", "Unable to contact your iFP device. Check " "that the device is powered on and plugged " "in, and that you have ifp-line " "(http://ifp-driver.sf.net) installed.").run() return True self.__madedir = [] w = qltk.WaitLoadWindow( None, len(songs), "Uploading %d/%d", (0, len(songs))) for i, song in enumerate(songs): if self.__upload(song) or w.step(i, len(songs)): w.destroy() return True else: w.destroy() def __upload(self, song): filename = song["~filename"] basename = song("~basename") dirname = os.path.basename(os.path.dirname(filename)) target = os.path.join(dirname, basename) # Avoid spurious calls to ifp mkdir; this can take a long time # on a noisy USB line. if dirname not in self.__madedir: os.system("ifp mkdir %r> /dev/null 2>/dev/null" % dirname) self.__madedir.append(dirname) if os.system("ifp upload %r %r > /dev/null" % (filename, target)): qltk.ErrorMessage( None, "Error uploading", "Unable to upload <b>%s</b>. The device may be " "out of space, or turned off." % ( util.escape(filename))).run() return True ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/k3b.py������������������������������������������������������������0000644�0001750�0001750�00000003110�12173213426�020454� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2005 Joe Wreschnig, # 2009,2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from quodlibet import util from quodlibet.plugins.songsmenu import SongsMenuPlugin class BurnCD(SongsMenuPlugin): PLUGIN_ID = 'Burn CD' PLUGIN_NAME = _('Burn CD') PLUGIN_DESC = 'Burn CDs with K3b or Brasero.' PLUGIN_ICON = 'gtk-cdrom' PLUGIN_VERSION = '0.2' burn_programs = { 'K3b': ['k3b', '--audiocd'], 'Brasero': ['brasero', '--audio'], 'Xfburn': ['xfburn', '--audio-composition'], } def __init__(self, *args, **kwargs): super(BurnCD, self).__init__(*args, **kwargs) self.prog_name = None items = self.burn_programs.items() progs = [(util.iscommand(x[1][0]), x) for x in items] progs.sort(reverse=True) submenu = Gtk.Menu() for (is_cmd, (name, (cmd, arg))) in progs: item = Gtk.MenuItem(name) if not is_cmd: item.set_sensitive(False) else: item.connect_object('activate', self.__set, name) submenu.append(item) self.set_submenu(submenu) def __set(self, name): self.prog_name = name def plugin_songs(self, songs): if self.prog_name is None: return cmd, arg = self.burn_programs[self.prog_name] util.spawn([cmd, arg] + [song['~filename'] for song in songs]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/songsmenu/console.py��������������������������������������������������������0000644�0001750�0001750�00000032155�12173212464�021453� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- # Copyright (C) 2006 - Steve Frécinaux # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Parts from "Interactive Python-GTK Console" # (stolen from epiphany's console.py) # Copyright (C), 1998 James Henstridge <james@daa.com.au> # Copyright (C), 2005 Adam Hooper <adamh@densi.com> # Bits from gedit Python Console Plugin # Copyrignt (C), 2005 Raphaël Slinckx # PythonConsole taken from totem # Plugin parts: # Copyright 2009,2010,2013 Christoph Reiter import sys import re import traceback from gi.repository import Gtk, Pango, Gdk, GLib from quodlibet import qltk, const from quodlibet.plugins.songsmenu import SongsMenuPlugin class PyConsole(SongsMenuPlugin): PLUGIN_ID = 'Python Console' PLUGIN_NAME = _('Python Console') PLUGIN_DESC = _('Interactive Python console') PLUGIN_ICON = 'gtk-execute' PLUGIN_VERSION = '0.2' def plugin_songs(self, songs): win = ConsoleWindow(songs) win.set_icon_name(self.PLUGIN_ICON) win.set_title(self.PLUGIN_DESC + " (Quod Libet)") win.show_all() class ConsoleWindow(Gtk.Window): def __init__(self, songs): Gtk.Window.__init__(self) files = [song('~filename') for song in songs] song_dicts = [song._song for song in songs] self.set_size_request(600, 400) from quodlibet import app console = PythonConsole( namespace={ 'songs': songs, 'files': files, 'sdict': song_dicts, 'app': app}) self.add(console) acces_string = _("You can access the following objects by default:\\n" " '%s' (SongWrapper objects)\\n" " '%s' (Song dictionaries)\\n" " '%s' (Filename list)\\n" " '%s' (Application instance)") % ( "songs", "sdict", "files", "app") dir_string = _("Your current working directory is:") console.eval("import mutagen", False) console.eval("import os", False) console.eval("print \"Python: %s / Quod Libet: %s\"" % (sys.version.split()[0], const.VERSION), False) console.eval("print \"%s\"" % acces_string, False) console.eval("print \"%s \"+ os.getcwd()" % dir_string, False) console.connect("destroy", lambda *x: self.destroy()) class PythonConsole(Gtk.ScrolledWindow): def __init__(self, namespace={}, destroy_cb=None): Gtk.ScrolledWindow.__init__(self) self.destroy_cb = destroy_cb self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) self.set_shadow_type(Gtk.ShadowType.IN) self.view = Gtk.TextView() self.view.modify_font(Pango.font_description_from_string('Monospace')) self.view.set_editable(True) self.view.set_wrap_mode(Gtk.WrapMode.CHAR) self.add(self.view) self.view.show() buffer = self.view.get_buffer() self.normal = buffer.create_tag("normal") self.error = buffer.create_tag("error") self.error.set_property("foreground", "red") self.command = buffer.create_tag("command") self.command.set_property("foreground", "blue") self.__spaces_pattern = re.compile(r'^\s+') self.namespace = namespace self.block_command = False # Init first line buffer.create_mark("input-line", buffer.get_end_iter(), True) buffer.insert(buffer.get_end_iter(), ">>> ") buffer.create_mark("input", buffer.get_end_iter(), True) # Init history self.history = [''] self.history_pos = 0 self.current_command = '' self.namespace['__history__'] = self.history # Set up hooks for standard output. self.stdout = OutFile(self, sys.stdout.fileno(), self.normal) self.stderr = OutFile(self, sys.stderr.fileno(), self.error) # Signals self.view.connect("key-press-event", self.__key_press_event_cb) buffer.connect("mark-set", self.__mark_set_cb) def __key_press_event_cb(self, view, event): modifier_mask = Gtk.accelerator_get_default_mod_mask() event_state = event.state & modifier_mask if event.keyval == Gdk.KEY_d and \ event_state == Gdk.ModifierType.CONTROL_MASK: self.destroy() elif event.keyval == Gdk.KEY_Return and \ event_state == Gdk.ModifierType.CONTROL_MASK: # Get the command buffer = view.get_buffer() inp_mark = buffer.get_mark("input") inp = buffer.get_iter_at_mark(inp_mark) cur = buffer.get_end_iter() line = buffer.get_text(inp, cur, True) self.current_command = self.current_command + line + "\n" self.history_add(line) # Prepare the new line cur = buffer.get_end_iter() buffer.insert(cur, "\n... ") cur = buffer.get_end_iter() buffer.move_mark(inp_mark, cur) # Keep indentation of precendent line spaces = re.match(self.__spaces_pattern, line) if spaces is not None: buffer.insert(cur, line[spaces.start():spaces.end()]) cur = buffer.get_end_iter() buffer.place_cursor(cur) GLib.idle_add(self.scroll_to_end) return True elif event.keyval == Gdk.KEY_Return: # Get the marks buffer = view.get_buffer() lin_mark = buffer.get_mark("input-line") inp_mark = buffer.get_mark("input") # Get the command line inp = buffer.get_iter_at_mark(inp_mark) cur = buffer.get_end_iter() line = buffer.get_text(inp, cur, True) self.current_command = self.current_command + line + "\n" self.history_add(line) # Make the line blue lin = buffer.get_iter_at_mark(lin_mark) buffer.apply_tag(self.command, lin, cur) buffer.insert(cur, "\n") cur_strip = self.current_command.rstrip() if cur_strip.endswith(":") \ or (self.current_command[-2:] != "\n\n" and self.block_command): # Unfinished block command self.block_command = True com_mark = "... " elif cur_strip.endswith("\\"): com_mark = "... " else: # Eval the command self.__run(self.current_command) self.current_command = '' self.block_command = False com_mark = ">>> " # Prepare the new line cur = buffer.get_end_iter() buffer.move_mark(lin_mark, cur) buffer.insert(cur, com_mark) cur = buffer.get_end_iter() buffer.move_mark(inp_mark, cur) buffer.place_cursor(cur) GLib.idle_add(self.scroll_to_end) return True elif event.keyval == Gdk.KEY_KP_Down or event.keyval == Gdk.KEY_Down: # Next entry from history view.emit_stop_by_name("key_press_event") self.history_down() GLib.idle_add(self.scroll_to_end) return True elif event.keyval == Gdk.KEY_KP_Up or event.keyval == Gdk.KEY_Up: # Previous entry from history view.emit_stop_by_name("key_press_event") self.history_up() GLib.idle_add(self.scroll_to_end) return True elif event.keyval == Gdk.KEY_KP_Left or \ event.keyval == Gdk.KEY_Left or \ event.keyval == Gdk.KEY_BackSpace: buffer = view.get_buffer() inp = buffer.get_iter_at_mark(buffer.get_mark("input")) cur = buffer.get_iter_at_mark(buffer.get_insert()) return inp.compare(cur) == 0 elif event.keyval == Gdk.KEY_Home: # Go to the begin of the command instead of the begin of the line buffer = view.get_buffer() inp = buffer.get_iter_at_mark(buffer.get_mark("input")) if event_state == Gdk.ModifierType.SHIFT_MASK: buffer.move_mark_by_name("insert", inp) else: buffer.place_cursor(inp) return True def __mark_set_cb(self, buffer, iter, name): input = buffer.get_iter_at_mark(buffer.get_mark("input")) pos = buffer.get_iter_at_mark(buffer.get_insert()) self.view.set_editable(pos.compare(input) != -1) def get_command_line(self): buffer = self.view.get_buffer() inp = buffer.get_iter_at_mark(buffer.get_mark("input")) cur = buffer.get_end_iter() return buffer.get_text(inp, cur, True) def set_command_line(self, command): buffer = self.view.get_buffer() mark = buffer.get_mark("input") inp = buffer.get_iter_at_mark(mark) cur = buffer.get_end_iter() buffer.delete(inp, cur) buffer.insert(inp, command) buffer.select_range(buffer.get_iter_at_mark(mark), buffer.get_end_iter()) self.view.grab_focus() def history_add(self, line): if line.strip() != '': self.history_pos = len(self.history) self.history[self.history_pos - 1] = line self.history.append('') def history_up(self): if self.history_pos > 0: self.history[self.history_pos] = self.get_command_line() self.history_pos = self.history_pos - 1 self.set_command_line(self.history[self.history_pos]) def history_down(self): if self.history_pos < len(self.history) - 1: self.history[self.history_pos] = self.get_command_line() self.history_pos = self.history_pos + 1 self.set_command_line(self.history[self.history_pos]) def scroll_to_end(self): iter = self.view.get_buffer().get_end_iter() self.view.scroll_to_iter(iter, 0.0, False, 0.5, 0.5) return False def write(self, text, tag=None): buf = self.view.get_buffer() if tag is None: buf.insert(buf.get_end_iter(), text) else: buf.insert_with_tags(buf.get_end_iter(), text, tag) GLib.idle_add(self.scroll_to_end) def eval(self, command, display_command=False): buffer = self.view.get_buffer() lin = buffer.get_mark("input-line") buffer.delete(buffer.get_iter_at_mark(lin), buffer.get_end_iter()) if isinstance(command, list) or isinstance(command, tuple): for c in command: if display_command: self.write(">>> " + c + "\n", self.command) self.__run(c) else: if display_command: self.write(">>> " + c + "\n", self.command) self.__run(command) cur = buffer.get_end_iter() buffer.move_mark_by_name("input-line", cur) buffer.insert(cur, ">>> ") cur = buffer.get_end_iter() buffer.move_mark_by_name("input", cur) self.view.scroll_to_iter(buffer.get_end_iter(), 0.0, False, 0.5, 0.5) def __run(self, command): sys.stdout, self.stdout = self.stdout, sys.stdout sys.stderr, self.stderr = self.stderr, sys.stderr try: try: r = eval(command, self.namespace, self.namespace) if r is not None: print repr(r) except SyntaxError: exec command in self.namespace except: if hasattr(sys, 'last_type') and sys.last_type == SystemExit: self.destroy() else: traceback.print_exc() sys.stdout, self.stdout = self.stdout, sys.stdout sys.stderr, self.stderr = self.stderr, sys.stderr class OutFile(object): """A fake output file object. It sends output to a TK test widget, and if asked for a file number, returns one set on instance creation""" def __init__(self, console, fn, tag): self.fn = fn self.console = console self.tag = tag def close(self): pass def flush(self): pass def fileno(self): return self.fn def isatty(self): return 0 def read(self, a): return '' def readline(self): return '' def readlines(self): return [] def write(self, s): self.console.write(s, self.tag) def writelines(self, l): self.console.write(l, self.tag) def seek(self, a): raise IOError(29, 'Illegal seek') def tell(self): raise IOError(29, 'Illegal seek') truncate = tell �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/editing/��������������������������������������������������������������������0000755�0001750�0001750�00000000000�12173213476�017042� 5����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/editing/resub.py������������������������������������������������������������0000644�0001750�0001750�00000002477�12173212464�020542� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import re from gi.repository import Gtk, GObject from quodlibet.plugins.editing import RenameFilesPlugin, TagsFromPathPlugin class RegExpSub(Gtk.HBox, RenameFilesPlugin, TagsFromPathPlugin): PLUGIN_ID = "Regex Substitution" PLUGIN_NAME = _("Regex Substitution") PLUGIN_DESC = _("Allow arbitrary regex substitutions (s///) when " "tagging or renaming files.") PLUGIN_ICON = Gtk.STOCK_FIND_AND_REPLACE PLUGIN_VERSION = "1" __gsignals__ = { "changed": (GObject.SignalFlags.RUN_LAST, None, ()) } active = True def __init__(self): super(RegExpSub, self).__init__() self._from = Gtk.Entry() self._to = Gtk.Entry() self.pack_start(Gtk.Label("s/"), True, True, 0) self.pack_start(self._from, True, True, 0) self.pack_start(Gtk.Label("/"), True, True, 0) self.pack_start(self._to, True, True, 0) self.pack_start(Gtk.Label("/"), True, True, 0) self._from.connect_object('changed', self.emit, 'changed') self._to.connect_object('changed', self.emit, 'changed') def filter(self, orig_or_tag, value): fr = self._from.get_text().decode('utf-8') to = self._to.get_text().decode('utf-8') try: return re.sub(fr, to, value) except: return value �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/editing/titlecase.py��������������������������������������������������������0000644�0001750�0001750�00000006624�12173212464�021375� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2010-12 Nick Boultbee # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation from gi.repository import Gtk from quodlibet import util from quodlibet.plugins.editing import EditTagsPlugin from quodlibet.plugins import PluginConfigMixin # Cheat list for human title-casing in English. See Issue 424. ENGLISH_INCORRECTLY_CAPITALISED_WORDS = \ [u"The", u"An", u"A", u"'N'", u"'N", u"N'", u"Tha", u"De", u"Da", u"In", u"To", u"For", u"Up", u"With", u"As", u"At", u"From", u"Into", u"On", u"Out", #, u"Over", u"Of", u"By", u"'Til", u"Til", u"And", u"Or", u"Nor", # u"Is", u"Are", u"Am" ] # Allow basic sentence-like concepts eg "Artist: The Greatest Hits" ENGLISH_SENTENCE_ENDS = [".", ":", "-"] def previous_real_word(words, i): """Returns the first word from words before position i that is non-null""" while i > 0: i -= 1 if words[i] != "": break return words[i] def humanise(text): """Returns a more natural (English) title-casing of text Intended for use after util.title() only""" words = text.split(" ") # Yes: to preserve double spacing (!) for i in xrange(1, len(words) - 1): word = words[i] if word in ENGLISH_INCORRECTLY_CAPITALISED_WORDS: prev = previous_real_word(words, i) if (prev and (not prev[-1] in ENGLISH_SENTENCE_ENDS # Add an exception for would-be ellipses... or prev[-3:] == '...')): words[i] = word.lower() return u" ".join(words) class TitleCase(EditTagsPlugin, PluginConfigMixin): PLUGIN_ID = "Title Case" PLUGIN_NAME = _("Title Case") PLUGIN_DESC = _("Title-case tag values in the tag editor.") PLUGIN_ICON = Gtk.STOCK_SPELL_CHECK PLUGIN_VERSION = "1.3" CONFIG_SECTION = "titlecase" # Issue 753: Allow all caps (as before). # Set to False means you get Run Dmc, Ac/Dc, Cd 1/2 etc allow_all_caps = True def process_tag(self, value): if not self.allow_all_caps: value = value.lower() value = util.title(value) return humanise(value) if self.human else value def __init__(self, tag, value): self.allow_all_caps = self.config_get_bool('allow_all_caps', True) self.human = self.config_get_bool('human_title_case', True) super(TitleCase, self).__init__( _("Title-_case Value"), use_underline=True) self.set_image( Gtk.Image.new_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)) self.set_sensitive(self.process_tag(value) != value) @classmethod def PluginPreferences(cls, window): vb = Gtk.VBox() vb.set_spacing(8) config_toggles = [ ('allow_all_caps', _("Allow _ALL-CAPS in tags"), None, True), ('human_title_case', _("_Human title case"), _("Uses common English rules for title casing, as in" " \"Dark Night of the Soul\""), True), ] for key, label, tooltip, default in config_toggles: ccb = cls.ConfigCheckButton(label, key, default) if tooltip: ccb.set_tooltip_text(tooltip) vb.pack_start(ccb, True, True, 0) return vb def activated(self, tag, value): return [(tag, self.process_tag(value))] ������������������������������������������������������������������������������������������������������������quodlibet-plugins-3.0.2/editing/iconv.py������������������������������������������������������������0000644�0001750�0001750�00000005000�12173212464�020521� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation # Encoding magic. Show off the submenu stuff. import locale from gi.repository import Gtk from quodlibet import util from quodlibet.plugins.editing import EditTagsPlugin ENCODINGS = """\ big5 cp1250 cp1251 cp1252 cp1253 cp1254 cp1255 cp1256 cp1257 cp1258 euc_jp euc_jis_2004 euc_jisx0213 euc_kr gb2312 gbk gb18030 iso2022_jp iso2022_kr iso8859_2 iso8859_3 iso8859_4 iso8859_5 iso8859_6 iso8859_7 iso8859_8 iso8859_9 iso8859_10 iso8859_13 iso8859_14 iso8859_15 johab koi8_r koi8_u ptcp154 shift_jis utf_16_be utf_16_le""".split() if util.fscoding not in ENCODINGS + ["utf-8", "latin1"]: ENCODINGS.append(util.fscoding) if locale.getpreferredencoding() not in ENCODINGS + ["utf-8", "latin1"]: ENCODINGS.append(util.fscoding) class Iconv(EditTagsPlugin): PLUGIN_ID = "Convert Encodings" PLUGIN_NAME = _("Convert Encodings") PLUGIN_DESC = _("Fix misinterpreted tag value encodings in the " "tag editor.") PLUGIN_ICON = Gtk.STOCK_CONVERT PLUGIN_VERSION = "2" def __init__(self, tag, value): super(Iconv, self).__init__( _("_Convert Encoding..."), use_underline=True) self.set_image( Gtk.Image.new_from_stock(Gtk.STOCK_CONVERT, Gtk.IconSize.MENU)) submenu = Gtk.Menu() items = [] # Ok, which encodings do work on this string? for enc in ENCODINGS: try: new = value.encode('latin1').decode(enc) except (UnicodeEncodeError, UnicodeDecodeError, LookupError): continue else: if new == value: continue if not new in items: items.append(new) if not items: self.set_sensitive(False) for i in items: item = Gtk.MenuItem() item.value = i item_label = Gtk.Label(label=i) item_label.set_alignment(0.0, 0.5) item.add(item_label) item.connect('activate', self.__convert) submenu.append(item) self.set_submenu(submenu) def __convert(self, item): self.__value = item.value self.activate() def activated(self, tag, value): try: return [(tag, self.__value)] except AttributeError: return [(tag, value)] quodlibet-plugins-3.0.2/editing/kakasi.py�����������������������������������������������������������0000644�0001750�0001750�00000002677�12173213426�020666� 0����������������������������������������������������������������������������������������������������ustar �lazka���������������������������lazka���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import os from gi.repository import Gtk, GObject from quodlibet import util from quodlibet.plugins.editing import RenameFilesPlugin class Kakasi(RenameFilesPlugin, Gtk.CheckButton): PLUGIN_ID = "Kana/Kanji Simple Inverter" PLUGIN_NAME = _("Kana/Kanji Simple Inverter") PLUGIN_DESC = _("Convert kana/kanji to romaji before renaming.") PLUGIN_ICON = Gtk.STOCK_CONVERT PLUGIN_VERSION = "1" __gsignals__ = { "preview": (GObject.SignalFlags.RUN_LAST, None, ()) } def __init__(self): super(Kakasi, self).__init__( _("Romanize _Japanese text"), use_underline=True) self.connect_object('toggled', self.emit, 'preview') active = property(lambda s: s.get_active()) # Use filter list rather than filter to avoid starting a new process # for each filename. def filter_list(self, originals, values): value = "\n".join(values) try: data = value.encode('shift-jis', 'replace') except None: return value line = ("kakasi -isjis -osjis -Ha -Ka -Ja -Ea -ka -s") w, r = os.popen2(line.split()) w.write(data) w.close() try: return r.read().decode('shift-jis').strip().split("\n") except: return values if not util.iscommand("kakasi"): from quodlibet import plugins raise plugins.PluginImportException( "Couldn't find the 'Kanji Kana Simple Inverter' (kakasi).") ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������