def _do_trash_songs(parent, songs, librarian): dialog = TrashDialog.for_songs(parent, songs) resp = dialog.run() if resp != TrashDialog.RESPONSE_TRASH: return window_title = _("Moving %(current)d/%(total)d.") w = WaitLoadWindow(parent, len(songs), window_title) w.show() ok = [] failed = [] for song in songs: filename = song("~filename") try: trash.trash(filename) except trash.TrashError as e: print_w("Couldn't trash file (%s)" % e) failed.append(song) else: ok.append(song) w.step() w.destroy() if failed: ErrorMessage(parent, _("Unable to move to trash"), _("Moving one or more files to the trash failed.") ).run() if ok: librarian.remove(ok)
def MenuItems(marks, player, seekable): sizes = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) items = [] if not marks or marks[0][0] != 0: # Translators: Refers to the beginning of the playing song. marks.insert(0, (0, _("Beginning"))) for time, mark in marks: i = Gtk.MenuItem() # older pygobject (~3.2) added a child on creation if i.get_child(): i.remove(i.get_child()) connect_obj(i, 'activate', player.seek, time * 1000) i.set_sensitive(time >= 0 and seekable) hbox = Gtk.HBox(spacing=12) i.add(hbox) if time < 0: l = Gtk.Label(label=_("N/A")) else: l = Gtk.Label(label=util.format_time(time)) l.set_alignment(0.0, 0.5) sizes.add_widget(l) hbox.pack_start(l, False, True, 0) text = Gtk.Label(mark) text.set_max_width_chars(80) text.set_ellipsize(Pango.EllipsizeMode.END) text.set_alignment(0.0, 0.5) hbox.pack_start(text, True, True, 0) i.show_all() items.append(i) return items
def __search(self, song, buffer, refresh, add): artist = song.comma("artist") title = song.comma("title") try: sock = urlopen( "http://lyricwiki.org/api.php?" "client=QuodLibet&func=getSong&artist=%s&song=%s&fmt=text" % ( quote(artist.encode('utf-8')), quote(title.encode('utf-8')))) text = sock.read() except Exception as err: encoding = util.get_locale_encoding() try: err = err.strerror.decode(encoding, 'replace') except: err = _("Unable to download lyrics.") GLib.idle_add(buffer.set_text, err) return sock.close() if text == 'Not found': GLib.idle_add( buffer.set_text, _("No lyrics found for this song.")) return else: GLib.idle_add(buffer.set_text, text) GLib.idle_add(refresh.set_sensitive, True)
def _do_trash_files(parent, paths): dialog = TrashDialog.for_files(parent, paths) resp = dialog.run() if resp != TrashDialog.RESPONSE_TRASH: return window_title = _("Moving %(current)d/%(total)d.") w = WaitLoadWindow(parent, len(paths), window_title) w.show() ok = [] failed = [] for path in paths: try: trash.trash(path) except trash.TrashError: failed.append(path) else: ok.append(path) w.step() w.destroy() if failed: ErrorMessage(parent, _("Unable to move to trash"), _("Moving one or more files to the trash failed.") ).run()
def __set_inhibit_play(self, inhibit): """Change the inhibit state""" if inhibit == self._inhibit_play: return self._inhibit_play = inhibit # task management if inhibit: if not self._task: def stop_buf(*args): self._player.paused = True self._task = Task(_("Stream"), _("Buffering"), stop=stop_buf) elif self._task: self._task.finish() self._task = None # state management if inhibit: # save the current state status, state, pending = self.bin.get_state( timeout=STATE_CHANGE_TIMEOUT) if status == Gst.StateChangeReturn.SUCCESS and \ state == Gst.State.PLAYING: self._wanted_state = state else: # no idea, at least don't play self._wanted_state = Gst.State.PAUSED self.bin.set_state(Gst.State.PAUSED) else: # restore the old state self.bin.set_state(self._wanted_state) self._wanted_state = None
def check_wrapper_changed(library, parent, songs): needs_write = filter(lambda s: s._needs_write, songs) if needs_write: win = WritingWindow(parent, len(needs_write)) win.show() for song in needs_write: try: song._song.write() except AudioFileError as e: qltk.ErrorMessage( None, _("Unable to edit song"), _("Saving <b>%s</b> failed. The file " "may be read-only, corrupted, or you " "do not have permission to edit it.") % util.escape(song('~basename'))).run() print_d("Couldn't save song %s (%s)" % (song("~filename"), e)) if win.step(): break win.destroy() changed = [] for song in songs: if song._was_updated(): changed.append(song._song) elif not song.valid() and song.exists(): library.reload(song._song) library.changed(changed)
def __init__(self, parent, song): super(Gtk.VBox, self).__init__() self.dialog = parent self.song = song self.original_bpm = song["bpm"] if "bpm" in song else _("n/a") self.set_margin_bottom(6) self.set_spacing(6) box = Gtk.HBox() box.set_spacing(6) # TRANSLATORS: BPM mean "beats per minute" box.pack_start(Gtk.Label(_("BPM:")), False, True, 0) self.bpm_label = Gtk.Label(_("n/a")) self.bpm_label.set_xalign(0.5) box.pack_start(self.bpm_label, True, True, 0) self.reset_btn = Gtk.Button(label=_("Reset")) self.reset_btn.connect('clicked', lambda *x: self.reset()) box.pack_end(self.reset_btn, False, True, 0) self.pack_start(box, False, True, 0) self.tap_btn = Gtk.Button(label=_("Tap")) self.tap_btn.connect('button-press-event', self.tap) self.tap_btn.connect('key-press-event', self.key_tap) self.pack_start(self.tap_btn, True, True, 0) self.init_tap() self.update() self.show_all()
def create_visible_columns_frame(): buttons = {} vbox = Gtk.VBox(spacing=12) table = Gtk.Table.new(3, 3, True) for i, (k, t) in enumerate(self.PREDEFINED_TAGS): x, y = i % 3, i / 3 buttons[k] = Gtk.CheckButton(label=t, use_underline=True) table.attach(buttons[k], x, x + 1, y, y + 1) vbox.pack_start(table, False, True, 0) # Other columns hbox = Gtk.HBox(spacing=6) l = Gtk.Label(label=_("_Others:"), use_underline=True) hbox.pack_start(l, False, True, 0) self.others = others = UndoEntry() others.set_sensitive(False) # Stock edit doesn't have ellipsis chars. edit_button = Gtk.Button( label=_(u"_Edit…"), use_underline=True) edit_button.connect("clicked", self.__config_cols, buttons) edit_button.set_tooltip_text( _("Add or remove additional column " "headers")) l.set_mnemonic_widget(edit_button) l.set_use_underline(True) hbox.pack_start(others, True, True, 0) vbox.pack_start(hbox, False, True, 0) b = Gtk.HButtonBox() b.set_layout(Gtk.ButtonBoxStyle.END) b.pack_start(edit_button, True, True, 0) vbox.pack_start(b, True, True, 0) return qltk.Frame(_("Visible Columns"), child=vbox), buttons
def plugin_songs(self, songs): value = -1 while not 0 <= value <= 1: input_string = GetStringDialog( self.plugin_window, self.PLUGIN_NAME, _("Please give your desired rating on a scale " "from 0.0 to 1.0"), _("_Apply"), Icons.NONE ).run() if input_string is None: return try: value = float(input_string) except ValueError: continue count = len(songs) if (count > 1 and config.getboolean("browsers", "rating_confirm_multiple")): confirm_dialog = ConfirmRateMultipleDialog( self.plugin_window, count, value) if confirm_dialog.run() != Gtk.ResponseType.YES: return for song in songs: song["~#rating"] = value
def __drag_data_received(self, view, ctx, x, y, sel, tid, etime): view.emit_stop_by_name('drag-data-received') targets = [ ("text/uri-list", 0, DND_URI_LIST), ("text/x-moz-url", 0, DND_MOZ_URL) ] targets = [Gtk.TargetEntry.new(*t) for t in targets] view.drag_dest_set(Gtk.DestDefaults.ALL, targets, Gdk.DragAction.COPY) if tid == DND_URI_LIST: uri = sel.get_uris()[0] elif tid == DND_MOZ_URL: uri = sel.data.decode('utf16', 'replace').split('\n')[0] else: ctx.finish(False, False, etime) return ctx.finish(True, False, etime) feed = Feed(uri.encode("ascii", "replace")) feed.changed = feed.parse() if feed: self.__feeds.append(row=[feed]) AudioFeeds.write() else: ErrorMessage( self, _("Unable to add feed"), _("%s could not be added. The server may be down, " "or the location may not be an audio feed.") % util.bold(util.escape(feed.uri))).run()
def tag_editing_vbox(self): """Returns a new VBox containing all tag editing widgets""" vbox = Gtk.VBox(spacing=6) cb = CCB(_("Auto-save tag changes"), 'editing', 'auto_save_changes', populate=True, tooltip=_("Save changes to tags without confirmation " "when editing multiple files")) vbox.pack_start(cb, False, True, 0) hb = Gtk.HBox(spacing=6) e = UndoEntry() e.set_text(config.get("editing", "split_on")) e.connect('changed', self.__changed, 'editing', 'split_on') e.set_tooltip_text( _("A set of separators to use when splitting tag values " "in the tag editor. " "The list is space-separated")) def do_revert_split(button, section, option): config.reset(section, option) e.set_text(config.get(section, option)) split_revert = Button(_("_Revert"), Icons.DOCUMENT_REVERT) split_revert.connect("clicked", do_revert_split, "editing", "split_on") l = Gtk.Label(label=_("Split _on:")) l.set_use_underline(True) l.set_mnemonic_widget(e) hb.pack_start(l, False, True, 0) hb.pack_start(e, True, True, 0) hb.pack_start(split_revert, False, True, 0) vbox.pack_start(hb, False, True, 0) return vbox
def __configure_buttons(self, library): new_pl = qltk.Button(None, Icons.DOCUMENT_NEW, Gtk.IconSize.MENU) new_pl.set_tooltip_text(_("New")) new_pl.connect('clicked', self.__new_playlist, library) import_pl = qltk.Button(None, Icons.LIST_ADD, Gtk.IconSize.MENU) import_pl.set_tooltip_text(_("Import")) import_pl.connect('clicked', self.__import, library) fb = Gtk.FlowBox() fb.set_selection_mode(Gtk.SelectionMode.NONE) fb.set_homogeneous(True) fb.insert(new_pl, 0) fb.insert(import_pl, 1) fb.set_max_children_per_line(2) # The pref button is in its own flowbox instead of directly under the # HBox to make it the same height as the other buttons pref = PreferencesButton(self) fb2 = Gtk.FlowBox() fb2.insert(pref, 0) hb = Gtk.HBox() hb.pack_start(fb, True, True, 0) hb.pack_start(fb2, False, False, 0) self.pack_start(hb, False, False, 0)
def __popup_menu(self, view, library): model, itr = view.get_selection().get_selected() if itr is None: return songs = list(model[itr][0]) songs = [s for s in songs if isinstance(s, AudioFile)] menu = SongsMenu(library, songs, playlists=False, remove=False, ratings=False) menu.preseparate() def _remove(model, itr): playlist = model[itr][0] dialog = ConfirmRemovePlaylistDialog(self, playlist) if dialog.run() == Gtk.ResponseType.YES: playlist.delete() model.get_model().remove( model.convert_iter_to_child_iter(itr)) rem = MenuItem(_("_Delete"), Icons.EDIT_DELETE) connect_obj(rem, 'activate', _remove, model, itr) menu.prepend(rem) def _rename(path): self._start_rename(path) ren = qltk.MenuItem(_("_Rename"), Icons.EDIT) qltk.add_fake_accel(ren, "F2") connect_obj(ren, 'activate', _rename, model.get_path(itr)) menu.prepend(ren) playlist = model[itr][0] PLAYLIST_HANDLER.populate_menu(menu, library, self, [playlist]) menu.show_all() return view.popup_menu(menu, 0, Gtk.get_current_event_time())
def __edit_tag(self, renderer, path, new_value, model): new_value = gdecode(new_value) new_value = ', '.join(new_value.splitlines()) path = Gtk.TreePath.new_from_string(path) entry = model[path][0] error_dialog = None if not massagers.is_valid(entry.tag, new_value): error_dialog = qltk.WarningMessage( self, _("Invalid value"), _("Invalid value: <b>%(value)s</b>\n\n%(error)s") % { "value": new_value, "error": massagers.error_message(entry.tag, new_value)}) else: new_value = massagers.validate(entry.tag, new_value) comment = entry.value changed = comment.text != new_value if (changed and ((comment.shared and comment.complete) or new_value)) \ or (new_value and comment.shared and not comment.complete): # only give an error if we would have applied the value if error_dialog is not None: error_dialog.run() return entry.value = Comment(new_value) entry.edited = True entry.deleted = False model.path_changed(path)
def __mkdir(self, button): model, paths = self.get_selection().get_selected_rows() if len(paths) != 1: return path = paths[0] directory = model[path][0] dir_ = GetStringDialog( None, _("New Folder"), _("Enter a name for the new folder:")).run() if not dir_: return dir_ = glib2fsn(dir_) fullpath = os.path.realpath(os.path.join(directory, dir_)) try: os.makedirs(fullpath) except EnvironmentError as err: error = "<b>%s</b>: %s" % (err.filename, err.strerror) qltk.ErrorMessage( None, _("Unable to create folder"), error).run() return self.emit('test-expand-row', model.get_iter(path), path) self.expand_row(path, False)
def __init__(self, browser): super(PreferencesButton, self).__init__() self._menu = menu = Gtk.Menu() wide_mode = ConfigCheckMenuItem( _("_Wide Mode"), "browsers", "pane_wide_mode", True) wide_mode.connect("toggled", self.__wide_mode_changed, browser) menu.append(wide_mode) pref_item = MenuItem(_("_Preferences"), Icons.PREFERENCES_SYSTEM) def preferences_cb(menu_item): window = Preferences(browser) window.show() pref_item.connect("activate", preferences_cb) menu.append(pref_item) menu.show_all() button = MenuButton( SymbolicIconImage(Icons.EMBLEM_SYSTEM, Gtk.IconSize.MENU), arrow=True) button.set_menu(menu) button.show() self.pack_start(button, True, True, 0)
def __create_children(self, menu, songs): self.__remove_children(menu) 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) for item in items: song_menu.append(item) song_menu.append(SeparatorMenuItem()) i = qltk.MenuItem(_(u"_Edit Bookmarks…"), Icons.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 __init__(self, album): self.album = album self._release = None self.model = ObjectStore() self.model.append_many(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) mode = Pango.EllipsizeMode cols = [ (_('Filename'), self.__name_datafunc, True, mode.MIDDLE), (_('Disc'), self.__disc_datafunc, False, mode.END), (_('Track'), self.__track_datafunc, False, mode.END), (_('Title'), self.__title_datafunc, True, mode.END), (_('Artist'), self.__artist_datafunc, True, mode.END), ] for title, func, resize, mode in cols: render = Gtk.CellRendererText() render.set_property('ellipsize', mode) 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 __init__(self, parent, default="", **kwargs): if self.is_not_unique(): return super(TextEdit, self).__init__() self.set_title(_("Edit Display")) self.set_transient_for(qltk.get_top_parent(parent)) self.set_border_width(12) self.set_default_size(420, 190) vbox = Gtk.VBox(spacing=12) close = Button(_("_Close"), Icons.WINDOW_CLOSE) close.connect('clicked', lambda *x: self.destroy()) b = Gtk.HButtonBox() b.set_layout(Gtk.ButtonBoxStyle.END) b.pack_start(close, True, True, 0) self.box = box = self.Box(default, **kwargs) vbox.pack_start(box, True, True, 0) self.use_header_bar() if not self.has_close_button(): vbox.pack_start(b, False, True, 0) self.add(vbox) self.apply = box.apply self.revert = box.revert close.grab_focus() self.get_child().show_all()
def __init__(self, browser): if self.is_not_unique(): return super(Preferences, self).__init__() self.set_border_width(12) self.set_title(_("Playlist Browser Preferences")) self.set_default_size(420, 240) self.set_transient_for(qltk.get_top_parent(browser)) box = Gtk.VBox(spacing=6) edit_frame = self.edit_display_pane(browser, _("Playlist display")) box.pack_start(edit_frame, False, True, 12) main_box = Gtk.VBox(spacing=12) close = Button(_("_Close"), Icons.WINDOW_CLOSE) close.connect('clicked', lambda *x: self.destroy()) b = Gtk.HButtonBox() b.set_layout(Gtk.ButtonBoxStyle.END) b.pack_start(close, True, True, 0) main_box.pack_start(box, True, True, 0) self.use_header_bar() if not self.has_close_button(): main_box.pack_start(b, False, True, 0) self.add(main_box) close.grab_focus() self.show_all()
def __setup_column(self, view): def tag_cdf(column, cell, model, iter, data): row = model[iter] if row: cell.set_property('text', row[0]) def desc_cdf(column, cell, model, iter, data): row = model[iter] if row: name = re.sub(':[a-z]+$', '', row[0].strip('~#')) try: t = _TAGS[name] valid = (not t.hidden and t.numeric == row[0].startswith('~#')) val = t.desc if valid else name except KeyError: val = name cell.set_property('text', util.title(val.replace('~', ' / '))) render = Gtk.CellRendererText() column = Gtk.TreeViewColumn(_("Tag expression"), render) column.set_cell_data_func(render, tag_cdf) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_expand(True) view.append_column(column) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) column = Gtk.TreeViewColumn(_("Description"), render) column.set_cell_data_func(render, desc_cdf) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_expand(True) view.append_column(column) view.set_headers_visible(True)
def _description(self, songs, box): text = [] cur_disc = songs[0]("~#disc", 1) - 1 cur_part = None cur_track = songs[0]("~#track", 1) - 1 for song in songs: track = song("~#track", 0) disc = song("~#disc", 0) part = song.get("part") if disc != cur_disc: if cur_disc: text.append("") cur_track = song("~#track", 1) - 1 cur_part = None cur_disc = disc if disc: text.append("<b>%s</b>" % (_("Disc %s") % disc)) if part != cur_part: ts = " " * bool(disc) cur_part = part if part: text.append("%s<b>%s</b>" % (ts, util.escape(part))) cur_track += 1 ts = " " * (bool(disc) + bool(part)) while cur_track < track: text.append("%s<b>%d.</b> <i>%s</i>" % ( ts, cur_track, _("Track unavailable"))) cur_track += 1 text.append("%s<b>%d.</b> %s" % ( ts, track, util.escape(song.comma("~title~version")))) l = Label() l.set_markup("\n".join(text)) l.set_ellipsize(Pango.EllipsizeMode.END) box.pack_start(Frame(_("Track List"), l), False, False, 0)
def _description(self, songs, box): text = [] cur_disc = songs[0]("~#disc", 1) - 1 cur_part = None cur_track = songs[0]("~#track", 1) - 1 for song in songs: track = song("~#track", 0) disc = song("~#disc", 0) part = song.get("part") if disc != cur_disc: if cur_disc: text.append("") cur_track = song("~#track", 1) - 1 cur_part = None cur_disc = disc if disc: text.append("%s" % (_("Disc %s") % disc)) if part != cur_part: ts = " " * bool(disc) cur_part = part if part: text.append("%s%s" % (ts, util.escape(part))) cur_track += 1 ts = " " * (bool(disc) + bool(part)) while cur_track < track: text.append("{ts}{cur: >2}. {text}".format( ts=ts, cur=cur_track, text=_("Track unavailable"))) cur_track += 1 markup = util.escape(song.comma("~title~version")) text.append("{ts}{cur: >2}. <i>{text}</i>".format( ts=ts, cur=track, text=markup)) l = Label(markup="\n".join(text), ellipsize=True) box.pack_start(Frame(_("Track List"), l), False, False, 0)
def __init__(self, library, song, lyrics=True, bookmarks=True): super(OneSong, self).__init__() vbox = Gtk.VBox(spacing=12) vbox.set_border_width(12) self._title(song, vbox) self._album(song, vbox) self._people(song, vbox) self._library(song, vbox) self._file(song, vbox) self._additional(song, vbox) sw = SW() sw.title = _("Information") sw.add_with_viewport(vbox) self.append_page(sw) if lyrics: lyrics = LyricsPane(song) lyrics.title = _("Lyrics") self.append_page(lyrics) if bookmarks: bookmarks = EditBookmarksPane(None, song) bookmarks.title = _("Bookmarks") bookmarks.set_border_width(12) self.append_page(bookmarks) connect_destroy(library, 'changed', self.__check_changed, vbox, song)
def _album(self, song, box): if "album" not in song: return text = ["<span size='x-large'><i>%s</i></span>" % util.escape(song.comma("album"))] secondary = [] if "discnumber" in song: secondary.append(_("Disc %s") % song["discnumber"]) if "discsubtitle" in song: secondary.append("<i>%s</i>" % util.escape(song.comma("discsubtitle"))) if "tracknumber" in song: secondary.append(_("Track %s") % song["tracknumber"]) if secondary: text.append(" - ".join(secondary)) if "date" in song: text.append(util.escape(song.comma("date"))) if "organization" in song or "labelid" in song: t = util.escape(song.comma("~organization~labelid")) text.append(t) if "producer" in song: text.append(_("Produced by %s") % ( util.escape(song.comma("producer")))) w = Label(markup="\n".join(text), ellipsize=True) hb = Gtk.HBox(spacing=12) hb.pack_start(w, True, True, 0) box.pack_start(Frame(tag("album"), hb), False, False, 0) cover = ReactiveCoverImage(song=song) hb.pack_start(cover, False, True, 0)
def _do_delete_files(parent, paths): dialog = DeleteDialog.for_files(parent, paths) resp = dialog.run() if resp != DeleteDialog.RESPONSE_DELETE: return window_title = _("Deleting %(current)d/%(total)d.") w = WaitLoadWindow(parent, len(paths), window_title) w.show() ok = [] failed = [] for path in paths: try: os.unlink(path) except EnvironmentError: failed.append(path) else: ok.append(path) w.step() w.destroy() if failed: ErrorMessage(parent, _("Unable to delete files"), _("Deleting one or more files failed.") ).run()
def Menu(self, songs, library, items): in_fav = False in_all = False for song in songs: if song in self.__fav_stations: in_fav = True elif song in self.__stations: in_all = True if in_fav and in_all: break iradio_items = [] button = MenuItem(_("Add to Favorites"), Icons.LIST_ADD) button.set_sensitive(in_all) connect_obj(button, 'activate', self.__add_fav, songs) iradio_items.append(button) button = MenuItem(_("Remove from Favorites"), Icons.LIST_REMOVE) button.set_sensitive(in_fav) connect_obj(button, 'activate', self.__remove_fav, songs) iradio_items.append(button) items.append(iradio_items) menu = SongsMenu(self.__librarian, songs, playlists=False, remove=True, queue=False, devices=False, items=items) return menu
def _people(self, songs, box): artists = set() performers = set() for song in songs: artists.update(song.list("artist")) performers.update(song.list("performer")) artists = sorted(artists) performers = sorted(performers) if artists: if len(artists) == 1: title = _("artist") else: title = _("artists") title = util.capitalize(title) box.pack_start(Frame(title, Label("\n".join(artists))), False, False, 0) if performers: if len(artists) == 1: title = _("performer") else: title = _("performers") title = util.capitalize(title) box.pack_start(Frame(title, Label("\n".join(performers))), False, False, 0)
def _do_delete_songs(parent, songs, librarian): dialog = DeleteDialog.for_songs(parent, songs) resp = dialog.run() if resp != DeleteDialog.RESPONSE_DELETE: return window_title = _("Deleting %(current)d/%(total)d.") w = WaitLoadWindow(parent, len(songs), window_title) w.show() ok = [] failed = [] for song in songs: filename = song("~filename") try: os.unlink(filename) except EnvironmentError: failed.append(song) else: ok.append(song) w.step() w.destroy() if failed: ErrorMessage(parent, _("Unable to delete files"), _("Deleting one or more files failed.") ).run() if ok: librarian.remove(ok)
def __init__(self, parent, app): super(AboutDialog, self).__init__() self.set_transient_for(parent) self.set_program_name(app.name) self.set_version(quodlibet.get_build_description()) self.set_authors(const.AUTHORS) self.set_artists(const.ARTISTS) self.set_logo_icon_name(app.icon_name) def chunks(l, n): return [l[i : i + n] for i in range(0, len(l), n)] is_real_player = app.player.name != "Null" format_names = sorted([t.format for t in formats.types]) fmts = ",\n".join(", ".join(c) for c in chunks(format_names, 4)) text = [] text.append(_("Supported formats: %s") % fmts) text.append("") if is_real_player: text.append(_("Audio device: %s") % app.player.name) text.append("Python: %s" % platform.python_version()) text.append("Mutagen: %s" % fver(mutagen.version)) text.append("GTK+: %s (%s)" % (fver(gtk_version), get_backend_name())) text.append("PyGObject: %s" % fver(pygobject_version)) if is_real_player: text.append(app.player.version_info) self.set_comments("\n".join(text)) self.set_license_type(Gtk.License.GPL_2_0) self.set_translator_credits("\n".join(const.TRANSLATORS)) self.set_website(const.WEBSITE) self.set_copyright(const.COPYRIGHT + "\n" + "<%s>" % const.SUPPORT_EMAIL)
def add_edit_item(cls, submenu): config = Gtk.MenuItem(label=_("Edit Custom Commands") + "…") connect_obj(config, 'activate', cls.edit_patterns, config) config.set_sensitive(not JSONBasedEditor.is_not_unique()) submenu.append(SeparatorMenuItem()) submenu.append(config)
def __init__(self): def create_behaviour_frame(): vbox = Gtk.VBox(spacing=6) jump_button = CCB(_("_Jump to playing song automatically"), 'settings', 'jump', populate=True, tooltip=_("When the playing song changes, " "scroll to it in the song list")) autosort_button = CCB( _("Sort songs when tags are modified"), 'song_list', 'auto_sort', populate=True, tooltip=_( "Automatically re-sort songs in the song list when " "tags are modified")) vbox.pack_start(jump_button, False, True, 0) vbox.pack_start(autosort_button, False, True, 0) return qltk.Frame(_("Behavior"), child=vbox) def create_visible_columns_frame(): buttons = {} vbox = Gtk.VBox(spacing=12) table = Gtk.Table.new(3, 3, True) for i, (k, t) in enumerate(self.PREDEFINED_TAGS): x, y = i % 3, i / 3 buttons[k] = Gtk.CheckButton(label=t, use_underline=True) table.attach(buttons[k], x, x + 1, y, y + 1) vbox.pack_start(table, False, True, 0) # Other columns hbox = Gtk.HBox(spacing=6) l = Gtk.Label(label=_("_Others:"), use_underline=True) hbox.pack_start(l, False, True, 0) self.others = others = UndoEntry() others.set_sensitive(False) # Stock edit doesn't have ellipsis chars. edit_button = Gtk.Button(label=_(u"_Edit…"), use_underline=True) edit_button.connect("clicked", self.__config_cols, buttons) edit_button.set_tooltip_text( _("Add or remove additional column " "headers")) l.set_mnemonic_widget(edit_button) l.set_use_underline(True) hbox.pack_start(others, True, True, 0) vbox.pack_start(hbox, False, True, 0) b = Gtk.HButtonBox() b.set_layout(Gtk.ButtonBoxStyle.END) b.pack_start(edit_button, True, True, 0) vbox.pack_start(b, True, True, 0) return qltk.Frame(_("Visible Columns"), child=vbox), buttons def create_columns_prefs_frame(): tiv = Gtk.CheckButton(label=_("Title includes _version"), use_underline=True) aio = Gtk.CheckButton(label=_("Artist includes all _people"), use_underline=True) aip = Gtk.CheckButton(label=_("Album includes _disc subtitle"), use_underline=True) fip = Gtk.CheckButton(label=_("Filename includes _folder"), use_underline=True) self._toggle_data = [(tiv, "title", "~title~version"), (aip, "album", "~album~discsubtitle"), (fip, "~basename", "~filename"), (aio, "artist", "~people")] t = Gtk.Table.new(2, 2, True) t.attach(tiv, 0, 1, 0, 1) t.attach(aip, 0, 1, 1, 2) t.attach(aio, 1, 2, 0, 1) t.attach(fip, 1, 2, 1, 2) return qltk.Frame(_("Column Preferences"), child=t) def create_apply_button(): vbox = Gtk.VBox(spacing=12) apply = Button(_("_Apply")) apply.set_tooltip_text( _("Apply current configuration to song list, " "adding new columns to the end")) apply.connect('clicked', self.__apply, buttons) # Apply on destroy, else config gets mangled self.connect('destroy', self.__apply, buttons) b = Gtk.HButtonBox() b.set_layout(Gtk.ButtonBoxStyle.END) b.pack_start(apply, True, True, 0) vbox.pack_start(b, True, True, 0) return vbox super().__init__(spacing=12) self.set_border_width(12) self.title = _("Song List") self.pack_start(create_behaviour_frame(), False, True, 0) columns_frame, buttons = create_visible_columns_frame() self.pack_start(columns_frame, False, True, 0) self.pack_start(create_columns_prefs_frame(), False, True, 0) self.pack_start(create_apply_button(), True, True, 0) self.__update(buttons, self._toggle_data, get_columns()) for child in self.get_children(): child.show_all()
def ratings_vbox(self): """Returns a new VBox containing all ratings widgets""" vb = Gtk.VBox(spacing=6) # Default Rating model = Gtk.ListStore(float) default_combo = Gtk.ComboBox(model=model) default_lab = Gtk.Label(label=_("_Default rating:")) default_lab.set_use_underline(True) default_lab.set_alignment(0, 0.5) def draw_rating(column, cell, model, it, data): num = model[it][0] text = "%0.2f: %s" % (num, util.format_rating(num)) cell.set_property('text', text) def default_rating_changed(combo, model): it = combo.get_active_iter() if it is None: return RATINGS.default = model[it][0] qltk.redraw_all_toplevels() def populate_default_rating_model(combo, num): model = combo.get_model() model.clear() deltas = [] default = RATINGS.default precision = RATINGS.precision for i in range(0, num + 1): r = i * precision model.append(row=[r]) deltas.append((abs(default - r), i)) active = sorted(deltas)[0][1] print_d("Choosing #%d (%.2f), closest to current %.2f" % (active, precision * active, default)) combo.set_active(active) cell = Gtk.CellRendererText() default_combo.pack_start(cell, True) default_combo.set_cell_data_func(cell, draw_rating, None) default_combo.connect('changed', default_rating_changed, model) default_lab.set_mnemonic_widget(default_combo) def refresh_default_combo(num): populate_default_rating_model(default_combo, num) # Rating Scale model = Gtk.ListStore(int) scale_combo = Gtk.ComboBox(model=model) scale_lab = Gtk.Label(label=_("Rating _scale:")) scale_lab.set_use_underline(True) scale_lab.set_mnemonic_widget(scale_combo) cell = Gtk.CellRendererText() scale_combo.pack_start(cell, False) num = RATINGS.number for i in [1, 2, 3, 4, 5, 6, 8, 10]: it = model.append(row=[i]) if i == num: scale_combo.set_active_iter(it) def draw_rating_scale(column, cell, model, it, data): num_stars = model[it][0] text = "%d: %s" % (num_stars, RATINGS.full_symbol * num_stars) cell.set_property('text', text) def rating_scale_changed(combo, model): it = combo.get_active_iter() if it is None: return RATINGS.number = num = model[it][0] refresh_default_combo(num) refresh_default_combo(RATINGS.number) scale_combo.set_cell_data_func(cell, draw_rating_scale, None) scale_combo.connect('changed', rating_scale_changed, model) default_align = Align(halign=Gtk.Align.START) default_align.add(default_lab) scale_align = Align(halign=Gtk.Align.START) scale_align.add(scale_lab) grid = Gtk.Grid(column_spacing=6, row_spacing=6) grid.add(scale_align) grid.add(scale_combo) grid.attach(default_align, 0, 1, 1, 1) grid.attach(default_combo, 1, 1, 1, 1) vb.pack_start(grid, False, False, 6) # Bayesian Factor bayesian_factor = config.getfloat("settings", "bayesian_rating_factor", 0.0) adj = Gtk.Adjustment.new(bayesian_factor, 0.0, 10.0, 0.5, 0.5, 0.0) bayes_spin = Gtk.SpinButton(adjustment=adj, numeric=True) bayes_spin.set_digits(1) bayes_spin.connect('changed', self.__changed_and_signal_library, 'settings', 'bayesian_rating_factor') bayes_spin.set_tooltip_text( _("Bayesian Average factor (C) for aggregated ratings.\n" "0 means a conventional average, higher values mean that " "albums with few tracks will have less extreme ratings. " "Changing this value triggers a re-calculation for all " "albums.")) bayes_label = Gtk.Label(label=_("_Bayesian averaging amount:")) bayes_label.set_use_underline(True) bayes_label.set_mnemonic_widget(bayes_spin) # Save Ratings hb = Gtk.HBox(spacing=6) hb.pack_start(bayes_label, False, True, 0) hb.pack_start(bayes_spin, False, True, 0) vb.pack_start(hb, True, True, 0) cb = CCB(_("Save ratings and play _counts in tags"), "editing", "save_to_songs", populate=True) def update_entry(widget, email_entry): email_entry.set_sensitive(widget.get_active()) vb.pack_start(cb, True, True, 0) hb = Gtk.HBox(spacing=6) lab = Gtk.Label(label=_("_Email:")) entry = UndoEntry() entry.set_tooltip_text( _("Ratings and play counts will be saved " "in tags for this email address")) entry.set_text(config.get("editing", "save_email")) entry.connect('changed', self.__changed, 'editing', 'save_email') # Disable the entry if not saving to tags cb.connect('clicked', update_entry, entry) update_entry(cb, entry) hb.pack_start(lab, False, True, 0) hb.pack_start(entry, True, True, 0) lab.set_mnemonic_widget(entry) lab.set_use_underline(True) vb.pack_start(hb, True, True, 0) return vb
class SongList(Gtk.VBox): name = "songlist" PREDEFINED_TAGS = [("~#disc", _("_Disc")), ("~#track", _("_Track")), ("grouping", _("Grou_ping")), ("artist", _("_Artist")), ("album", _("Al_bum")), ("title", util.tag("title")), ("genre", _("_Genre")), ("date", _("_Date")), ("~basename", _("_Filename")), ("~#length", _("_Length")), ("~rating", _("_Rating")), ("~#filesize", util.tag("~#filesize"))] def __init__(self): def create_behaviour_frame(): vbox = Gtk.VBox(spacing=6) jump_button = CCB(_("_Jump to playing song automatically"), 'settings', 'jump', populate=True, tooltip=_("When the playing song changes, " "scroll to it in the song list")) autosort_button = CCB( _("Sort songs when tags are modified"), 'song_list', 'auto_sort', populate=True, tooltip=_( "Automatically re-sort songs in the song list when " "tags are modified")) vbox.pack_start(jump_button, False, True, 0) vbox.pack_start(autosort_button, False, True, 0) return qltk.Frame(_("Behavior"), child=vbox) def create_visible_columns_frame(): buttons = {} vbox = Gtk.VBox(spacing=12) table = Gtk.Table.new(3, 3, True) for i, (k, t) in enumerate(self.PREDEFINED_TAGS): x, y = i % 3, i / 3 buttons[k] = Gtk.CheckButton(label=t, use_underline=True) table.attach(buttons[k], x, x + 1, y, y + 1) vbox.pack_start(table, False, True, 0) # Other columns hbox = Gtk.HBox(spacing=6) l = Gtk.Label(label=_("_Others:"), use_underline=True) hbox.pack_start(l, False, True, 0) self.others = others = UndoEntry() others.set_sensitive(False) # Stock edit doesn't have ellipsis chars. edit_button = Gtk.Button(label=_(u"_Edit…"), use_underline=True) edit_button.connect("clicked", self.__config_cols, buttons) edit_button.set_tooltip_text( _("Add or remove additional column " "headers")) l.set_mnemonic_widget(edit_button) l.set_use_underline(True) hbox.pack_start(others, True, True, 0) vbox.pack_start(hbox, False, True, 0) b = Gtk.HButtonBox() b.set_layout(Gtk.ButtonBoxStyle.END) b.pack_start(edit_button, True, True, 0) vbox.pack_start(b, True, True, 0) return qltk.Frame(_("Visible Columns"), child=vbox), buttons def create_columns_prefs_frame(): tiv = Gtk.CheckButton(label=_("Title includes _version"), use_underline=True) aio = Gtk.CheckButton(label=_("Artist includes all _people"), use_underline=True) aip = Gtk.CheckButton(label=_("Album includes _disc subtitle"), use_underline=True) fip = Gtk.CheckButton(label=_("Filename includes _folder"), use_underline=True) self._toggle_data = [(tiv, "title", "~title~version"), (aip, "album", "~album~discsubtitle"), (fip, "~basename", "~filename"), (aio, "artist", "~people")] t = Gtk.Table.new(2, 2, True) t.attach(tiv, 0, 1, 0, 1) t.attach(aip, 0, 1, 1, 2) t.attach(aio, 1, 2, 0, 1) t.attach(fip, 1, 2, 1, 2) return qltk.Frame(_("Column Preferences"), child=t) def create_apply_button(): vbox = Gtk.VBox(spacing=12) apply = Button(_("_Apply")) apply.set_tooltip_text( _("Apply current configuration to song list, " "adding new columns to the end")) apply.connect('clicked', self.__apply, buttons) # Apply on destroy, else config gets mangled self.connect('destroy', self.__apply, buttons) b = Gtk.HButtonBox() b.set_layout(Gtk.ButtonBoxStyle.END) b.pack_start(apply, True, True, 0) vbox.pack_start(b, True, True, 0) return vbox super().__init__(spacing=12) self.set_border_width(12) self.title = _("Song List") self.pack_start(create_behaviour_frame(), False, True, 0) columns_frame, buttons = create_visible_columns_frame() self.pack_start(columns_frame, False, True, 0) self.pack_start(create_columns_prefs_frame(), False, True, 0) self.pack_start(create_apply_button(), True, True, 0) self.__update(buttons, self._toggle_data, get_columns()) for child in self.get_children(): child.show_all() def __update(self, buttons, toggle_data, columns): """Updates all widgets based on the passed column list""" columns = list(columns) for key, widget in buttons.items(): widget.set_active(key in columns) if key in columns: columns.remove(key) for (check, off, on) in toggle_data: if on in columns: buttons[off].set_active(True) check.set_active(True) columns.remove(on) self.others.set_text(", ".join(columns)) self.other_cols = columns def __get_current_columns(self, buttons): """Given the current column list and the widgets states compute a new column list. """ new_headers = set() # Get the checked headers for key, name in self.PREDEFINED_TAGS: if buttons[key].get_active(): new_headers.add(key) # And the customs new_headers.update(set(self.other_cols)) on_to_off = dict((on, off) for (w, off, on) in self._toggle_data) result = [] cur_cols = get_columns() for h in cur_cols: if h in new_headers: result.append(h) else: try: alternative = on_to_off[h] if alternative in new_headers: result.append(alternative) except KeyError: pass # Add new ones on the end result.extend(new_headers - set(result)) # After this, do the substitutions for (check, off, on) in self._toggle_data: if check.get_active(): try: result[result.index(off)] = on except ValueError: pass return result def __apply(self, button, buttons): result = self.__get_current_columns(buttons) SongList.set_all_column_headers(result) def __config_cols(self, button, buttons): def __closed(widget): cols = widget.get_strings() self.__update(buttons, self._toggle_data, cols) columns = self.__get_current_columns(buttons) m = TagListEditor(_("Edit Columns"), columns) m.set_transient_for(qltk.get_top_parent(self)) m.connect('destroy', __closed) m.show()
def __init__(self): super().__init__(spacing=12) self.set_border_width(12) self.title = _("Playback") # player backend if app.player and hasattr(app.player, 'PlayerPreferences'): player_prefs = app.player.PlayerPreferences() f = qltk.Frame(_("Output Configuration"), child=player_prefs) self.pack_start(f, False, True, 0) # replaygain fallback_gain = config.getfloat("player", "fallback_gain", 0.0) adj = Gtk.Adjustment.new(fallback_gain, -12.0, 12.0, 0.5, 0.5, 0.0) fb_spin = Gtk.SpinButton(adjustment=adj) fb_spin.set_digits(1) fb_spin.connect('changed', self.__changed, 'player', 'fallback_gain') fb_spin.set_tooltip_text( _("If no Replay Gain information is available " "for a song, scale the volume by this value")) fb_label = Gtk.Label(label=_("_Fall-back gain (dB):")) fb_label.set_use_underline(True) fb_label.set_mnemonic_widget(fb_spin) pre_amp_gain = config.getfloat("player", "pre_amp_gain", 0.0) adj = Gtk.Adjustment.new(pre_amp_gain, -12, 12, 0.5, 0.5, 0.0) adj.connect('value-changed', self.__changed, 'player', 'pre_amp_gain') pre_spin = Gtk.SpinButton(adjustment=adj) pre_spin.set_digits(1) pre_spin.set_tooltip_text( _("Scale volume for all songs by this value, " "as long as the result will not clip")) pre_label = Gtk.Label(label=_("_Pre-amp gain (dB):")) pre_label.set_use_underline(True) pre_label.set_mnemonic_widget(pre_spin) widgets = [pre_label, pre_spin, fb_label, fb_spin] c = CCB(_("_Enable Replay Gain volume adjustment"), "player", "replaygain", populate=True) c.connect('toggled', self.__toggled_gain, widgets) # packing table = Gtk.Table.new(3, 2, False) table.set_col_spacings(6) table.set_row_spacings(6) table.attach(c, 0, 2, 0, 1) fb_label.set_alignment(0, 0.5) table.attach(fb_label, 0, 1, 1, 2, xoptions=Gtk.AttachOptions.FILL) pre_label.set_alignment(0, 0.5) table.attach(pre_label, 0, 1, 2, 3, xoptions=Gtk.AttachOptions.FILL) fb_align = Align(halign=Gtk.Align.START) fb_align.add(fb_spin) table.attach(fb_align, 1, 2, 1, 2) pre_align = Align(halign=Gtk.Align.START) pre_align.add(pre_spin) table.attach(pre_align, 1, 2, 2, 3) f = qltk.Frame(_("Replay Gain Volume Adjustment"), child=table) c.emit('toggled') self.pack_start(f, False, True, 0) vbox = Gtk.VBox() c = CCB(_("_Continue playback on startup"), "player", "restore_playing", populate=True, tooltip=_("If music is playing on shutdown, automatically " "start playing on next startup")) vbox.pack_start(c, False, False, 0) f = qltk.Frame(_("Startup"), child=vbox) self.pack_start(f, False, True, 0) for child in self.get_children(): child.show_all()
def __init__(self): def create_display_frame(): vbox = Gtk.VBox(spacing=6) model = Gtk.ListStore(str, str) def on_changed(combo): it = combo.get_active_iter() if it is None: return DURATION.format = model[it][0] app.window.songlist.info.refresh() app.window.qexpander.refresh() # TODO: refresh info windows ideally too (but see #2019) def draw_duration(column, cell, model, it, data): df, example = model[it] cell.set_property('text', example) for df in sorted(DurationFormat.values): # 4954s == longest ever CD, FWIW model.append([df, format_time_preferred(4954, df)]) duration = Gtk.ComboBox(model=model) cell = Gtk.CellRendererText() duration.pack_start(cell, True) duration.set_cell_data_func(cell, draw_duration, None) index = sorted(DurationFormat.values).index(DURATION.format) duration.set_active(index) duration.connect('changed', on_changed) hbox = Gtk.HBox(spacing=6) label = Gtk.Label(label=_("Duration totals") + ":", use_underline=True) label.set_mnemonic_widget(duration) hbox.pack_start(label, False, True, 0) hbox.pack_start(duration, False, True, 0) vbox.pack_start(hbox, False, True, 0) return qltk.Frame(_("Display"), child=vbox) def create_search_frame(): vb = Gtk.VBox(spacing=6) hb = Gtk.HBox(spacing=6) l = Gtk.Label(label=_("_Global filter:")) l.set_use_underline(True) e = ValidatingEntry(Query.validator) e.set_text(config.get("browsers", "background")) e.connect('changed', self._entry, 'background', 'browsers') e.set_tooltip_text( _("Apply this query in addition to all others")) l.set_mnemonic_widget(e) hb.pack_start(l, False, True, 0) hb.pack_start(e, True, True, 0) vb.pack_start(hb, False, True, 0) # Translators: The heading of the preference group, no action return qltk.Frame(C_("heading", "Search"), child=vb) super().__init__(spacing=12) self.set_border_width(12) self.title = _("Browsers") self.pack_start(create_search_frame(), False, True, 0) self.pack_start(create_display_frame(), False, True, 0) # Ratings vb = Gtk.VBox(spacing=6) c1 = CCB(_("Confirm _multiple ratings"), 'browsers', 'rating_confirm_multiple', populate=True, tooltip=_("Ask for confirmation before changing the " "rating of multiple songs at once")) c2 = CCB(_("Enable _one-click ratings"), 'browsers', 'rating_click', populate=True, tooltip=_("Enable rating by clicking on the rating " "column in the song list")) vbox = Gtk.VBox(spacing=6) vbox.pack_start(c1, False, True, 0) vbox.pack_start(c2, False, True, 0) f = qltk.Frame(_("Ratings"), child=vbox) self.pack_start(f, False, True, 0) vb = Gtk.VBox(spacing=6) # Filename choice algorithm config cb = CCB(_("Prefer _embedded art"), 'albumart', 'prefer_embedded', populate=True, tooltip=_("Choose to use artwork embedded in the audio " "(where available) over other sources")) vb.pack_start(cb, False, True, 0) hb = Gtk.HBox(spacing=3) preferred_image_filename_tooltip = _( "The album art image file(s) to use when available " "(supports wildcards). If you want to supply more " "than one, separate them with commas.") cb = CCB(_("_Preferred image filename(s):"), 'albumart', 'force_filename', populate=True, tooltip=preferred_image_filename_tooltip) hb.pack_start(cb, False, True, 0) entry = UndoEntry() entry.set_tooltip_text(preferred_image_filename_tooltip) entry.set_text(config.get("albumart", "filename")) entry.connect('changed', self.__changed_text, 'filename') # Disable entry when not forcing entry.set_sensitive(cb.get_active()) cb.connect('toggled', self.__toggled_force_filename, entry) hb.pack_start(entry, True, True, 0) vb.pack_start(hb, False, True, 0) f = qltk.Frame(_("Album Art"), child=vb) self.pack_start(f, False, True, 0) for child in self.get_children(): child.show_all()
class Command(JSONObject): """ Wraps an arbitrary shell command and its argument pattern. Serialises as JSON for some editability """ NAME = _("Command") FIELDS = { "name": Field(_("name"), _("The name of this command")), "command": Field(_("command"), _("The shell command syntax to run")), "parameter": Field( _("parameter"), _("If specified, a parameter whose occurrences in " "the command will be substituted with a " "user-supplied value, e.g. by using 'PARAM' " "all instances of '{PARAM}' in your command will " "have the value prompted for when run")), "pattern": Field( _("pattern"), _("The QL pattern, e.g. <~filename>, to use to " "compute a value for the command. For playlists, " "this also supports virtual tags <~playlistname> " "and <~#playlistindex>.")), "unique": Field( _("unique"), _("If set, this will remove duplicate computed values " "of the pattern")), "max_args": Field( _("max args"), _("The maximum number of argument to pass to the " "command at one time (like xargs)")), } def __init__(self, name=None, command=None, pattern="<~filename>", unique=False, parameter=None, max_args=10000, warn_threshold=50): JSONObject.__init__(self, name) self.command = str(command or "") self.pattern = str(pattern) self.unique = bool(unique) self.max_args = max_args self.parameter = str(parameter or "") self.__pat = Pattern(self.pattern) self.warn_threshold = warn_threshold def run(self, songs, playlist_name=None): """ Runs this command on `songs`, splitting into multiple calls if necessary. `playlist_name` if populated contains the Playlist's name. """ args = [] template_vars = {} if self.parameter: value = GetStringDialog(None, _("Input value"), _("Value for %s?") % self.parameter).run() template_vars[self.parameter] = value if playlist_name: print_d("Playlist command for %s" % playlist_name) template_vars["PLAYLIST"] = playlist_name self.command = self.command.format(**template_vars) print_d("Actual command=%s" % self.command) for i, song in enumerate(songs): wrapped = SongWrapper(song) if playlist_name: wrapped["~playlistname"] = playlist_name wrapped["~playlistindex"] = str(i + 1) wrapped["~#playlistindex"] = i + 1 arg = str(self.__pat.format(wrapped)) 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:] @property def playlists_only(self): return ("~playlistname" in self.pattern or "playlistindex" in self.pattern) def __str__(self): return 'Command: "{command} {pattern}"'.format(**dict(self.data))
class AutoLibraryUpdate(EventPlugin): PLUGIN_ID = "Automatic library update" PLUGIN_NAME = _("Automatic Library Update") PLUGIN_DESC = _("Keeps your library up to date with inotify. " "Requires %s.") % "pyinotify" PLUGIN_ICON = Icons.VIEW_REFRESH # 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(library=app.library) FLAGS = ['IN_DELETE', 'IN_CLOSE_WRITE', # 'IN_MODIFY', 'IN_MOVED_FROM', 'IN_MOVED_TO', 'IN_CREATE'] masks = [EventsCodes.FLAG_COLLECTIONS['OP_FLAGS'][s] for s in FLAGS] mask = reduce(operator.or_, masks, 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 get_scan_dirs(): real_path = os.path.realpath(path) print_d('Watching directory %s for %s (mask: %x)' % (real_path, FLAGS, mask)) # See https://github.com/seb-m/pyinotify/wiki/ # Frequently-Asked-Questions wm.add_watch(real_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()
def __init__(self, parent, plugin_instance): GObject.GObject.__init__(self, spacing=12) self.plugin_instance = plugin_instance # notification text settings table = Gtk.Table(n_rows=2, n_columns=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(pconfig.gettext("titlepattern")) def on_entry_changed(entry, cfgname): pconfig.settext(cfgname, gdecode(entry.get_text())) title_entry.connect("changed", on_entry_changed, "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_icon_name( Icons.DOCUMENT_REVERT, Gtk.IconSize.MENU)) title_revert.set_tooltip_text(_("Revert to default pattern")) title_revert.connect( "clicked", lambda *x: title_entry.set_text( pconfig.defaults.gettext("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(pconfig.gettext("bodypattern")) def on_textbuffer_changed(text_buffer, cfgname): start, end = text_buffer.get_bounds() text = gdecode(text_buffer.get_text(start, end, True)) pconfig.settext(cfgname, text) body_textbuffer.connect("changed", on_textbuffer_changed, "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) body_revert = Gtk.Button() body_revert.add(Gtk.Image.new_from_icon_name( Icons.DOCUMENT_REVERT, Gtk.IconSize.MENU)) body_revert.set_tooltip_text(_("Revert to default pattern")) body_revert.connect("clicked", lambda *x: body_textbuffer.set_text(pconfig.defaults.gettext("bodypattern"))) table.attach( body_revert, 2, 3, 1, 2, xoptions=Gtk.AttachOptions.SHRINK, yoptions=Gtk.AttachOptions.FILL | Gtk.AttachOptions.SHRINK) # preview button preview_button = qltk.Button( _("_Show notification"), Icons.SYSTEM_RUN) 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) { "user": only_user_radio, "auto": only_auto_radio, "all": all_radio }.get(pconfig.gettext("show_notifications"), all_radio).set_active(True) focus_check = Gtk.CheckButton( label=_("Only when the main window is not _focused"), use_underline=True) focus_check.set_active(pconfig.getboolean("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) show_next = Gtk.CheckButton( label=_("Show \"_Next\" button"), use_underline=True) show_next.set_active(pconfig.getboolean("show_next_button")) show_next.connect("toggled", self.on_checkbutton_toggled, "show_next_button") display_box.pack_start(show_next, True, True, 0) self.pack_start(display_frame, True, True, 0) self.show_all() self.connect("destroy", self.on_destroyed)
class CustomCommands(PlaylistPlugin, SongsMenuPlugin, PluginConfigMixin): PLUGIN_ICON = Icons.APPLICATION_UTILITIES PLUGIN_ID = "CustomCommands" PLUGIN_NAME = _("Custom Commands") PLUGIN_DESC = _("Runs custom commands (in batches if required) on songs " "using any of their tags.") # Here are some starters... DEFAULT_COMS = [ Command("Compress files", "file-roller -d"), Command("Browse folders (Thunar)", "thunar", "<~dirname>", unique=True, max_args=50, warn_threshold=20), Command(name="Flash notification", command="notify-send" " -t 2000" " -i " "/usr/share/icons/hicolor/scalable/apps/" "io.github.quodlibet.QuodLibet.svg", pattern="<~rating> \"<title><version| (<version>)>\"" "<~people| by <~people>>" "<album|, from <album><discnumber| : disk <discnumber>>" "<~length| (<~length>)>", max_args=1, warn_threshold=10), Command(name="Output playlist to stdout", command="echo -e", pattern="<~playlistname>: <~playlistindex>. " " <~artist~title>\\\\n", warn_threshold=20), Command("Fix MP3 VBR with mp3val", "mp3val -f", unique=True, max_args=1), ] COMS_FILE = os.path.join(quodlibet.get_user_dir(), 'lists', 'customcommands.json') _commands = None """Commands known to the class""" def __set_pat(self, name): self.com_index = name def get_data(self, key): """Gets the pattern for a given key""" try: return self.all_commands()[key] except (KeyError, TypeError): print_d("Invalid key %s" % key) return None @classmethod def edit_patterns(cls, button): win = JSONBasedEditor(Command, cls.all_commands(), filename=cls.COMS_FILE, title=_("Edit Custom Commands")) # Cache busting cls._commands = None win.show() @classmethod def PluginPreferences(cls, parent): hb = Gtk.HBox(spacing=3) hb.set_border_width(0) button = qltk.Button(_("Edit Custom Commands") + "…", Icons.EDIT) button.set_tooltip_markup( _("Supports QL patterns\neg " "<tt><~artist~title></tt>")) button.connect("clicked", cls.edit_patterns) hb.pack_start(button, True, True, 0) hb.show_all() return hb @classmethod def all_commands(cls): if cls._commands is None: cls._commands = cls._get_saved_commands() return cls._commands @classmethod def _get_saved_commands(cls): filename = cls.COMS_FILE print_d("Loading saved commands from '%s'..." % filename) coms = None try: with open(filename, "r", encoding="utf-8") as f: coms = JSONObjectDict.from_json(Command, f.read()) except (IOError, ValueError) as e: print_w("Couldn't parse saved commands (%s)" % e) # Failing all else... if not coms: print_d("No commands found in %s. Using defaults." % filename) coms = {c.name: c for c in cls.DEFAULT_COMS} print_d("Loaded commands: %s" % coms.keys()) return coms def __init__(self, *args, **kwargs): super(CustomCommands, self).__init__(**kwargs) pl_mode = hasattr(self, '_playlists') and bool(len(self._playlists)) self.com_index = None self.unique_only = False submenu = Gtk.Menu() for name, c in self.all_commands().items(): item = Gtk.MenuItem(label=name) connect_obj(item, 'activate', self.__set_pat, name) if pl_mode and not c.playlists_only: continue item.set_sensitive(c.playlists_only == pl_mode) submenu.append(item) self.add_edit_item(submenu) if submenu.get_children(): self.set_submenu(submenu) else: self.set_sensitive(False) @classmethod def add_edit_item(cls, submenu): config = Gtk.MenuItem(label=_("Edit Custom Commands") + "…") connect_obj(config, 'activate', cls.edit_patterns, config) config.set_sensitive(not JSONBasedEditor.is_not_unique()) submenu.append(SeparatorMenuItem()) submenu.append(config) def plugin_songs(self, songs): self._handle_songs(songs) def plugin_playlist(self, playlist): print_d("Running playlist plugin for %s" % playlist) return self._handle_songs(playlist.songs, playlist) def _handle_songs(self, songs, playlist=None): # Check this is a launch, not a configure if self.com_index: com = self.get_data(self.com_index) if len(songs) > com.warn_threshold: if not confirm_multi_song_invoke(self, com.name, len(songs)): print_d("User decided not to run on %d songs" % len(songs)) return print_d("Running %s on %d song(s)" % (com, len(songs))) try: com.run(songs, playlist and playlist.name) except Exception as err: print_e("Couldn't run command %s: %s %s at:" % ( com.name, type(err), err, )) print_exc() ErrorMessage( self.plugin_window, _("Unable to run custom command %s") % util.escape(self.com_index), util.escape(str(err))).run()
class Notify(EventPlugin): PLUGIN_ID = "Notify" PLUGIN_NAME = _("Song Notifications") PLUGIN_DESC = _("Displays a notification when the song changes.") PLUGIN_ICON = Icons.DIALOG_INFORMATION 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 __watch = 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._set_image_fileobj(None) def __enable_watch(self): """Enable events for dbus name owner change""" try: 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) except dbus.DBusException: pass 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 _set_image_fileobj(self, fileobj): if self.__image_fp is not None: self.__image_fp.close() self.__image_fp = None self.__image_fp = fileobj def _get_image_uri(self, song): """A unicode file URI or an empty string""" fileobj = app.cover_manager.get_cover(song) self._set_image_fileobj(fileobj) if fileobj: return fsn2uri(fileobj.name) return u"" 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(pconfig.gettext("titlepattern")) % song title = unescape(strip_markup(strip_links(strip_images(title)))) body = "" if "body" in caps: body = XMLFromPattern(pconfig.gettext("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) actions = [] if pconfig.getboolean("show_next_button") and "actions" in caps: actions = ["next", _("Next")] hints = { "desktop-entry": "quodlibet", } image_uri = self._get_image_uri(song) if image_uri: hints["image_path"] = image_uri hints["image-path"] = image_uri try: self.__last_id = iface.Notify( "Quod Libet", self.__last_id, image_uri, title, body, actions, hints, pconfig.getint("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 pconfig.gettext("show_notifications") in [typ, "all"] \ and not (pconfig.getboolean("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
class SessionInhibit(EventPlugin): PLUGIN_ID = "screensaver_inhibit" PLUGIN_NAME = _("Inhibit Screensaver/Suspend") PLUGIN_DESC = _("On a GNOME desktop, when a song is playing, prevents" " either the screensaver from activating, or prevents the" " computer from suspending.") PLUGIN_ICON = Icons.PREFERENCES_DESKTOP_SCREENSAVER CONFIG_MODE = PLUGIN_ID + "_mode" 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 __get_dbus_proxy(self): bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) return Gio.DBusProxy.new_sync(bus, Gio.DBusProxyFlags.NONE, None, self.DBUS_NAME, self.DBUS_PATH, self.DBUS_INTERFACE, 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 = get_toplevel_xid() mode = config.get("plugins", self.CONFIG_MODE, InhibitStrings.IDLE) flags = InhibitFlags.SUSPEND if mode == InhibitStrings.SUSPEND \ else InhibitFlags.IDLE try: dbus_proxy = self.__get_dbus_proxy() self.__cookie = dbus_proxy.Inhibit('(susu)', self.APPLICATION_ID, xid, self.INHIBIT_REASON, flags) except GLib.Error: pass def plugin_on_paused(self): if self.__cookie is None: return try: dbus_proxy = self.__get_dbus_proxy() dbus_proxy.Uninhibit('(u)', self.__cookie) self.__cookie = None except GLib.Error: pass def PluginPreferences(self, parent): def changed(combo): index = combo.get_active() mode = InhibitStrings.SUSPEND if index == 1 \ else InhibitStrings.IDLE config.set("plugins", self.CONFIG_MODE, mode) if not app.player.paused: self.plugin_on_paused() self.plugin_on_unpaused() mode = config.get("plugins", self.CONFIG_MODE, InhibitStrings.IDLE) hb = Gtk.HBox(spacing=6) hb.set_border_width(6) # Translators: Inhibiting Mode hb.pack_start(Gtk.Label(label=_("Mode:")), False, True, 0) combo = Gtk.ComboBoxText() combo.append_text(_("Inhibit Screensaver")) combo.append_text(_("Inhibit Suspend")) combo.set_active(1 if mode == InhibitStrings.SUSPEND else 0) combo.connect('changed', changed) hb.pack_start(combo, True, True, 0) return hb
class Import(SongsMenuPlugin): PLUGIN_ID = "ImportMeta" PLUGIN_NAME = _("Import Metadata") PLUGIN_DESC = _("Imports metadata for selected songs from a .tags file.") PLUGIN_ICON = Icons.DOCUMENT_OPEN REQUIRES_ACTION = True plugin_handles = each_song(is_writable, is_a_file) # 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(key=sort_key_for) 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, 'r', encoding="utf-8"): if index == len(metadata): names.append(line[:line.rfind('.')]) metadata.append({}) elif line == '\n': index = len(metadata) else: key, value = line[:-1].split('=', 1) 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 self.update_files(songs, metadata, names, append=append, rename=rename) def update_files(self, songs, metadata, names, append=True, rename=False): for song, meta, name in zip(songs, metadata, names): for key, values in meta.items(): if append and key in song: values = song.list(key) + values song[key] = '\n'.join(values) if rename: path = song('~dirname') base = os.path.basename(name) newname = os.path.join(path, base) try: app.library.rename(song._song, newname) except ValueError: print_e("File {} already exists. Ignoring file " "rename.".format(newname)) app.library.changed(songs)
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(pconfig.gettext("titlepattern")) % song title = unescape(strip_markup(strip_links(strip_images(title)))) body = "" if "body" in caps: body = XMLFromPattern(pconfig.gettext("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) actions = [] if pconfig.getboolean("show_next_button") and "actions" in caps: actions = ["next", _("Next")] hints = { "desktop-entry": "quodlibet", } image_uri = self._get_image_uri(song) if image_uri: hints["image_path"] = image_uri hints["image-path"] = image_uri try: self.__last_id = iface.Notify( "Quod Libet", self.__last_id, image_uri, title, body, actions, hints, pconfig.getint("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 ftime(t): if t == 0: return _("Unknown") else: return str(time.strftime("%c", time.localtime(t)))
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 __init__(self): super(NoSongs, self).__init__(label=_("No songs are selected.")) self.title = _("No Songs")
class AlbumList(Browser, util.InstanceTracker, VisibleUpdate, DisplayPatternMixin): __model = None __last_render = None __last_render_surface = None _PATTERN_FN = os.path.join(quodlibet.get_user_dir(), "album_pattern") _DEFAULT_PATTERN_TEXT = DEFAULT_PATTERN_TEXT STAR = ["~people", "album"] name = _("Album List") accelerated_name = _("_Album List") keys = ["AlbumList"] priority = 4 def pack(self, songpane): container = qltk.ConfigRHPaned("browsers", "albumlist_pos", 0.4) container.pack1(self, True, False) container.pack2(songpane, True, False) return container def unpack(self, container, songpane): container.remove(songpane) container.remove(self) @classmethod def init(klass, library): super(AlbumList, klass).load_pattern() @classmethod def _destroy_model(klass): klass.__model.destroy() klass.__model = None @classmethod def toggle_covers(klass): on = config.getboolean("browsers", "album_covers") for albumlist in klass.instances(): albumlist.__cover_column.set_visible(on) for column in albumlist.view.get_columns(): column.queue_resize() @classmethod def refresh_all(cls): cls.__model.refresh_all() @classmethod def _init_model(klass, library): klass.__model = AlbumModel(library) klass.__library = library @util.cached_property def _no_cover(self): """Returns a cairo surface representing a missing cover""" cover_size = get_cover_size() scale_factor = self.get_scale_factor() pb = get_no_cover_pixbuf(cover_size, cover_size, scale_factor) return get_surface_for_pixbuf(self, pb) def __init__(self, library): super(AlbumList, self).__init__(spacing=6) self.set_orientation(Gtk.Orientation.VERTICAL) self._register_instance() if self.__model is None: self._init_model(library) self._cover_cancel = Gio.Cancellable() sw = ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) self.view = view = AllTreeView() view.set_headers_visible(False) model_sort = AlbumSortModel(model=self.__model) model_filter = AlbumFilterModel(child_model=model_sort) self.__bg_filter = background_filter() self.__filter = None model_filter.set_visible_func(self.__parse_query) render = Gtk.CellRendererPixbuf() self.__cover_column = column = Gtk.TreeViewColumn("covers", render) column.set_visible(config.getboolean("browsers", "album_covers")) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) column.set_fixed_width(get_cover_size() + 12) render.set_property('height', get_cover_size() + 8) render.set_property('width', get_cover_size() + 8) def cell_data_pb(column, cell, model, iter_, no_cover): item = model.get_value(iter_) if item.album is None: surface = None elif item.cover: pixbuf = item.cover pixbuf = add_border_widget(pixbuf, self.view) surface = get_surface_for_pixbuf(self, pixbuf) # don't cache, too much state has an effect on the result self.__last_render_surface = None else: surface = no_cover if self.__last_render_surface == surface: return self.__last_render_surface = surface cell.set_property("surface", surface) column.set_cell_data_func(render, cell_data_pb, self._no_cover) view.append_column(column) render = Gtk.CellRendererText() column = Gtk.TreeViewColumn("albums", render) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) if view.supports_hints(): render.set_property('ellipsize', Pango.EllipsizeMode.END) def cell_data(column, cell, model, iter_, data): album = model.get_album(iter_) if album is None: text = "<b>%s</b>\n" % _("All Albums") text += numeric_phrase("%d album", "%d albums", len(model) - 1) markup = text else: markup = self.display_pattern % album if self.__last_render == markup: return self.__last_render = markup cell.markup = markup cell.set_property('markup', markup) column.set_cell_data_func(render, cell_data) view.append_column(column) view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) view.set_rules_hint(True) view.set_search_equal_func(self.__search_func, None) view.set_search_column(0) view.set_model(model_filter) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(view) view.connect('row-activated', self.__play_selection) self.__sig = view.connect('selection-changed', util.DeferredSignal(self.__update_songs, owner=view)) targets = [("text/x-quodlibet-songs", Gtk.TargetFlags.SAME_APP, 1), ("text/uri-list", 0, 2)] targets = [Gtk.TargetEntry.new(*t) for t in targets] view.drag_source_set( Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.COPY) view.connect("drag-data-get", self.__drag_data_get) connect_obj(view, 'popup-menu', self.__popup, view, library) self.accelerators = Gtk.AccelGroup() search = SearchBarBox(completion=AlbumTagCompletion(), accel_group=self.accelerators, star=self.STAR) search.connect('query-changed', self.__update_filter) connect_obj(search, 'focus-out', lambda w: w.grab_focus(), view) self.__search = search prefs = PreferencesButton(self, model_sort) search.pack_start(prefs, False, True, 0) self.pack_start(Align(search, left=6, top=6), False, True, 0) self.pack_start(sw, True, True, 0) self.connect("destroy", self.__destroy) self.enable_row_update(view, sw, self.__cover_column) self.connect('key-press-event', self.__key_pressed, library.librarian) if app.cover_manager: connect_destroy( app.cover_manager, "cover-changed", self._cover_changed) self.show_all() def _cover_changed(self, manager, songs): model = self.__model songs = set(songs) for iter_, item in model.iterrows(): album = item.album if album is not None and songs & album.songs: item.scanned = False model.row_changed(model.get_path(iter_), iter_) def __key_pressed(self, widget, event, librarian): if qltk.is_accel(event, "<Primary>I"): songs = self.__get_selected_songs() if songs: window = Information(librarian, songs, self) window.show() return True elif qltk.is_accel(event, "<alt>Return"): songs = self.__get_selected_songs() if songs: window = SongProperties(librarian, songs, self) window.show() return True return False def _row_needs_update(self, model, iter_): item = model.get_value(iter_) return item.album is not None and not item.scanned def _update_row(self, filter_model, iter_): sort_model = filter_model.get_model() model = sort_model.get_model() iter_ = filter_model.convert_iter_to_child_iter(iter_) iter_ = sort_model.convert_iter_to_child_iter(iter_) tref = Gtk.TreeRowReference.new(model, model.get_path(iter_)) def callback(): path = tref.get_path() if path is not None: model.row_changed(path, model.get_iter(path)) item = model.get_value(iter_) scale_factor = self.get_scale_factor() item.scan_cover(scale_factor=scale_factor, callback=callback, cancel=self._cover_cancel) def __destroy(self, browser): self._cover_cancel.cancel() self.disable_row_update() self.view.set_model(None) klass = type(browser) if not klass.instances(): klass._destroy_model() def __update_filter(self, entry, text, scroll_up=True, restore=False): model = self.view.get_model() self.__filter = None query = self.__search.query if not query.matches_all: self.__filter = query.search self.__bg_filter = background_filter() self.__inhibit() # We could be smart and try to scroll to a selected album # but that introduces lots of wild scrolling. Feel free to change it. # Without scrolling the TV tries to stay at the same position # (40% down) which makes no sense, so always go to the top. if scroll_up: self.view.scroll_to_point(0, 0) # Don't filter on restore if there is nothing to filter if not restore or self.__filter or self.__bg_filter: model.refilter() self.__uninhibit() def __parse_query(self, model, iter_, data): f, b = self.__filter, self.__bg_filter if f is None and b is None: return True else: album = model.get_album(iter_) if album is None: return True elif b is None: return f(album) elif f is None: return b(album) else: return b(album) and f(album) def __search_func(self, model, column, key, iter_, data): album = model.get_album(iter_) if album is None: return True key = gdecode(key).lower() title = album.title.lower() if key in title: return False if config.getboolean("browsers", "album_substrings"): people = (p.lower() for p in album.list("~people")) for person in people: if key in person: return False return True def __popup(self, view, library): albums = self.__get_selected_albums() songs = self.__get_songs_from_albums(albums) items = [] if self.__cover_column.get_visible(): num = len(albums) button = MenuItem( ngettext("Reload album _cover", "Reload album _covers", num), Icons.VIEW_REFRESH) button.connect('activate', self.__refresh_album, view) items.append(button) menu = SongsMenu(library, songs, items=[items]) menu.show_all() return view.popup_menu(menu, 0, Gtk.get_current_event_time()) def __refresh_album(self, menuitem, view): items = self.__get_selected_items() for item in items: item.scanned = False model = self.view.get_model() for iter_, item in model.iterrows(): if item in items: model.row_changed(model.get_path(iter_), iter_) def __get_selected_items(self): selection = self.view.get_selection() model, paths = selection.get_selected_rows() return model.get_items(paths) def __get_selected_albums(self): selection = self.view.get_selection() model, paths = selection.get_selected_rows() return model.get_albums(paths) def __get_songs_from_albums(self, albums, sort=True): # Sort first by how the albums appear in the model itself, # then within the album using the default order. songs = [] if sort: for album in albums: songs.extend(sorted(album.songs, key=lambda s: s.sort_key)) else: for album in albums: songs.extend(album.songs) return songs def __get_selected_songs(self, sort=True): albums = self.__get_selected_albums() return self.__get_songs_from_albums(albums, sort) def __drag_data_get(self, view, ctx, sel, tid, etime): songs = self.__get_selected_songs() if tid == 1: qltk.selection_set_songs(sel, songs) else: sel.set_uris([song("~uri") for song in songs]) def __play_selection(self, view, indices, col): self.songs_activated() def active_filter(self, song): for album in self.__get_selected_albums(): if song in album.songs: return True return False def can_filter_text(self): return True def filter_text(self, text): self.__search.set_text(text) if Query(text).is_parsable: self.__update_filter(self.__search, text) self.__inhibit() self.view.set_cursor((0,)) self.__uninhibit() self.activate() def get_filter_text(self): return self.__search.get_text() def can_filter(self, key): # Numerics are different for collections, and although title works, # it's not of much use here. if key is not None and (key.startswith("~#") or key == "title"): return False return super(AlbumList, self).can_filter(key) def can_filter_albums(self): return True def list_albums(self): model = self.view.get_model() return [row[0].album.key for row in model if row[0].album] def filter_albums(self, values): view = self.view self.__inhibit() changed = view.select_by_func( lambda r: r[0].album and r[0].album.key in values) self.__uninhibit() if changed: self.activate() def unfilter(self): self.filter_text("") self.view.set_cursor((0,)) def activate(self): self.view.get_selection().emit('changed') def __inhibit(self): self.view.handler_block(self.__sig) def __uninhibit(self): self.view.handler_unblock(self.__sig) def restore(self): text = config.gettext("browsers", "query_text") entry = self.__search entry.set_text(text) # update_filter expects a parsable query if Query(text).is_parsable: self.__update_filter(entry, text, scroll_up=False, restore=True) keys = config.gettext("browsers", "albums").split("\n") # FIXME: If albums is "" then it could be either all albums or # no albums. If it's "" and some other stuff, assume no albums, # otherwise all albums. self.__inhibit() if keys == [""]: self.view.set_cursor((0,)) else: def select_fun(row): album = row[0].album if not album: # all return False return album.str_key in keys self.view.select_by_func(select_fun) self.__uninhibit() def scroll(self, song): album_key = song.album_key select = lambda r: r[0].album and r[0].album.key == album_key self.view.select_by_func(select, one=True) def __get_config_string(self): selection = self.view.get_selection() model, paths = selection.get_selected_rows() # All is selected if model.contains_all(paths): return "" # All selected albums albums = model.get_albums(paths) confval = "\n".join((a.str_key for a in albums)) # ConfigParser strips a trailing \n so we move it to the front if confval and confval[-1] == "\n": confval = "\n" + confval[:-1] return confval def save(self): conf = self.__get_config_string() config.settext("browsers", "albums", conf) text = self.__search.get_text() config.settext("browsers", "query_text", text) def __update_songs(self, view, selection): songs = self.__get_selected_songs(sort=False) self.songs_selected(songs)
def __init__(self, title, values=None): super(TagListEditor, self).__init__() self.use_header_bar() self.data = values or [] self.set_border_width(12) self.set_title(title) self.set_default_size(self._WIDTH, self._HEIGHT) vbox = Gtk.VBox(spacing=12) hbox = Gtk.HBox(spacing=12) # Set up the model for this widget self.model = Gtk.ListStore(str) self.__fill_values() # Main view view = self.view = HintedTreeView(model=self.model) view.set_fixed_height_mode(True) view.set_headers_visible(False) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.set_shadow_type(Gtk.ShadowType.IN) sw.add(view) sw.set_size_request(-1, max(sw.size_request().height, 100)) hbox.pack_start(sw, True, True, 0) self.__setup_column(view) # Context menu menu = Gtk.Menu() remove_item = MenuItem(_("_Remove"), Icons.LIST_REMOVE) menu.append(remove_item) menu.show_all() view.connect('popup-menu', self.__popup, menu) connect_obj(remove_item, 'activate', self.__remove, view) # Add and Remove buttons vbbox = Gtk.VButtonBox() vbbox.set_layout(Gtk.ButtonBoxStyle.START) vbbox.set_spacing(6) add = Button(_("_Add"), Icons.LIST_ADD) add.connect("clicked", self.__add) vbbox.pack_start(add, False, True, 0) remove = Button(_("_Remove"), Icons.LIST_REMOVE) remove.connect("clicked", self.__remove) vbbox.pack_start(remove, False, True, 0) hbox.pack_start(vbbox, False, True, 0) vbox.pack_start(hbox, True, True, 0) # Close buttons bbox = Gtk.HButtonBox() self.remove_but = Button(_("_Remove"), Icons.LIST_REMOVE) self.remove_but.set_sensitive(False) close = Button(_("_Close"), Icons.WINDOW_CLOSE) connect_obj(close, 'clicked', qltk.Window.destroy, self) bbox.set_layout(Gtk.ButtonBoxStyle.END) if not self.has_close_button(): bbox.pack_start(close, True, True, 0) vbox.pack_start(bbox, False, True, 0) # Finish up self.add(vbox) self.get_child().show_all()
def counter(i): return _("Never") if i == 0 \ else numeric_phrase("%(n)d time", "%(n)d times", i, "n")
def PluginPreferences(self, parent): red = Gdk.RGBA() red.parse("#ff0000") def validate_color(entry): text = entry.get_text() if not Gdk.RGBA().parse(text): # Invalid color, make text red entry.override_color(Gtk.StateFlags.NORMAL, red) else: # Reset text color entry.override_color(Gtk.StateFlags.NORMAL, None) def elapsed_color_changed(entry): validate_color(entry) CONFIG.elapsed_color = entry.get_text() def hover_color_changed(entry): validate_color(entry) CONFIG.hover_color = entry.get_text() def remaining_color_changed(entry): validate_color(entry) CONFIG.remaining_color = entry.get_text() def on_show_pos_toggled(button, *args): CONFIG.show_current_pos = button.get_active() def seek_amount_changed(spinbox): CONFIG.seek_amount = spinbox.get_value_as_int() vbox = Gtk.VBox(spacing=6) def on_show_time_labels_toggled(button, *args): CONFIG.show_time_labels = button.get_active() if self._bar is not None: self._bar.set_time_label_visibility(CONFIG.show_time_labels) def create_color(label_text, color, callback): hbox = Gtk.HBox(spacing=6) hbox.set_border_width(6) label = Gtk.Label(label=label_text) hbox.pack_start(label, False, True, 0) entry = Gtk.Entry() if color: entry.set_text(color) entry.connect('changed', callback) hbox.pack_start(entry, True, True, 0) return hbox box = create_color(_("Override foreground color:"), CONFIG.elapsed_color, elapsed_color_changed) vbox.pack_start(box, True, True, 0) box = create_color(_("Override hover color:"), CONFIG.hover_color, hover_color_changed) vbox.pack_start(box, True, True, 0) box = create_color(_("Override remaining color:"), CONFIG.remaining_color, remaining_color_changed) vbox.pack_start(box, True, True, 0) show_current_pos = Gtk.CheckButton(label=_("Show current position")) show_current_pos.set_active(CONFIG.show_current_pos) show_current_pos.connect("toggled", on_show_pos_toggled) vbox.pack_start(show_current_pos, True, True, 0) show_time_labels = Gtk.CheckButton(label=_("Show time labels")) show_time_labels.set_active(CONFIG.show_time_labels) show_time_labels.connect("toggled", on_show_time_labels_toggled) vbox.pack_start(show_time_labels, True, True, 0) hbox = Gtk.HBox(spacing=6) hbox.set_border_width(6) label = Gtk.Label(label=_( "Seek amount when scrolling (milliseconds):" )) hbox.pack_start(label, False, True, 0) seek_amount = Gtk.SpinButton( adjustment=Gtk.Adjustment(CONFIG.seek_amount, 0, 60000, 1000, 1000, 0) ) seek_amount.set_numeric(True) seek_amount.connect("changed", seek_amount_changed) hbox.pack_start(seek_amount, True, True, 0) vbox.pack_start(hbox, True, True, 0) return vbox
def __init__(self, Prototype, values, filename, title): if self.is_not_unique(): return super(JSONBasedEditor, self).__init__() self.Prototype = Prototype self.current = None self.filename = filename self.name = Prototype.NAME or Prototype.__name__ self.input_entries = {} self.set_border_width(12) self.set_title(title) self.set_default_size(self._WIDTH, self._HEIGHT) self.add(Gtk.HBox(spacing=6)) self.get_child().set_homogeneous(True) self.accels = Gtk.AccelGroup() # Set up the model for this widget self.model = Gtk.ListStore(object) self._fill_values(values) # The browser for existing data self.view = view = RCMHintedTreeView(model=self.model) view.set_headers_visible(False) view.set_reorderable(True) view.set_rules_hint(True) render = Gtk.CellRendererText() render.set_padding(3, 6) render.props.ellipsize = Pango.EllipsizeMode.END column = Gtk.TreeViewColumn("", render) column.set_cell_data_func(render, self.__cdf) view.append_column(column) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.add(view) self.get_child().pack_start(sw, True, True, 0) vbox = Gtk.VBox(spacing=6) # Input for new ones. frame = self.__build_input_frame() vbox.pack_start(frame, False, True, 0) # Add context menu menu = Gtk.Menu() rem = MenuItem(_("_Remove"), Icons.LIST_REMOVE) keyval, mod = Gtk.accelerator_parse("Delete") rem.add_accelerator('activate', self.accels, keyval, mod, Gtk.AccelFlags.VISIBLE) connect_obj(rem, 'activate', self.__remove, view) menu.append(rem) menu.show_all() view.connect('popup-menu', self.__popup, menu) view.connect('key-press-event', self.__view_key_press) connect_obj(self, 'destroy', Gtk.Menu.destroy, menu) # New and Close buttons bbox = Gtk.HButtonBox() self.remove_but = Button(_("_Remove"), Icons.LIST_REMOVE) self.remove_but.set_sensitive(False) self.new_but = Button(_("_New"), Icons.DOCUMENT_NEW) self.new_but.connect('clicked', self._new_item) bbox.pack_start(self.new_but, True, True, 0) close = Button(_("_Close"), Icons.WINDOW_CLOSE) connect_obj(close, 'clicked', qltk.Window.destroy, self) bbox.pack_start(close, True, True, 0) vbox.pack_end(bbox, False, True, 0) self.get_child().pack_start(vbox, True, True, 0) # Initialise self.selection = view.get_selection() self.selection.connect('changed', self.__select) self.connect('destroy', self.__finish) self.get_child().show_all()
def __init__(self, library): super(MaskedBox, self).__init__(spacing=6) self.model = model = Gtk.ListStore(object) view = RCMHintedTreeView(model=model) view.set_fixed_height_mode(True) view.set_headers_visible(False) self.view = view menu = Gtk.Menu() unhide_item = qltk.MenuItem(_("Unhide"), Icons.LIST_ADD) connect_obj(unhide_item, 'activate', self.__unhide, view, library) menu.append(unhide_item) remove_item = qltk.MenuItem(_("_Remove"), Icons.LIST_REMOVE) connect_obj(remove_item, 'activate', self.__remove, view, library) menu.append(remove_item) menu.show_all() view.connect('popup-menu', self.__popup, menu) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.set_shadow_type(Gtk.ShadowType.IN) sw.add(view) sw.set_size_request(-1, max(sw.size_request().height, 80)) def cdf(column, cell, model, iter, data): row = model[iter] cell.set_property('text', fsn2text(row[0])) def cdf_count(column, cell, model, iter, data): mount = model[iter][0] song_count = len(library.get_masked(mount)) text = ngettext("%d song", "%d songs", song_count) % song_count cell.set_property('text', text) column = Gtk.TreeViewColumn(None) column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) column.pack_start(render, True) column.set_cell_data_func(render, cdf) render = Gtk.CellRendererText() render.props.sensitive = False column.pack_start(render, False) column.set_cell_data_func(render, cdf_count) view.append_column(column) unhide = qltk.Button(_("_Unhide"), Icons.LIST_ADD) connect_obj(unhide, "clicked", self.__unhide, view, library) remove = qltk.Button(_("_Remove"), Icons.LIST_REMOVE) selection = view.get_selection() selection.set_mode(Gtk.SelectionMode.MULTIPLE) selection.connect("changed", self.__select_changed, remove, unhide) selection.emit("changed") connect_obj(remove, "clicked", self.__remove, view, library) vbox = Gtk.VBox(spacing=6) vbox.pack_start(unhide, False, True, 0) vbox.pack_start(remove, False, True, 0) self.pack_start(sw, True, True, 0) self.pack_start(vbox, False, True, 0) for path in library.masked_mount_points: model.append(row=[path]) if not len(model): self.set_sensitive(False) for child in self.get_children(): child.show_all()
def get_field_name(field, key): field_name = (field.human_name or (key and key.replace("_", " "))) return field_name and util.capitalize(field_name) or _("(unknown)")
class ReplayGain(SongsMenuPlugin, PluginConfigMixin): PLUGIN_ID = 'ReplayGain' PLUGIN_NAME = _('Replay Gain') PLUGIN_DESC = _('Analyzes and updates ReplayGain information, ' 'using GStreamer. Results are grouped by album.') PLUGIN_ICON = Icons.MULTIMEDIA_VOLUME_CONTROL CONFIG_SECTION = 'replaygain' plugin_handles = each_song(is_finite, is_writable) def plugin_albums(self, albums): mode = self.config_get("process_if", UpdateMode.ALWAYS) win = RGDialog(albums, parent=self.plugin_window, process_mode=mode) 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() @classmethod def PluginPreferences(cls, parent): vb = Gtk.VBox(spacing=12) # Tabulate all settings for neatness table = Gtk.Table(n_rows=1, n_columns=2) table.props.expand = False table.set_col_spacings(6) table.set_row_spacings(6) rows = [] def process_option_changed(combo): #xcode = combo.get_child().get_text() model = combo.get_model() lbl, value = model[combo.get_active()] cls.config_set("process_if", value) def create_model(): model = Gtk.ListStore(str, str) model.append(["<b>%s</b>" % _("always"), UpdateMode.ALWAYS]) model.append([ _("if <b>any</b> RG tags are missing"), UpdateMode.ANY_MISSING ]) model.append([ _("if <b>album</b> RG tags are missing"), UpdateMode.ALBUM_MISSING ]) return model def set_active(value): for i, item in enumerate(model): if value == item[1]: combo.set_active(i) model = create_model() combo = Gtk.ComboBox(model=model) set_active(cls.config_get("process_if", UpdateMode.ALWAYS)) renderer = Gtk.CellRendererText() combo.connect('changed', process_option_changed) combo.pack_start(renderer, True) combo.add_attribute(renderer, "markup", 0) rows.append((_("_Process albums:"), combo)) for (row, (label_text, entry)) in enumerate(rows): label = Gtk.Label(label=label_text) label.set_alignment(0.0, 0.5) label.set_use_underline(True) label.set_mnemonic_widget(entry) table.attach(label, 0, 1, row, row + 1, xoptions=Gtk.AttachOptions.FILL) table.attach(entry, 1, 2, row, row + 1) # Server settings Frame frame = Frame(_("Existing Tags"), table) vb.pack_start(frame, True, True, 0) return vb
class WaveformSeekBarPlugin(EventPlugin): """The plugin class.""" PLUGIN_ID = "WaveformSeekBar" PLUGIN_NAME = _("Waveform Seek Bar") PLUGIN_ICON = Icons.GO_JUMP PLUGIN_CONFIG_SECTION = __name__ PLUGIN_DESC = _( "A seekbar in the shape of the waveform of the current song.") def __init__(self): self._bar = None def enabled(self): self._bar = WaveformSeekBar(app.player, app.librarian) self._bar.show() app.window.set_seekbar_widget(self._bar) def disabled(self): app.window.set_seekbar_widget(None) self._bar.destroy() self._bar = None def PluginPreferences(self, parent): red = Gdk.RGBA() red.parse("#ff0000") def validate_color(entry): text = entry.get_text() if not Gdk.RGBA().parse(text): # Invalid color, make text red entry.override_color(Gtk.StateFlags.NORMAL, red) else: # Reset text color entry.override_color(Gtk.StateFlags.NORMAL, None) def elapsed_color_changed(entry): validate_color(entry) CONFIG.elapsed_color = entry.get_text() def hover_color_changed(entry): validate_color(entry) CONFIG.hover_color = entry.get_text() def remaining_color_changed(entry): validate_color(entry) CONFIG.remaining_color = entry.get_text() def on_show_pos_toggled(button, *args): CONFIG.show_current_pos = button.get_active() def seek_amount_changed(spinbox): CONFIG.seek_amount = spinbox.get_value_as_int() vbox = Gtk.VBox(spacing=6) def on_show_time_labels_toggled(button, *args): CONFIG.show_time_labels = button.get_active() if self._bar is not None: self._bar.set_time_label_visibility(CONFIG.show_time_labels) def create_color(label_text, color, callback): hbox = Gtk.HBox(spacing=6) hbox.set_border_width(6) label = Gtk.Label(label=label_text) hbox.pack_start(label, False, True, 0) entry = Gtk.Entry() if color: entry.set_text(color) entry.connect('changed', callback) hbox.pack_start(entry, True, True, 0) return hbox box = create_color(_("Override foreground color:"), CONFIG.elapsed_color, elapsed_color_changed) vbox.pack_start(box, True, True, 0) box = create_color(_("Override hover color:"), CONFIG.hover_color, hover_color_changed) vbox.pack_start(box, True, True, 0) box = create_color(_("Override remaining color:"), CONFIG.remaining_color, remaining_color_changed) vbox.pack_start(box, True, True, 0) show_current_pos = Gtk.CheckButton(label=_("Show current position")) show_current_pos.set_active(CONFIG.show_current_pos) show_current_pos.connect("toggled", on_show_pos_toggled) vbox.pack_start(show_current_pos, True, True, 0) show_time_labels = Gtk.CheckButton(label=_("Show time labels")) show_time_labels.set_active(CONFIG.show_time_labels) show_time_labels.connect("toggled", on_show_time_labels_toggled) vbox.pack_start(show_time_labels, True, True, 0) hbox = Gtk.HBox(spacing=6) hbox.set_border_width(6) label = Gtk.Label(label=_( "Seek amount when scrolling (milliseconds):" )) hbox.pack_start(label, False, True, 0) seek_amount = Gtk.SpinButton( adjustment=Gtk.Adjustment(CONFIG.seek_amount, 0, 60000, 1000, 1000, 0) ) seek_amount.set_numeric(True) seek_amount.connect("changed", seek_amount_changed) hbox.pack_start(seek_amount, True, True, 0) vbox.pack_start(hbox, True, True, 0) return vbox
class Browser(Gtk.Box, Filter): """Browsers are how the audio library is presented to the user; they create the list of songs that MainSongList is filled with, and pass them back via a callback function. """ __gsignals__ = { 'songs-selected': (GObject.SignalFlags.RUN_LAST, None, (object, object)), 'songs-activated': (GObject.SignalFlags.RUN_LAST, None, ()), 'uri-received': (GObject.SignalFlags.RUN_LAST, None, (str, )) } name = _("Library Browser") """The browser's name, without an accelerator.""" accelerated_name = _("Library Browser") """The name, with an accelerator.""" keys = ["Unknown"] """Keys which are used to reference the browser from the command line. The first is the primary one. """ priority = 100 """Priority in the menu list (0 is first, higher numbers come later)""" uses_main_library = True """Whether the browser has the main library as source""" def songs_selected(self, songs, is_sorted=False): """Emits the songs-selected signal. If is_sorted is True the songs will be put as is in the song list. In case it's False the songs will be sorted by the song list depending on its current sort configuration. """ self.emit("songs-selected", songs, is_sorted) def songs_activated(self): """Call after calling songs_selected() to activate the songs (start playing, enqueue etc..) """ self.emit("songs-activated") def pack(self, songpane): """For custom packing, define a function that returns a Widget with the browser and MainSongList both packed into it. """ raise NotImplementedError def unpack(self, container, songpane): """Unpack the browser and songlist when switching browsers in the main window. The container will be automatically destroyed afterwards. """ raise NotImplementedError background = True """If true, the global filter will be applied by MainSongList to the songs returned. """ headers: Optional[List[str]] = None """A list of column headers to display; None means all are okay.""" @classmethod def init(klass, library): """Called after library and MainWindow initialization, before the GTK main loop starts. """ pass def save(self): """Save the selected songlist. Browsers should save whatever they need to recreate the criteria for the current song list (not the list itself). """ raise NotImplementedError def restore(self): """Restore the selected songlist. restore is called at startup if the browser is the first loaded. """ raise NotImplementedError def finalize(self, restored): """Called after restore/activate or after the browser is loaded. restored is True if restore was called.""" pass def scroll(self, song): """Scroll to something related to the given song.""" pass def activate(self): """Do whatever is needed to emit songs-selected again.""" raise NotImplementedError can_reorder = False """If the song list should be reorderable. In case this is True every time the song list gets reordered the whole list of songs is passed to reordered(). """ def reordered(self, songs): """In case can_reorder is True and the song list gets reordered this gets called with the whole list of songs. """ raise NotImplementedError def dropped(self, songs): """Called with a list of songs when songs are dropped but the song list does not support reordering. This function should return True if the drop was successful. """ return False def key_pressed(self, event): """Gets called with a key pressed event from the song list. Should return True if the key was handled. """ return False accelerators = None """An AccelGroup that is added to / removed from the window where the browser is. """ def Menu(self, songs, library, items): """This method returns a Gtk.Menu, probably a SongsMenu. After this menu is returned the SongList may modify it further. """ return SongsMenu(library, songs, delete=True, items=items) def status_text(self, count: int, time: Optional[str] = None) -> str: tmpl = numeric_phrase("%d song", "%d songs", count) return f"{tmpl} ({time})" if time else tmpl replaygain_profiles: Optional[List[str]] = None """Replay Gain profiles for this browser.""" def __str__(self): return f"<{type(self).__name__} @ {hex(id(self))}>"
def PluginPreferences(cls, parent): vb = Gtk.VBox(spacing=12) # Tabulate all settings for neatness table = Gtk.Table(n_rows=1, n_columns=2) table.props.expand = False table.set_col_spacings(6) table.set_row_spacings(6) rows = [] def process_option_changed(combo): #xcode = combo.get_child().get_text() model = combo.get_model() lbl, value = model[combo.get_active()] cls.config_set("process_if", value) def create_model(): model = Gtk.ListStore(str, str) model.append(["<b>%s</b>" % _("always"), UpdateMode.ALWAYS]) model.append([ _("if <b>any</b> RG tags are missing"), UpdateMode.ANY_MISSING ]) model.append([ _("if <b>album</b> RG tags are missing"), UpdateMode.ALBUM_MISSING ]) return model def set_active(value): for i, item in enumerate(model): if value == item[1]: combo.set_active(i) model = create_model() combo = Gtk.ComboBox(model=model) set_active(cls.config_get("process_if", UpdateMode.ALWAYS)) renderer = Gtk.CellRendererText() combo.connect('changed', process_option_changed) combo.pack_start(renderer, True) combo.add_attribute(renderer, "markup", 0) rows.append((_("_Process albums:"), combo)) for (row, (label_text, entry)) in enumerate(rows): label = Gtk.Label(label=label_text) label.set_alignment(0.0, 0.5) label.set_use_underline(True) label.set_mnemonic_widget(entry) table.attach(label, 0, 1, row, row + 1, xoptions=Gtk.AttachOptions.FILL) table.attach(entry, 1, 2, row, row + 1) # Server settings Frame frame = Frame(_("Existing Tags"), table) vb.pack_start(frame, True, True, 0) return vb
class LastFMCover(ApiCoverSourcePlugin): PLUGIN_ID = "lastfm-cover" PLUGIN_NAME = _("Last.fm Cover Source") PLUGIN_DESC = _("Downloads covers from Last.fm's cover art archive.") @classmethod def group_by(cls, song): return song.album_key @staticmethod def priority(): return 0.33 # No cover size guarantee, accurate @property def cover_path(self): mbid = self.song.get('musicbrainz_albumid', None) # It is beneficial to use mbid for cover names. if mbid: return path.join(cover_dir, escape_filename(mbid)) else: return super().cover_path @property def url(self): _url = 'https://ws.audioscrobbler.com/2.0?method=album.getinfo&' + \ 'api_key=107db6fd4c1c7f53b1526fafddab2c82&format=json&' + \ 'artist={artist}&album={album}&mbid={mbid}' song = self.song # This can work well for albums in Last.FM artists = self._album_artists_for(song) or 'Various Artists' song = self.song artist = escape_query_value(artists) album = escape_query_value(song.get('album', '')) mbid = escape_query_value(song.get('musicbrainz_albumid', '')) if (artist and album) or mbid: return _url.format(artist=artist, album=album, mbid=mbid) else: return None # Not enough data def _handle_search_response(self, message, json, data=None): if not json: print_d('Server did not return valid JSON') return self.emit('search-complete', []) album = json.get('album', {}) if not album: print_d('Album data is not available') return self.emit('search-complete', []) results = [] for img in album['image']: if img['size'] in ('mega', 'extralarge'): url = img['#text'] if not url: # Yes sometimes it's there but blank continue print_d("Got last.fm image: %s" % img) results.append({ 'artist': album['artist'], 'album': album['name'], 'cover': url.replace('/300x300', '/500x500'), 'dimensions': '500x500' }) # This one can be massive, and slow results.append({ 'artist': album['artist'], 'album': album['name'], 'cover': url.replace('/300x300', ''), 'dimensions': '(original)' }) # Prefer the bigger ones break self.emit('search-complete', results)
def __init__(self, albums, parent, process_mode): super(RGDialog, self).__init__(title=_('ReplayGain Analyzer'), parent=parent) self.add_button(_("_Cancel"), Gtk.ResponseType.CANCEL) self.add_icon_button(_("_Save"), Icons.DOCUMENT_SAVE, Gtk.ResponseType.OK) self.process_mode = process_mode self.set_default_size(600, 400) self.set_border_width(6) hbox = Gtk.HBox(spacing=6) info = Gtk.Label() hbox.pack_start(info, True, True, 0) self.vbox.pack_start(hbox, False, False, 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('icon-name', Icons.DIALOG_ERROR) else: cell.set_property('icon-name', Icons.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) cell.set_sensitive(model[iter_][1]) 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)) cell.set_sensitive(model[iter_][1]) 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) cell.set_sensitive(model[iter_][1]) 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) cell.set_sensitive(model[iter_][1]) 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) self.create_pipelines() self._timeout = None self._sigs = {} self._done = [] self.__fill_view(view, albums) num_to_process = sum(int(rga.should_process) for rga in self._todo) template = ngettext( "There is <b>%(to-process)s</b> album to update (of %(all)s)", "There are <b>%(to-process)s</b> albums to update (of %(all)s)", num_to_process) info.set_markup( template % { "to-process": format_int_locale(num_to_process), "all": format_int_locale(len(self._todo)), }) self.connect("destroy", self.__destroy) self.connect('response', self.__response)