class TComboBoxEntrySave(TestCase): memory = "pattern 1\npattern 2\n" saved = "pattern text\npattern name\n" def setUp(self): quodlibet.config.init() self.fname = tempfile.mkstemp()[1] 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_shared_model(self): self.cbes.append_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.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 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
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