def __init__(self, filename=None, completion=None, accel_group=None, timeout=DEFAULT_TIMEOUT, validator=Query.validator, star=None): super(SearchBarBox, self).__init__(spacing=6) if filename is None: filename = os.path.join(quodlibet.get_user_dir(), "lists", "queries") combo = ComboBoxEntrySave(filename, count=8, validator=validator, title=_("Saved Searches"), edit_title=_(u"Edit saved searches…")) self.__deferred_changed = DeferredSignal(self.__filter_changed, timeout=timeout, owner=self) self.__combo = combo entry = combo.get_child() self.__entry = entry if completion: entry.set_completion(completion) self._star = star self._query = None self.__sig = combo.connect('text-changed', self.__text_changed) entry.connect('clear', self.__filter_changed) entry.connect('backspace', self.__text_changed) entry.connect('populate-popup', self.__menu) entry.connect('activate', self.__filter_changed) entry.connect('activate', self.__save_search) entry.connect('focus-out-event', self.__save_search) entry.connect('key-press-event', self.__key_pressed) entry.set_placeholder_text(_("Search")) entry.set_tooltip_text( _("Search your library, " "using free text or QL queries")) combo.enable_clear_button() self.pack_start(combo, True, True, 0) if accel_group: key, mod = Gtk.accelerator_parse("<Primary>L") accel_group.connect(key, mod, 0, lambda *x: entry.mnemonic_activate(True)) for child in self.get_children(): child.show_all()
def __init__(self, filename=None, completion=None, accel_group=None, timeout=DEFAULT_TIMEOUT, validator=Query.validator, star=None): super(SearchBarBox, self).__init__(spacing=6) if filename is None: filename = os.path.join( quodlibet.get_user_dir(), "lists", "queries") combo = ComboBoxEntrySave(filename, count=8, validator=validator, title=_("Saved Searches"), edit_title=_(u"Edit saved searches…")) self.__deferred_changed = DeferredSignal( self.__filter_changed, timeout=timeout, owner=self) self.__combo = combo entry = combo.get_child() self.__entry = entry if completion: entry.set_completion(completion) self._star = star self._query = None self.__sig = combo.connect('text-changed', self.__text_changed) entry.connect('clear', self.__filter_changed) entry.connect('backspace', self.__text_changed) entry.connect('populate-popup', self.__menu) entry.connect('activate', self.__filter_changed) entry.connect('activate', self.__save_search) entry.connect('focus-out-event', self.__save_search) entry.connect('key-press-event', self.__key_pressed) entry.set_placeholder_text(_("Search")) entry.set_tooltip_text(_("Search your library, " "using free text or QL queries")) combo.enable_clear_button() self.pack_start(combo, True, True, 0) if accel_group: key, mod = Gtk.accelerator_parse("<Primary>L") accel_group.connect(key, mod, 0, lambda *x: entry.mnemonic_activate(True)) for child in self.get_children(): child.show_all()
def __init__(self, filename=None, completion=None, accel_group=None): super(SearchBarBox, self).__init__(spacing=6) if filename is None: filename = os.path.join(const.USERDIR, "lists", "queries") combo = ComboBoxEntrySave(filename, count=8, validator=Query.is_valid_color, title=_("Saved Searches"), edit_title=_("Edit saved searches...")) self.__refill_id = None self.__combo = combo entry = combo.get_child() self.__entry = entry if completion: entry.set_completion(completion) self.connect('destroy', lambda w: w.__remove_timeout()) self.__sig = combo.connect('text-changed', self.__text_changed) entry.connect('clear', self.__filter_changed) entry.connect('backspace', self.__text_changed) entry.connect('populate-popup', self.__menu) entry.connect('activate', self.__filter_changed) entry.connect('activate', self.__save_search) entry.connect('focus-out-event', self.__save_search) entry.set_placeholder_text(_("Search")) entry.set_tooltip_text( _("Search your library, " "using free text or QL queries")) combo.enable_clear_button() self.pack_start(combo, True, True, 0) if accel_group: key, mod = Gtk.accelerator_parse("<ctrl>L") accel_group.connect(key, mod, 0, lambda *x: entry.mnemonic_activate(True)) for child in self.get_children(): child.show_all()
class RenameFiles(Gtk.VBox): title = _("Rename Files") FILTERS = [SpacesToUnderscores, StripWindowsIncompat, StripDiacriticals, StripNonASCII, Lowercase] handler = RenameFilesPluginHandler() @classmethod def init_plugins(cls): PluginManager.instance.register_handler(cls.handler) def __init__(self, parent, library): super(RenameFiles, self).__init__(spacing=6) self.set_border_width(12) hbox = Gtk.HBox(spacing=6) cbes_defaults = NBP_EXAMPLES.split("\n") self.combo = ComboBoxEntrySave(NBP, cbes_defaults, title=_("Path Patterns"), edit_title=_(u"Edit saved patterns…")) self.combo.show_all() hbox.pack_start(self.combo, True, True, 0) self.preview = qltk.Button(_("_Preview"), Icons.VIEW_REFRESH) self.preview.show() hbox.pack_start(self.preview, False, True, 0) self.pack_start(hbox, False, True, 0) self.combo.get_child().connect('changed', self._changed) model = ObjectStore() self.view = Gtk.TreeView(model=model) self.view.show() sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) self.pack_start(Gtk.VBox(), False, True, 0) filter_box = FilterPluginBox(self.handler, self.FILTERS) filter_box.connect("preview", self.__filter_preview) filter_box.connect("changed", self.__filter_changed) self.filter_box = filter_box self.pack_start(filter_box, False, True, 0) # Save button self.save = Button(_("_Save"), Icons.DOCUMENT_SAVE) self.save.show() bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.save, True, True, 0) self.pack_start(bbox, False, True, 0) render = Gtk.CellRendererText() column = TreeViewColumn(_('File'), render) def cell_data_file(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.name) column.set_cell_data_func(render, cell_data_file) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) render = Gtk.CellRendererText() render.set_property('editable', True) column = TreeViewColumn(_('New Name'), render) def cell_data_new_name(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.new_name or u"") column.set_cell_data_func(render, cell_data_new_name) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) connect_obj(self.preview, 'clicked', self.__preview, None) connect_obj(parent, 'changed', self.__class__.__preview, self) connect_obj(self.save, 'clicked', self.__rename, library) render.connect('edited', self.__row_edited) for child in self.get_children(): child.show() def __filter_preview(self, *args): Gtk.Button.clicked(self.preview) def __filter_changed(self, *args): self._changed(self.combo.get_child()) def _changed(self, entry): self.save.set_sensitive(False) self.preview.set_sensitive(bool(entry.get_text())) def __row_edited(self, renderer, path, new): path = Gtk.TreePath.new_from_string(path) model = self.view.get_model() entry = model[path][0] new = gdecode(new) if entry.new_name != new: entry.new_name = new self.preview.set_sensitive(True) self.save.set_sensitive(True) model.path_changed(path) def __rename(self, library): model = self.view.get_model() win = WritingWindow(self, len(model)) win.show() was_changed = set() skip_all = False self.view.freeze_child_notify() for entry in model.itervalues(): song = entry.song new_name = entry.new_name old_name = entry.name if new_name is None: continue try: library.rename(song, fsnative(new_name), changed=was_changed) except Exception: util.print_exc() if skip_all: continue RESPONSE_SKIP_ALL = 1 msg = qltk.Message( Gtk.MessageType.ERROR, win, _("Unable to rename file"), _("Renaming <b>%(old-name)s</b> to <b>%(new-name)s</b> " "failed. Possibly the target file already exists, " "or you do not have permission to make the " "new file or remove the old one.") % { "old-name": util.escape(old_name), "new-name": util.escape(new_name), }, buttons=Gtk.ButtonsType.NONE) msg.add_button(_("Ignore _All Errors"), RESPONSE_SKIP_ALL) msg.add_icon_button(_("_Stop"), Icons.PROCESS_STOP, Gtk.ResponseType.CANCEL) msg.add_button(_("_Continue"), Gtk.ResponseType.OK) msg.set_default_response(Gtk.ResponseType.OK) resp = msg.run() skip_all |= (resp == RESPONSE_SKIP_ALL) # Preserve old behavior: shift-click is Ignore All mods = Gdk.Display.get_default().get_pointer()[3] skip_all |= mods & Gdk.ModifierType.SHIFT_MASK library.reload(song, changed=was_changed) if resp != Gtk.ResponseType.OK and resp != RESPONSE_SKIP_ALL: break if win.step(): break self.view.thaw_child_notify() win.destroy() library.changed(was_changed) self.save.set_sensitive(False) def __preview(self, songs): model = self.view.get_model() if songs is None: songs = [e.song for e in model.itervalues()] pattern_text = gdecode(self.combo.get_child().get_text()) try: pattern = FileFromPattern(pattern_text) except ValueError: qltk.ErrorMessage( self, _("Path is not absolute"), _("The pattern\n\t<b>%s</b>\ncontains / but " "does not start from root. To avoid misnamed " "folders, root your pattern by starting " "it with / or ~/.") % ( util.escape(pattern))).run() return else: if pattern: self.combo.prepend_text(pattern_text) self.combo.write(NBP) # native paths orignames = [song["~filename"] for song in songs] newnames = [pattern.format(song) for song in songs] for f in self.filter_box.filters: if f.active: newnames = f.filter_list(orignames, newnames) model.clear() for song, newname in zip(songs, newnames): entry = Entry(song) entry.new_name = fsdecode(newname) model.append(row=[entry]) self.preview.set_sensitive(False) self.save.set_sensitive(bool(pattern_text)) for song in songs: if not song.is_file: self.set_sensitive(False) break else: self.set_sensitive(True)
class TagsFromPath(Gtk.VBox): title = _("Tags From Path") FILTERS = [UnderscoresToSpaces, TitleCase, SplitTag] handler = TagsFromPathPluginHandler() @classmethod def init_plugins(cls): PluginManager.instance.register_handler(cls.handler) def __init__(self, parent, library): super(TagsFromPath, self).__init__(spacing=6) self.set_border_width(12) hbox = Gtk.HBox(spacing=6) cbes_defaults = const.TBP_EXAMPLES.split("\n") self.combo = ComboBoxEntrySave(const.TBP, cbes_defaults, title=_("Path Patterns"), edit_title=_(u"Edit saved patterns…")) self.combo.show_all() hbox.pack_start(self.combo, True, True, 0) self.preview = qltk.Button(_("_Preview"), Gtk.STOCK_CONVERT) self.preview.show() hbox.pack_start(self.preview, False, True, 0) self.pack_start(hbox, False, True, 0) self.combo.get_child().connect('changed', self._changed) model = ObjectStore() self.view = Gtk.TreeView(model=model) self.view.show() sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) vbox = Gtk.VBox() addreplace = Gtk.ComboBoxText() addreplace.append_text(_("Tags replace existing ones")) addreplace.append_text(_("Tags are added to existing ones")) addreplace.set_active(config.getboolean("tagsfrompath", "add")) addreplace.connect('changed', self.__add_changed) vbox.pack_start(addreplace, True, True, 0) addreplace.show() self.pack_start(vbox, False, True, 0) filter_box = FilterPluginBox(self.handler, self.FILTERS) filter_box.connect("preview", self.__filter_preview) filter_box.connect("changed", self.__filter_changed) self.filter_box = filter_box self.pack_start(filter_box, False, True, 0) # Save button self.save = Gtk.Button(stock=Gtk.STOCK_SAVE) self.save.show() bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.save, True, True, 0) self.pack_start(bbox, False, True, 0) connect_obj(self.preview, 'clicked', self.__preview, None) connect_obj(parent, 'changed', self.__class__.__preview, self) # Save changes connect_obj(self.save, 'clicked', self.__save, addreplace, library) for child in self.get_children(): child.show() def __filter_preview(self, *args): Gtk.Button.clicked(self.preview) def __filter_changed(self, *args): self._changed(self.combo.get_child()) def _changed(self, entry): self.save.set_sensitive(False) self.preview.set_sensitive(bool(entry.get_text())) def __add_changed(self, combo): config.set("tagsfrompath", "add", str(bool(combo.get_active()))) def __preview(self, songs): if songs is None: songs = [row[0].song for row in (self.view.get_model() or [])] if songs: pattern_text = self.combo.get_child().get_text().decode("utf-8") else: pattern_text = "" try: pattern = TagsFromPattern(pattern_text) except re.error: qltk.ErrorMessage( self, _("Invalid pattern"), _("The pattern\n\t<b>%s</b>\nis invalid. " "Possibly it contains the same tag twice or " "it has unbalanced brackets (< / >).") % (util.escape(pattern_text))).run() return else: if pattern_text: self.combo.prepend_text(pattern_text) self.combo.write(const.TBP) invalid = [] for header in pattern.headers: if not min([song.can_change(header) for song in songs]): invalid.append(header) if len(invalid) and songs: if len(invalid) == 1: title = _("Invalid tag") msg = _("Invalid tag <b>%s</b>\n\nThe files currently" " selected do not support editing this tag.") else: title = _("Invalid tags") msg = _("Invalid tags <b>%s</b>\n\nThe files currently" " selected do not support editing these tags.") qltk.ErrorMessage(self, title, msg % ", ".join(invalid)).run() pattern = TagsFromPattern("") self.view.set_model(None) model = ObjectStore() for col in self.view.get_columns(): self.view.remove_column(col) render = Gtk.CellRendererText() col = TreeViewColumn(_('File'), render) col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) def cell_data_file(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.name) col.set_cell_data_func(render, cell_data_file) def cell_data_header(column, cell, model, iter_, header): entry = model.get_value(iter_) cell.set_property("text", entry.get_match(header)) self.view.append_column(col) for i, header in enumerate(pattern.headers): render = Gtk.CellRendererText() render.set_property('editable', True) render.connect('edited', self.__row_edited, model, header) escaped_title = header.replace("_", "__") col = Gtk.TreeViewColumn(escaped_title, render) col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) col.set_cell_data_func(render, cell_data_header, header) self.view.append_column(col) for song in songs: entry = ListEntry(song) match = pattern.match(song) for h in pattern.headers: text = match.get(h, '') for f in self.filter_box.filters: if f.active: text = f.filter(h, text) if not song.can_multiple_values(h): text = u", ".join(text.split("\n")) entry.matches[h] = text model.append([entry]) # save for last to potentially save time if songs: self.view.set_model(model) self.preview.set_sensitive(False) self.save.set_sensitive(len(pattern.headers) > 0) def __save(self, addreplace, library): pattern_text = self.combo.get_child().get_text().decode('utf-8') pattern = TagsFromPattern(pattern_text) model = self.view.get_model() add = bool(addreplace.get_active()) win = WritingWindow(self, len(model)) win.show() was_changed = set() all_done = False for entry in ((model and model.itervalues()) or []): song = entry.song changed = False if not song.valid(): win.hide() dialog = OverwriteWarning(self, song) resp = dialog.run() win.show() if resp != OverwriteWarning.RESPONSE_SAVE: break for i, h in enumerate(pattern.headers): text = entry.get_match(h) if text: can_multiple = song.can_multiple_values(h) if not add or h not in song or not can_multiple: song[h] = text changed = True else: for val in text.split("\n"): if val not in song.list(h): song.add(h, val) changed = True if changed: try: song.write() except: util.print_exc() WriteFailedError(self, song).run() library.reload(song, changed=was_changed) break was_changed.add(song) if win.step(): break else: all_done = True win.destroy() library.changed(was_changed) self.save.set_sensitive(not all_done) def __row_edited(self, renderer, path, new, model, header): entry = model[path][0] new = new.decode("utf-8") if entry.get_match(header) != new: entry.replace_match(header, new) self.preview.set_sensitive(True) self.save.set_sensitive(True)
class TComboBoxEntrySave(TestCase): memory = "pattern 1\npattern 2\n" saved = "pattern text\npattern name\n" def setUp(self): quodlibet.config.init() h, self.fname = mkstemp() os.close(h) with open(self.fname, "w") as f: f.write(self.memory) with open(self.fname + ".saved", "w") as f: f.write(self.saved) self.cbes = ComboBoxEntrySave(self.fname, count=2) self.cbes2 = ComboBoxEntrySave(self.fname, count=2) def test_equivalence(self): model1 = self.cbes.get_model() model2 = self.cbes2.get_model() self.failUnlessEqual(model1, model2) rows1 = list(model1) rows2 = list(model2) for row1, row2 in zip(rows1, rows2): self.failUnlessEqual(row1[0], row2[0]) self.failUnlessEqual(row1[1], row2[1]) self.failUnlessEqual(row1[2], row2[2]) def test_text_changed_signal(self): called = [0] def cb(*args): called[0] += 1 def get_count(): c = called[0] called[0] = 0 return c self.cbes.connect("text-changed", cb) entry = self.cbes.get_child() entry.set_text("foo") self.failUnlessEqual(get_count(), 1) self.cbes.prepend_text("bar") # in case the model got changed but the entry is still the same # the text-changed signal should not be triggered self.failUnlessEqual(entry.get_text(), "foo") self.failUnlessEqual(get_count(), 0) def test_shared_model(self): self.cbes.prepend_text("a test") self.test_equivalence() def test_initial_size(self): # 1 saved, Edit, separator, 2 remembered self.failUnlessEqual(5, len(self.cbes.get_model())) def test_prepend_text(self): self.cbes.prepend_text("pattern 3") self.memory = "pattern 3\npattern 1\n" self.test_save() def test_save(self): self.cbes.write() self.failUnlessEqual(self.memory, open(self.fname).read()) self.failUnlessEqual(self.saved, open(self.fname + ".saved").read()) def test_set_text_then_prepend(self): self.cbes.get_child().set_text("foobar") self.cbes.prepend_text("foobar") self.memory = "foobar\npattern 1\n" self.test_save() def tearDown(self): self.cbes.destroy() self.cbes2.destroy() os.unlink(self.fname) os.unlink(self.fname + ".saved") quodlibet.config.quit()
class TComboBoxEntrySave(TestCase): memory = "pattern 1\npattern 2\n" saved = "pattern text\npattern name\n" def setUp(self): quodlibet.config.init() h, self.fname = mkstemp() os.close(h) f = file(self.fname, "w") f.write(self.memory) f.close() f = file(self.fname + ".saved", "w") f.write(self.saved) f.close() self.cbes = ComboBoxEntrySave(self.fname, count=2) self.cbes2 = ComboBoxEntrySave(self.fname, count=2) def test_equivalence(self): model1 = self.cbes.get_model() model2 = self.cbes2.get_model() self.failUnlessEqual(model1, model2) rows1 = list(model1) rows2 = list(model2) for row1, row2 in zip(rows1, rows2): self.failUnlessEqual(row1[0], row2[0]) self.failUnlessEqual(row1[1], row2[1]) self.failUnlessEqual(row1[2], row2[2]) def test_text_changed_signal(self): called = [0] def cb(*args): called[0] += 1 def get_count(): c = called[0] called[0] = 0 return c self.cbes.connect("text-changed", cb) entry = self.cbes.get_child() entry.set_text("foo") self.failUnlessEqual(get_count(), 1) self.cbes.prepend_text("bar") # in case the model got changed but the entry is still the same # the text-changed signal should not be triggered self.failUnlessEqual(entry.get_text(), "foo") self.failUnlessEqual(get_count(), 0) def test_shared_model(self): self.cbes.prepend_text("a test") self.test_equivalence() def test_initial_size(self): # 1 saved, Edit, separator, 2 remembered self.failUnlessEqual(5, len(self.cbes.get_model())) def test_prepend_text(self): self.cbes.prepend_text("pattern 3") self.memory = "pattern 3\npattern 1\n" self.test_save() def test_save(self): self.cbes.write() self.failUnlessEqual(self.memory, file(self.fname).read()) self.failUnlessEqual(self.saved, file(self.fname + ".saved").read()) def test_set_text_then_prepend(self): self.cbes.get_child().set_text("foobar") self.cbes.prepend_text("foobar") self.memory = "foobar\npattern 1\n" self.test_save() def tearDown(self): self.cbes.destroy() self.cbes2.destroy() os.unlink(self.fname) os.unlink(self.fname + ".saved") quodlibet.config.quit()
class TagsFromPath(Gtk.VBox): title = _("Tags From Path") FILTERS = [UnderscoresToSpaces, TitleCase, SplitTag] handler = TagsFromPathPluginHandler() @classmethod def init_plugins(cls): PluginManager.instance.register_handler(cls.handler) def __init__(self, parent, library): super(TagsFromPath, self).__init__(spacing=6) self.set_border_width(12) hbox = Gtk.HBox(spacing=6) cbes_defaults = TBP_EXAMPLES.split("\n") self.combo = ComboBoxEntrySave(TBP, cbes_defaults, title=_("Path Patterns"), edit_title=_(u"Edit saved patterns…")) self.combo.show_all() hbox.pack_start(self.combo, True, True, 0) self.preview = qltk.Button(_("_Preview"), Icons.VIEW_REFRESH) self.preview.show() hbox.pack_start(self.preview, False, True, 0) self.pack_start(hbox, False, True, 0) self.combo.get_child().connect('changed', self._changed) model = ObjectStore() self.view = Gtk.TreeView(model=model) self.view.show() sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) vbox = Gtk.VBox() addreplace = Gtk.ComboBoxText() addreplace.append_text(_("Tags replace existing ones")) addreplace.append_text(_("Tags are added to existing ones")) addreplace.set_active(config.getboolean("tagsfrompath", "add")) addreplace.connect('changed', self.__add_changed) vbox.pack_start(addreplace, True, True, 0) addreplace.show() self.pack_start(vbox, False, True, 0) filter_box = FilterPluginBox(self.handler, self.FILTERS) filter_box.connect("preview", self.__filter_preview) filter_box.connect("changed", self.__filter_changed) self.filter_box = filter_box self.pack_start(filter_box, False, True, 0) # Save button self.save = qltk.Button(_("Save"), Icons.DOCUMENT_SAVE) self.save.show() bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.save, True, True, 0) self.pack_start(bbox, False, True, 0) connect_obj(self.preview, 'clicked', self.__preview, None) connect_obj(parent, 'changed', self.__class__.__preview, self) # Save changes connect_obj(self.save, 'clicked', self.__save, addreplace, library) for child in self.get_children(): child.show() def __filter_preview(self, *args): Gtk.Button.clicked(self.preview) def __filter_changed(self, *args): self._changed(self.combo.get_child()) def _changed(self, entry): self.save.set_sensitive(False) self.preview.set_sensitive(bool(entry.get_text())) def __add_changed(self, combo): config.set("tagsfrompath", "add", str(bool(combo.get_active()))) def __preview(self, songs): if songs is None: songs = [row[0].song for row in (self.view.get_model() or [])] if songs: pattern_text = gdecode(self.combo.get_child().get_text()) else: pattern_text = "" try: pattern = TagsFromPattern(pattern_text) except re.error: qltk.ErrorMessage( self, _("Invalid pattern"), _("The pattern\n\t<b>%s</b>\nis invalid. " "Possibly it contains the same tag twice or " "it has unbalanced brackets (< / >).") % ( util.escape(pattern_text))).run() return else: if pattern_text: self.combo.prepend_text(pattern_text) self.combo.write(TBP) invalid = [] for header in pattern.headers: if not min([song.can_change(header) for song in songs]): invalid.append(header) if len(invalid) and songs: if len(invalid) == 1: title = _("Invalid tag") msg = _("Invalid tag <b>%s</b>\n\nThe files currently" " selected do not support editing this tag.") else: title = _("Invalid tags") msg = _("Invalid tags <b>%s</b>\n\nThe files currently" " selected do not support editing these tags.") qltk.ErrorMessage( self, title, msg % ", ".join(invalid)).run() pattern = TagsFromPattern("") self.view.set_model(None) model = ObjectStore() for col in self.view.get_columns(): self.view.remove_column(col) render = Gtk.CellRendererText() col = TreeViewColumn(title=_('File')) col.pack_start(render, True) col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) def cell_data_file(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.name) col.set_cell_data_func(render, cell_data_file) def cell_data_header(column, cell, model, iter_, header): entry = model.get_value(iter_) cell.set_property("text", entry.get_match(header)) self.view.append_column(col) for i, header in enumerate(pattern.headers): render = Gtk.CellRendererText() render.set_property('editable', True) render.connect('edited', self.__row_edited, model, header) escaped_title = header.replace("_", "__") col = Gtk.TreeViewColumn(escaped_title, render) col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) col.set_cell_data_func(render, cell_data_header, header) self.view.append_column(col) for song in songs: entry = ListEntry(song) match = pattern.match(song) for h in pattern.headers: text = match.get(h, '') for f in self.filter_box.filters: if f.active: text = f.filter(h, text) if not song.can_multiple_values(h): text = u", ".join(text.split("\n")) entry.matches[h] = text model.append([entry]) # save for last to potentially save time if songs: self.view.set_model(model) self.preview.set_sensitive(False) self.save.set_sensitive(len(pattern.headers) > 0) def __save(self, addreplace, library): pattern_text = gdecode(self.combo.get_child().get_text()) pattern = TagsFromPattern(pattern_text) model = self.view.get_model() add = bool(addreplace.get_active()) win = WritingWindow(self, len(model)) win.show() was_changed = set() all_done = False for entry in ((model and model.itervalues()) or []): song = entry.song changed = False if not song.valid(): win.hide() dialog = OverwriteWarning(self, song) resp = dialog.run() win.show() if resp != OverwriteWarning.RESPONSE_SAVE: break for i, h in enumerate(pattern.headers): text = entry.get_match(h) if text: can_multiple = song.can_multiple_values(h) if not add or h not in song or not can_multiple: song[h] = text changed = True else: for val in text.split("\n"): if val not in song.list(h): song.add(h, val) changed = True if changed: try: song.write() except AudioFileError: util.print_exc() WriteFailedError(self, song).run() library.reload(song, changed=was_changed) break was_changed.add(song) if win.step(): break else: all_done = True win.destroy() library.changed(was_changed) self.save.set_sensitive(not all_done) def __row_edited(self, renderer, path, new, model, header): entry = model[path][0] new = gdecode(new) if entry.get_match(header) != new: entry.replace_match(header, new) self.preview.set_sensitive(True) self.save.set_sensitive(True)
class RenameFiles(Gtk.VBox): title = _("Rename Files") FILTERS = [ SpacesToUnderscores, StripWindowsIncompat, StripDiacriticals, StripNonASCII, Lowercase ] handler = RenameFilesPluginHandler() IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp'] @classmethod def init_plugins(cls): PluginManager.instance.register_handler(cls.handler) def __init__(self, parent, library): super(RenameFiles, self).__init__(spacing=6) self.__skip_interactive = False self.set_border_width(12) hbox = Gtk.HBox(spacing=6) cbes_defaults = NBP_EXAMPLES.split("\n") self.combo = ComboBoxEntrySave(NBP, cbes_defaults, title=_("Path Patterns"), edit_title=_(u"Edit saved patterns…")) self.combo.show_all() hbox.pack_start(self.combo, True, True, 0) self.preview = qltk.Button(_("_Preview"), Icons.VIEW_REFRESH) self.preview.show() hbox.pack_start(self.preview, False, True, 0) self.pack_start(hbox, False, True, 0) self.combo.get_child().connect('changed', self._changed) model = ObjectStore() self.view = Gtk.TreeView(model=model) self.view.show() sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) self.pack_start(Gtk.VBox(), False, True, 0) # rename options rename_options = Gtk.HBox() # file name options filter_box = FilterPluginBox(self.handler, self.FILTERS) filter_box.connect("preview", self.__filter_preview) filter_box.connect("changed", self.__filter_changed) self.filter_box = filter_box frame_filename_options = Frame(_("File names"), filter_box) frame_filename_options.show_all() rename_options.pack_start(frame_filename_options, False, True, 0) # album art options albumart_box = Gtk.VBox() # move art moveart_box = Gtk.VBox() self.moveart = ConfigCheckButton(_('_Move album art'), "rename", "move_art", populate=True) self.moveart.set_tooltip_text( _("See '[albumart] filenames' config entry " + "for image search strings")) self.moveart.show() moveart_box.pack_start(self.moveart, False, True, 0) self.moveart_overwrite = ConfigCheckButton( _('_Overwrite album art at target'), "rename", "move_art_overwrite", populate=True) self.moveart_overwrite.show() moveart_box.pack_start(self.moveart_overwrite, False, True, 0) albumart_box.pack_start(moveart_box, False, True, 0) # remove empty removeemptydirs_box = Gtk.VBox() self.removeemptydirs = ConfigCheckButton( _('_Remove empty directories'), "rename", "remove_empty_dirs", populate=True) self.removeemptydirs.show() removeemptydirs_box.pack_start(self.removeemptydirs, False, True, 0) albumart_box.pack_start(removeemptydirs_box, False, True, 0) frame_albumart_options = Frame(_("Album art"), albumart_box) frame_albumart_options.show_all() rename_options.pack_start(frame_albumart_options, False, True, 0) self.pack_start(rename_options, False, True, 0) # Save button self.save = Button(_("_Save"), Icons.DOCUMENT_SAVE) self.save.show() bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.save, True, True, 0) self.pack_start(bbox, False, True, 0) render = Gtk.CellRendererText() column = TreeViewColumn(title=_('File')) column.pack_start(render, True) def cell_data_file(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.name) column.set_cell_data_func(render, cell_data_file) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) render = Gtk.CellRendererText() render.set_property('editable', True) column = TreeViewColumn(title=_('New Name')) column.pack_start(render, True) def cell_data_new_name(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.new_name or u"") column.set_cell_data_func(render, cell_data_new_name) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) connect_obj(self.preview, 'clicked', self._preview, None) connect_obj(parent, 'changed', self.__class__._preview, self) connect_obj(self.save, 'clicked', self._rename, library) render.connect('edited', self.__row_edited) for child in self.get_children(): child.show() def __filter_preview(self, *args): Gtk.Button.clicked(self.preview) def __filter_changed(self, *args): self._changed(self.combo.get_child()) def _changed(self, entry): self.save.set_sensitive(False) self.preview.set_sensitive(bool(entry.get_text())) def __row_edited(self, renderer, path, new): path = Gtk.TreePath.new_from_string(path) model = self.view.get_model() entry = model[path][0] new = gdecode(new) if entry.new_name != new: entry.new_name = new self.preview.set_sensitive(True) self.save.set_sensitive(True) model.path_changed(path) def _rename(self, library): model = self.view.get_model() win = WritingWindow(self, len(model)) win.show() was_changed = set() skip_all = self.__skip_interactive self.view.freeze_child_notify() should_move_art = config.getboolean("rename", "move_art") moveart_sets = {} remove_empty_dirs = config.getboolean("rename", "remove_empty_dirs") for entry in itervalues(model): if entry.new_name is None: continue song = entry.song old_name = entry.name old_pathfile = song['~filename'] new_name = entry.new_name new_pathfile = "" # ensure target is a full path if os.path.abspath(new_name) != \ os.path.abspath(os.path.join(os.getcwd(), new_name)): new_pathfile = new_name else: # must be a relative pattern, so prefix the path new_pathfile = \ os.path.join(os.path.dirname(old_pathfile), new_name) try: library.rename(song, text2fsn(new_name), changed=was_changed) except Exception: util.print_exc() if skip_all: continue RESPONSE_SKIP_ALL = 1 msg = qltk.Message( Gtk.MessageType.ERROR, win, _("Unable to rename file"), _("Renaming <b>%(old-name)s</b> to <b>%(new-name)s</b> " "failed. Possibly the target file already exists, " "or you do not have permission to make the " "new file or remove the old one.") % { "old-name": util.escape(old_name), "new-name": util.escape(new_name), }, buttons=Gtk.ButtonsType.NONE) msg.add_button(_("Ignore _All Errors"), RESPONSE_SKIP_ALL) msg.add_icon_button(_("_Stop"), Icons.PROCESS_STOP, Gtk.ResponseType.CANCEL) msg.add_button(_("_Continue"), Gtk.ResponseType.OK) msg.set_default_response(Gtk.ResponseType.OK) resp = msg.run() skip_all |= (resp == RESPONSE_SKIP_ALL) # Preserve old behavior: shift-click is Ignore All mods = Gdk.Display.get_default().get_pointer()[3] skip_all |= mods & Gdk.ModifierType.SHIFT_MASK library.reload(song, changed=was_changed) if resp != Gtk.ResponseType.OK and resp != RESPONSE_SKIP_ALL: break if should_move_art: self._moveart(moveart_sets, old_pathfile, new_pathfile, song) if remove_empty_dirs: path_old = os.path.dirname(old_pathfile) if not os.listdir(path_old): try: os.rmdir(path_old) print_d("Removed empty directory: %r" % path_old, self) except Exception: util.print_exc() if win.step(): break self.view.thaw_child_notify() win.destroy() library.changed(was_changed) self.save.set_sensitive(False) def _moveart(self, art_sets, pathfile_old, pathfile_new, song): path_old = os.path.dirname(os.path.realpath(pathfile_old)) path_new = os.path.dirname(os.path.realpath(pathfile_new)) if os.path.realpath(path_old) == os.path.realpath(path_new): return if (path_old in art_sets.keys() and not art_sets[path_old]): return # get art set for path images = [] if path_old in art_sets.keys(): images = art_sets[path_old] else: def glob_escape(s): for c in '[*?': s = s.replace(c, '[' + c + ']') return s # generate art set for path art_sets[path_old] = images path_old_escaped = glob_escape(path_old) for suffix in self.IMAGE_EXTENSIONS: images.extend( glob.glob(os.path.join(path_old_escaped, "*." + suffix))) if images: # set not empty yet, (re)process filenames = config.getstringlist("albumart", "search_filenames") moves = [] for fn in filenames: fn = os.path.join(path_old, fn) if "<" in fn: # resolve path fnres = ArbitraryExtensionFileFromPattern(fn).format(song) if fnres in images and fnres not in moves: moves.append(fnres) elif "*" in fn: moves.extend(f for f in glob.glob(fn) if f in images and f not in moves) elif fn in images and fn not in moves: moves.append(fn) if len(moves) > 0: overwrite = config.getboolean("rename", "move_art_overwrite") for fnmove in moves: try: # existing files safeguarded until move successful, # then deleted if overwrite set fnmoveto = os.path.join(path_new, os.path.split(fnmove)[1]) fnmoveto_orig = "" if os.path.exists(fnmoveto): fnmoveto_orig = fnmoveto + ".orig" if not os.path.exists(fnmoveto_orig): os.rename(fnmoveto, fnmoveto_orig) else: suffix = 1 while os.path.exists(fnmoveto_orig + "." + str(suffix)): suffix += 1 fnmoveto_orig = (fnmoveto_orig + "." + str(suffix)) os.rename(fnmoveto, fnmoveto_orig) print_d("Renaming image %r to %r" % (fnmove, fnmoveto), self) shutil.move(fnmove, fnmoveto) if overwrite and fnmoveto_orig: os.remove(fnmoveto_orig) images.remove(fnmove) except Exception: util.print_exc() def _preview(self, songs): model = self.view.get_model() if songs is None: songs = [e.song for e in itervalues(model)] pattern_text = gdecode(self.combo.get_child().get_text()) try: pattern = FileFromPattern(pattern_text) except ValueError: qltk.ErrorMessage( self, _("Path is not absolute"), _("The pattern\n\t<b>%s</b>\ncontains / but " "does not start from root. To avoid misnamed " "folders, root your pattern by starting " "it with / or ~/.") % (util.escape(pattern_text))).run() return else: if pattern: self.combo.prepend_text(pattern_text) self.combo.write(NBP) # native paths orignames = [song["~filename"] for song in songs] newnames = [fsn2text(pattern.format(song)) for song in songs] for f in self.filter_box.filters: if f.active: newnames = f.filter_list(orignames, newnames) model.clear() for song, newname in zip(songs, newnames): entry = Entry(song) entry.new_name = newname model.append(row=[entry]) self.preview.set_sensitive(False) self.save.set_sensitive(bool(pattern_text)) for song in songs: if not song.is_file: self.set_sensitive(False) break else: self.set_sensitive(True) @property def test_mode(self): return self.__skip_interactive @test_mode.setter def test_mode(self, value): self.__skip_interactive = value
def PluginPreferences(self, parent): # Check if the queries file exists if not os.path.exists(self.path_query): return self._no_queries_frame() # Read saved searches from file self.queries = {} with open(self.path_query, 'r', encoding='utf-8') as query_file: for query_string in query_file: name = next(query_file).strip() self.queries[name] = Query(query_string.strip()) if not self.queries: # query_file is empty return self._no_queries_frame() main_vbox = Gtk.VBox(spacing=self.spacing_main) self.main_vbox = main_vbox # Saved search selection frame saved_search_vbox = Gtk.VBox(spacing=self.spacing_large) self.saved_search_vbox = saved_search_vbox for query_name, query in self.queries.items(): query_config = self.CONFIG_QUERY_PREFIX + query_name check_button = ConfigCheckButton(query_name, PM.CONFIG_SECTION, self._config_key(query_config)) check_button.set_active(self.config_get_bool(query_config)) saved_search_vbox.pack_start(check_button, False, False, 0) saved_search_scroll = self._expandable_scroll(min_h=0, max_h=300) saved_search_scroll.add(saved_search_vbox) frame = qltk.Frame( label=_('Synchronize the following saved searches:'), child=saved_search_scroll) main_vbox.pack_start(frame, False, False, 0) # Destination path entry field destination_entry = Gtk.Entry( placeholder_text=_('The absolute path to your export location'), text=config.get(PM.CONFIG_SECTION, self.CONFIG_PATH_KEY, '')) destination_entry.connect('changed', self._destination_path_changed) self.destination_entry = destination_entry # Destination path selection button destination_button = qltk.Button(label='', icon_name=Icons.FOLDER_OPEN) destination_button.connect('clicked', self._select_destination_path) # Destination path hbox destination_path_hbox = Gtk.HBox(spacing=self.spacing_small) destination_path_hbox.pack_start(destination_entry, True, True, 0) destination_path_hbox.pack_start(destination_button, False, False, 0) # Destination path information destination_warn_label = self._label_with_icon( _("All pre-existing files in the destination folder that aren't in " "the saved searches will be deleted."), Icons.DIALOG_WARNING) destination_info_label = self._label_with_icon( _('For devices mounted with MTP, export to a local destination ' 'folder, then transfer it to your device with rsync. ' 'Or, when syncing many files to an Android Device, use adb-sync, ' 'which is much faster.'), Icons.DIALOG_INFORMATION) # Destination path frame destination_vbox = Gtk.VBox(spacing=self.spacing_large) destination_vbox.pack_start(destination_path_hbox, False, False, 0) destination_vbox.pack_start(destination_warn_label, False, False, 0) destination_vbox.pack_start(destination_info_label, False, False, 0) frame = qltk.Frame(label=_('Destination path:'), child=destination_vbox) main_vbox.pack_start(frame, False, False, 0) # Export pattern frame export_pattern_combo = ComboBoxEntrySave( self.path_pattern, [self.default_export_pattern], title=_('Path Patterns'), edit_title=_(u'Edit saved patterns…')) export_pattern_combo.enable_clear_button() export_pattern_combo.show_all() export_pattern_entry = export_pattern_combo.get_child() export_pattern_entry.set_placeholder_text( _('The structure of the exported filenames, based on their tags')) export_pattern_entry.set_text( config.get(PM.CONFIG_SECTION, self.CONFIG_PATTERN_KEY, self.default_export_pattern)) export_pattern_entry.connect('changed', self._export_pattern_changed) self.export_pattern_entry = export_pattern_entry frame = qltk.Frame(label=_('Export pattern:'), child=export_pattern_combo) main_vbox.pack_start(frame, False, False, 0) # Start preview button preview_start_button = qltk.Button(label=_('Preview'), icon_name=Icons.VIEW_REFRESH) preview_start_button.set_visible(True) preview_start_button.connect('clicked', self._start_preview) self.preview_start_button = preview_start_button # Stop preview button preview_stop_button = qltk.Button(label=_('Stop preview'), icon_name=Icons.PROCESS_STOP) preview_stop_button.set_visible(False) preview_stop_button.set_no_show_all(True) preview_stop_button.connect('clicked', self._stop_preview) self.preview_stop_button = preview_stop_button # Details view column_types = [column[1] for column in self.model_cols.values()] self.model = Gtk.ListStore(*column_types) self.details_tree = details_tree = HintedTreeView(model=self.model) details_scroll = self._expandable_scroll() details_scroll.set_shadow_type(Gtk.ShadowType.IN) details_scroll.add(details_tree) self.renders = {} # Preview column: status render = Gtk.CellRendererText() column = self._tree_view_column(render, self._cdf_status, title=_('Status'), expand=False, sort=self._model_col_id('tag')) details_tree.append_column(column) # Preview column: file render = Gtk.CellRendererText() column = self._tree_view_column(render, self._cdf_source_path, title=_('Source File'), sort=self._model_col_id('filename')) details_tree.append_column(column) # Preview column: export path render = Gtk.CellRendererText() render.set_property('editable', True) render.connect('edited', self._row_edited) column = self._tree_view_column(render, self._cdf_export_path, title=_('Export Path'), sort=self._model_col_id('export')) details_tree.append_column(column) # Status labels self.status_operation = Gtk.Label(xalign=0.0, yalign=0.5, wrap=True, visible=False, no_show_all=True) self.status_progress = Gtk.Label(xalign=0.0, yalign=0.5, wrap=True, visible=False, no_show_all=True) self.status_duplicates = self._label_with_icon(_( 'Duplicate export paths detected! The export paths above can be ' 'edited before starting the synchronization.'), Icons.DIALOG_WARNING, visible=False) self.status_deletions = self._label_with_icon( _('Existing files in the destination path will be deleted!'), Icons.DIALOG_WARNING, visible=False) # Section for previewing exported files preview_vbox = Gtk.VBox(spacing=self.spacing_large) preview_vbox.pack_start(preview_start_button, False, False, 0) preview_vbox.pack_start(preview_stop_button, False, False, 0) preview_vbox.pack_start(details_scroll, True, True, 0) preview_vbox.pack_start(self.status_operation, False, False, 0) preview_vbox.pack_start(self.status_progress, False, False, 0) preview_vbox.pack_start(self.status_duplicates, False, False, 0) preview_vbox.pack_start(self.status_deletions, False, False, 0) main_vbox.pack_start(preview_vbox, True, True, 0) # Start sync button sync_start_button = qltk.Button(label=_('Start synchronization'), icon_name=Icons.DOCUMENT_SAVE) sync_start_button.set_sensitive(False) sync_start_button.set_visible(True) sync_start_button.connect('clicked', self._start_sync) self.sync_start_button = sync_start_button # Stop sync button sync_stop_button = qltk.Button(label=_('Stop synchronization'), icon_name=Icons.PROCESS_STOP) sync_stop_button.set_visible(False) sync_stop_button.set_no_show_all(True) sync_stop_button.connect('clicked', self._stop_sync) self.sync_stop_button = sync_stop_button # Section for the sync buttons sync_vbox = Gtk.VBox(spacing=self.spacing_large) sync_vbox.pack_start(sync_start_button, False, False, 0) sync_vbox.pack_start(sync_stop_button, False, False, 0) main_vbox.pack_start(sync_vbox, False, False, 0) return main_vbox
class RenameFiles(Gtk.VBox): title = _("Rename Files") FILTERS = [ SpacesToUnderscores, StripWindowsIncompat, StripDiacriticals, StripNonASCII, Lowercase ] handler = RenameFilesPluginHandler() @classmethod def init_plugins(cls): PluginManager.instance.register_handler(cls.handler) def __init__(self, parent, library): super(RenameFiles, self).__init__(spacing=6) self.set_border_width(12) hbox = Gtk.HBox(spacing=6) cbes_defaults = NBP_EXAMPLES.split("\n") self.combo = ComboBoxEntrySave(NBP, cbes_defaults, title=_("Path Patterns"), edit_title=_(u"Edit saved patterns…")) self.combo.show_all() hbox.pack_start(self.combo, True, True, 0) self.preview = qltk.Button(_("_Preview"), Icons.VIEW_REFRESH) self.preview.show() hbox.pack_start(self.preview, False, True, 0) self.pack_start(hbox, False, True, 0) self.combo.get_child().connect('changed', self._changed) model = ObjectStore() self.view = Gtk.TreeView(model=model) self.view.show() sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) self.pack_start(Gtk.VBox(), False, True, 0) filter_box = FilterPluginBox(self.handler, self.FILTERS) filter_box.connect("preview", self.__filter_preview) filter_box.connect("changed", self.__filter_changed) self.filter_box = filter_box self.pack_start(filter_box, False, True, 0) # Save button self.save = Button(_("_Save"), Icons.DOCUMENT_SAVE) self.save.show() bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.save, True, True, 0) self.pack_start(bbox, False, True, 0) render = Gtk.CellRendererText() column = TreeViewColumn(_('File'), render) def cell_data_file(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.name) column.set_cell_data_func(render, cell_data_file) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) render = Gtk.CellRendererText() render.set_property('editable', True) column = TreeViewColumn(_('New Name'), render) def cell_data_new_name(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.new_name or u"") column.set_cell_data_func(render, cell_data_new_name) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) connect_obj(self.preview, 'clicked', self.__preview, None) connect_obj(parent, 'changed', self.__class__.__preview, self) connect_obj(self.save, 'clicked', self.__rename, library) render.connect('edited', self.__row_edited) for child in self.get_children(): child.show() def __filter_preview(self, *args): Gtk.Button.clicked(self.preview) def __filter_changed(self, *args): self._changed(self.combo.get_child()) def _changed(self, entry): self.save.set_sensitive(False) self.preview.set_sensitive(bool(entry.get_text())) def __row_edited(self, renderer, path, new): path = Gtk.TreePath.new_from_string(path) model = self.view.get_model() entry = model[path][0] new = gdecode(new) if entry.new_name != new: entry.new_name = new self.preview.set_sensitive(True) self.save.set_sensitive(True) model.path_changed(path) def __rename(self, library): model = self.view.get_model() win = WritingWindow(self, len(model)) win.show() was_changed = set() skip_all = False self.view.freeze_child_notify() for entry in model.itervalues(): song = entry.song new_name = entry.new_name old_name = entry.name if new_name is None: continue try: library.rename(song, fsnative(new_name), changed=was_changed) except Exception: util.print_exc() if skip_all: continue RESPONSE_SKIP_ALL = 1 msg = qltk.Message( Gtk.MessageType.ERROR, win, _("Unable to rename file"), _("Renaming <b>%(old-name)s</b> to <b>%(new-name)s</b> " "failed. Possibly the target file already exists, " "or you do not have permission to make the " "new file or remove the old one.") % { "old-name": util.escape(old_name), "new-name": util.escape(new_name), }, buttons=Gtk.ButtonsType.NONE) msg.add_button(_("Ignore _All Errors"), RESPONSE_SKIP_ALL) msg.add_icon_button(_("_Stop"), Icons.PROCESS_STOP, Gtk.ResponseType.CANCEL) msg.add_button(_("_Continue"), Gtk.ResponseType.OK) msg.set_default_response(Gtk.ResponseType.OK) resp = msg.run() skip_all |= (resp == RESPONSE_SKIP_ALL) # Preserve old behavior: shift-click is Ignore All mods = Gdk.Display.get_default().get_pointer()[3] skip_all |= mods & Gdk.ModifierType.SHIFT_MASK library.reload(song, changed=was_changed) if resp != Gtk.ResponseType.OK and resp != RESPONSE_SKIP_ALL: break if win.step(): break self.view.thaw_child_notify() win.destroy() library.changed(was_changed) self.save.set_sensitive(False) def __preview(self, songs): model = self.view.get_model() if songs is None: songs = [e.song for e in model.itervalues()] pattern_text = gdecode(self.combo.get_child().get_text()) try: pattern = FileFromPattern(pattern_text) except ValueError: qltk.ErrorMessage( self, _("Path is not absolute"), _("The pattern\n\t<b>%s</b>\ncontains / but " "does not start from root. To avoid misnamed " "folders, root your pattern by starting " "it with / or ~/.") % (util.escape(pattern))).run() return else: if pattern: self.combo.prepend_text(pattern_text) self.combo.write(NBP) # native paths orignames = [song["~filename"] for song in songs] newnames = [pattern.format(song) for song in songs] for f in self.filter_box.filters: if f.active: newnames = f.filter_list(orignames, newnames) model.clear() for song, newname in zip(songs, newnames): entry = Entry(song) entry.new_name = fsdecode(newname) model.append(row=[entry]) self.preview.set_sensitive(False) self.save.set_sensitive(bool(pattern_text)) for song in songs: if not song.is_file: self.set_sensitive(False) break else: self.set_sensitive(True)
class RenameFiles(Gtk.VBox): title = _("Rename Files") FILTERS = [SpacesToUnderscores, StripWindowsIncompat, StripDiacriticals, StripNonASCII, Lowercase] handler = RenameFilesPluginHandler() IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp'] @classmethod def init_plugins(cls): PluginManager.instance.register_handler(cls.handler) def __init__(self, parent, library): super(RenameFiles, self).__init__(spacing=6) self.__skip_interactive = False self.set_border_width(12) hbox = Gtk.HBox(spacing=6) cbes_defaults = NBP_EXAMPLES.split("\n") self.combo = ComboBoxEntrySave(NBP, cbes_defaults, title=_("Path Patterns"), edit_title=_(u"Edit saved patterns…")) self.combo.show_all() hbox.pack_start(self.combo, True, True, 0) self.preview = qltk.Button(_("_Preview"), Icons.VIEW_REFRESH) self.preview.show() hbox.pack_start(self.preview, False, True, 0) self.pack_start(hbox, False, True, 0) self.combo.get_child().connect('changed', self._changed) model = ObjectStore() self.view = Gtk.TreeView(model=model) self.view.show() sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) self.pack_start(Gtk.VBox(), False, True, 0) # rename options rename_options = Gtk.HBox() # file name options filter_box = FilterPluginBox(self.handler, self.FILTERS) filter_box.connect("preview", self.__filter_preview) filter_box.connect("changed", self.__filter_changed) self.filter_box = filter_box frame_filename_options = Frame(_("File names"), filter_box) frame_filename_options.show_all() rename_options.pack_start(frame_filename_options, False, True, 0) # album art options albumart_box = Gtk.VBox() # move art moveart_box = Gtk.VBox() self.moveart = ConfigCheckButton( _('_Move album art'), "rename", "move_art", populate=True) self.moveart.set_tooltip_text( _("See '[albumart] filenames' config entry " + "for image search strings")) self.moveart.show() moveart_box.pack_start(self.moveart, False, True, 0) self.moveart_overwrite = ConfigCheckButton( _('_Overwrite album art at target'), "rename", "move_art_overwrite", populate=True) self.moveart_overwrite.show() moveart_box.pack_start(self.moveart_overwrite, False, True, 0) albumart_box.pack_start(moveart_box, False, True, 0) # remove empty removeemptydirs_box = Gtk.VBox() self.removeemptydirs = ConfigCheckButton( _('_Remove empty directories'), "rename", "remove_empty_dirs", populate=True) self.removeemptydirs.show() removeemptydirs_box.pack_start(self.removeemptydirs, False, True, 0) albumart_box.pack_start(removeemptydirs_box, False, True, 0) frame_albumart_options = Frame(_("Album art"), albumart_box) frame_albumart_options.show_all() rename_options.pack_start(frame_albumart_options, False, True, 0) self.pack_start(rename_options, False, True, 0) # Save button self.save = Button(_("_Save"), Icons.DOCUMENT_SAVE) self.save.show() bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.save, True, True, 0) self.pack_start(bbox, False, True, 0) render = Gtk.CellRendererText() column = TreeViewColumn(title=_('File')) column.pack_start(render, True) def cell_data_file(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.name) column.set_cell_data_func(render, cell_data_file) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) render = Gtk.CellRendererText() render.set_property('editable', True) column = TreeViewColumn(title=_('New Name')) column.pack_start(render, True) def cell_data_new_name(column, cell, model, iter_, data): entry = model.get_value(iter_) cell.set_property("text", entry.new_name or u"") column.set_cell_data_func(render, cell_data_new_name) column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) self.view.append_column(column) connect_obj(self.preview, 'clicked', self._preview, None) connect_obj(parent, 'changed', self.__class__._preview, self) connect_obj(self.save, 'clicked', self._rename, library) render.connect('edited', self.__row_edited) for child in self.get_children(): child.show() def __filter_preview(self, *args): Gtk.Button.clicked(self.preview) def __filter_changed(self, *args): self._changed(self.combo.get_child()) def _changed(self, entry): self.save.set_sensitive(False) self.preview.set_sensitive(bool(entry.get_text())) def __row_edited(self, renderer, path, new): path = Gtk.TreePath.new_from_string(path) model = self.view.get_model() entry = model[path][0] if entry.new_name != new: entry.new_name = new self.preview.set_sensitive(True) self.save.set_sensitive(True) model.path_changed(path) def _rename(self, library): model = self.view.get_model() win = WritingWindow(self, len(model)) win.show() was_changed = set() skip_all = self.__skip_interactive self.view.freeze_child_notify() should_move_art = config.getboolean("rename", "move_art") moveart_sets = {} remove_empty_dirs = config.getboolean("rename", "remove_empty_dirs") for entry in itervalues(model): if entry.new_name is None: continue song = entry.song old_name = entry.name old_pathfile = song['~filename'] new_name = entry.new_name new_pathfile = "" # ensure target is a full path if os.path.abspath(new_name) != \ os.path.abspath(os.path.join(os.getcwd(), new_name)): new_pathfile = new_name else: # must be a relative pattern, so prefix the path new_pathfile = \ os.path.join(os.path.dirname(old_pathfile), new_name) try: library.rename(song, text2fsn(new_name), changed=was_changed) except Exception: util.print_exc() if skip_all: continue RESPONSE_SKIP_ALL = 1 msg = qltk.Message( Gtk.MessageType.ERROR, win, _("Unable to rename file"), _("Renaming <b>%(old-name)s</b> to <b>%(new-name)s</b> " "failed. Possibly the target file already exists, " "or you do not have permission to make the " "new file or remove the old one.") % { "old-name": util.escape(old_name), "new-name": util.escape(new_name), }, buttons=Gtk.ButtonsType.NONE) msg.add_button(_("Ignore _All Errors"), RESPONSE_SKIP_ALL) msg.add_icon_button(_("_Stop"), Icons.PROCESS_STOP, Gtk.ResponseType.CANCEL) msg.add_button(_("_Continue"), Gtk.ResponseType.OK) msg.set_default_response(Gtk.ResponseType.OK) resp = msg.run() skip_all |= (resp == RESPONSE_SKIP_ALL) # Preserve old behavior: shift-click is Ignore All mods = Gdk.Display.get_default().get_pointer()[3] skip_all |= mods & Gdk.ModifierType.SHIFT_MASK library.reload(song, changed=was_changed) if resp != Gtk.ResponseType.OK and resp != RESPONSE_SKIP_ALL: break if should_move_art: self._moveart(moveart_sets, old_pathfile, new_pathfile, song) if remove_empty_dirs: path_old = os.path.dirname(old_pathfile) if not os.listdir(path_old): try: os.rmdir(path_old) print_d("Removed empty directory: %r" % path_old, self) except Exception: util.print_exc() if win.step(): break self.view.thaw_child_notify() win.destroy() library.changed(was_changed) self.save.set_sensitive(False) def _moveart(self, art_sets, pathfile_old, pathfile_new, song): path_old = os.path.dirname(os.path.realpath(pathfile_old)) path_new = os.path.dirname(os.path.realpath(pathfile_new)) if os.path.realpath(path_old) == os.path.realpath(path_new): return if (path_old in art_sets.keys() and not art_sets[path_old]): return # get art set for path images = [] if path_old in art_sets.keys(): images = art_sets[path_old] else: def glob_escape(s): for c in '[*?': s = s.replace(c, '[' + c + ']') return s # generate art set for path art_sets[path_old] = images path_old_escaped = glob_escape(path_old) for suffix in self.IMAGE_EXTENSIONS: images.extend(glob.glob(os.path.join(path_old_escaped, "*." + suffix))) if images: # set not empty yet, (re)process filenames = config.getstringlist("albumart", "search_filenames") moves = [] for fn in filenames: fn = os.path.join(path_old, fn) if "<" in fn: # resolve path fnres = ArbitraryExtensionFileFromPattern(fn).format(song) if fnres in images and fnres not in moves: moves.append(fnres) elif "*" in fn: moves.extend(f for f in glob.glob(fn) if f in images and f not in moves) elif fn in images and fn not in moves: moves.append(fn) if len(moves) > 0: overwrite = config.getboolean("rename", "move_art_overwrite") for fnmove in moves: try: # existing files safeguarded until move successful, # then deleted if overwrite set fnmoveto = os.path.join(path_new, os.path.split(fnmove)[1]) fnmoveto_orig = "" if os.path.exists(fnmoveto): fnmoveto_orig = fnmoveto + ".orig" if not os.path.exists(fnmoveto_orig): os.rename(fnmoveto, fnmoveto_orig) else: suffix = 1 while os.path.exists(fnmoveto_orig + "." + str(suffix)): suffix += 1 fnmoveto_orig = (fnmoveto_orig + "." + str(suffix)) os.rename(fnmoveto, fnmoveto_orig) print_d("Renaming image %r to %r" % (fnmove, fnmoveto), self) shutil.move(fnmove, fnmoveto) if overwrite and fnmoveto_orig: os.remove(fnmoveto_orig) images.remove(fnmove) except Exception: util.print_exc() def _preview(self, songs): model = self.view.get_model() if songs is None: songs = [e.song for e in itervalues(model)] pattern_text = self.combo.get_child().get_text() try: pattern = FileFromPattern(pattern_text) except ValueError: qltk.ErrorMessage( self, _("Path is not absolute"), _("The pattern\n\t<b>%s</b>\ncontains / but " "does not start from root. To avoid misnamed " "folders, root your pattern by starting " "it with / or ~/.") % ( util.escape(pattern_text))).run() return else: if pattern: self.combo.prepend_text(pattern_text) self.combo.write(NBP) # native paths orignames = [song["~filename"] for song in songs] newnames = [fsn2text(pattern.format(song)) for song in songs] for f in self.filter_box.filters: if f.active: newnames = f.filter_list(orignames, newnames) model.clear() for song, newname in zip(songs, newnames): entry = Entry(song) entry.new_name = newname model.append(row=[entry]) self.preview.set_sensitive(False) self.save.set_sensitive(bool(pattern_text)) for song in songs: if not song.is_file: self.set_sensitive(False) break else: self.set_sensitive(True) @property def test_mode(self): return self.__skip_interactive @test_mode.setter def test_mode(self, value): self.__skip_interactive = value
class EditPane(Gtk.VBox): @classmethod def init_plugins(cls): PluginManager.instance.register_handler(cls.handler) def __init__(self, cbes_filename, cbes_defaults): super(EditPane, self).__init__(spacing=6) self.set_border_width(12) hbox = Gtk.HBox(spacing=12) self.combo = ComboBoxEntrySave(cbes_filename, cbes_defaults, title=_("Path Patterns"), edit_title=_("Edit saved patterns...")) hbox.pack_start(self.combo, True, True, 0) self.preview = qltk.Button(_("_Preview"), Gtk.STOCK_CONVERT) hbox.pack_start(self.preview, False, True, 0) self.pack_start(hbox, False, True, 0) self.combo.get_child().connect('changed', self._changed) model = Gtk.ListStore(object, str, str) self.view = Gtk.TreeView(model) sw = Gtk.ScrolledWindow() sw.set_shadow_type(Gtk.ShadowType.IN) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.add(self.view) self.pack_start(sw, True, True, 0) filters = [Kind() for Kind in self.FILTERS] filters.sort() vbox = Gtk.VBox() for f in filters: vbox.pack_start(f, True, True, 0) self.pack_start(vbox, False, True, 0) hb = Gtk.HBox() expander = Gtk.Expander(label=_("_More options...")) expander.set_use_underline(True) adj = Gtk.Alignment(yalign=1.0, xscale=1.0) adj.add(expander) hb.pack_start(adj, True, True, 0) # Save button self.save = Gtk.Button(stock=Gtk.STOCK_SAVE) bbox = Gtk.HButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.END) bbox.pack_start(self.save, True, True, 0) hb.pack_start(bbox, False, True, 0) self.pack_start(hb, False, True, 0) for filt in filters: filt.connect_object('preview', Gtk.Button.clicked, self.preview) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) vbox = Gtk.VBox() self.__filters = filters self.__plugins = [] self.handler.connect("changed", self.__refresh_plugins, vbox, expander) sw.add_with_viewport(vbox) self.pack_start(sw, False, True, 0) expander.connect("notify::expanded", self.__notify_expanded, sw) expander.set_expanded(False) self.show_all() self.handler.emit("changed") sw.hide() @property def filters(self): return self.__filters + self.__plugins def __refresh_plugins(self, handler, vbox, expander): instances = [] for Kind in handler.plugins: try: f = Kind() except: util.print_exc() continue else: instances.append(f) instances.sort() for child in vbox.get_children(): child.destroy() del self.__plugins[:] for f in instances: try: vbox.pack_start(f, True, True, 0) except: util.print_exc() f.destroy() else: try: f.connect_object('preview', Gtk.Button.clicked, self.preview) except: try: f.connect_object('changed', self._changed, self.combo.get_child()) except: util.print_exc() else: self.__plugins.append(f) else: self.__plugins.append(f) vbox.show_all() # Don't display the expander if there aren't any plugins. if not vbox.get_children(): expander.set_expanded(False) expander.hide() else: expander.show() def __notify_expanded(self, expander, event, vbox): vbox.set_property('visible', expander.get_property('expanded')) def _changed(self, entry): self.save.set_sensitive(False) self.preview.set_sensitive(bool(entry.get_text()))