def __save_files(self, parent, model, library): win = WritingWindow(parent, len(model)) was_changed = [] for row in model: song = row[0] track = row[2] if song.get("tracknumber") == track: win.step() continue if not song.valid() and not qltk.ConfirmAction( win, _("Tag may not be accurate"), _("<b>%s</b> changed while the program was running. " "Saving without refreshing your library may " "overwrite other changes to the song.\n\n" "Save this song anyway?") %( util.escape(util.fsdecode(song("~basename")))) ).run(): break song["tracknumber"] = track try: song.write() except: util.print_exc() qltk.ErrorMessage( win, _("Unable to save song"), _("Saving <b>%s</b> failed. The file may be " "read-only, corrupted, or you do not have " "permission to edit it.")%( util.escape(util.fsdecode(song('~basename'))))).run() library.reload(song, changed=was_changed) break was_changed.append(song) if win.step(): break library.changed(was_changed) win.destroy()
def __init__(self, parent, files, asktrash=True, askonly=False): super(DeleteDialog, self).__init__( _("Delete Files"), get_top_parent(parent)) self.set_border_width(6) self.vbox.set_spacing(6) self.set_has_separator(False) self.action_area.set_border_width(0) self.set_resizable(False) self.__files = files if asktrash and trash.can_trash(): b = Button(_("_Move to Trash"), gtk.STOCK_DELETE) self.add_action_widget(b, 0) self.__askonly = askonly self.add_button(gtk.STOCK_CANCEL, 1) self.add_button(gtk.STOCK_DELETE, 2) hbox = gtk.HBox() hbox.set_border_width(6) i = gtk.Image() i.set_from_stock(gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG) i.set_padding(12, 0) i.set_alignment(0.5, 0.0) hbox.pack_start(i, expand=False) vbox = gtk.VBox(spacing=6) base = os.path.basename(files[0]) if len(files) == 1: l = _("Permanently delete this file?") else: l = _("Permanently delete these files?") if len(files) == 1: exp = gtk.Expander("%s" % util.fsdecode(base)) else: exp = gtk.Expander(ngettext("%(title)s and %(count)d more...", "%(title)s and %(count)d more...", len(files)-1) % {'title': util.fsdecode(base), 'count': len(files) - 1}) lab = gtk.Label() lab.set_markup("<big><b>%s</b></big>" % l) lab.set_alignment(0.0, 0.5) vbox.pack_start(lab, expand=False) lab = gtk.Label("\n".join( map(util.fsdecode, map(util.unexpand, files)))) lab.set_alignment(0.1, 0.0) exp.add(gtk.ScrolledWindow()) exp.child.add_with_viewport(lab) exp.child.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) exp.child.child.set_shadow_type(gtk.SHADOW_NONE) vbox.pack_start(exp) hbox.pack_start(vbox) self.vbox.pack_start(hbox) self.vbox.show_all()
def __save(self, addreplace, library): pattern_text = self.combo.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)) was_changed = [] for row in (model or []): song = row[0] changed = False if not song.valid() and not qltk.ConfirmAction( self, _("Tag may not be accurate"), _("<b>%s</b> changed while the program was running. " "Saving without refreshing your library may " "overwrite other changes to the song.\n\n" "Save this song anyway?") %( util.escape(util.fsdecode(song("~basename")))) ).run(): break for i, h in enumerate(pattern.headers): if row[i + 2]: text = row[i + 2].decode("utf-8") if not add or h not in song or not song.multiple_values: 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: qltk.ErrorMessage( self, _("Unable to edit song"), _("Saving <b>%s</b> failed. The file " "may be read-only, corrupted, or you " "do not have permission to edit it.")%( util.escape(util.fsdecode(song('~basename')))) ).run() library.reload(song, changed=was_changed) break was_changed.append(song) if win.step(): break win.destroy() library.changed(was_changed) self.save.set_sensitive(False)
def scan(self, paths, exclude=[], cofuncid=None): added = [] exclude = [util.expanduser(path) for path in exclude if path] for fullpath in paths: print_d("Scanning %r." % fullpath, self) desc = _("Scanning %s") % (util.unexpand(util.fsdecode(fullpath))) with Task(_("Library"), desc) as task: if cofuncid: task.copool(cofuncid) fullpath = util.expanduser(fullpath) if filter(fullpath.startswith, exclude): continue for path, dnames, fnames in os.walk(util.fsnative(fullpath)): for filename in fnames: fullfilename = os.path.join(path, filename) if filter(fullfilename.startswith, exclude): continue if fullfilename not in self._contents: fullfilename = os.path.realpath(fullfilename) if filter(fullfilename.startswith, exclude): continue if fullfilename not in self._contents: item = self.add_filename(fullfilename, False) if item is not None: added.append(item) if len(added) > 20: self.add(added) added = [] task.pulse() yield True if added: self.add(added) added = [] task.pulse() yield True
def ParseM3U(filename, library=None): plname = util.fsdecode(os.path.basename( os.path.splitext(filename)[0])).encode('utf-8') filenames = [] for line in file(filename): line = line.strip() if line.startswith("#"): continue else: filenames.append(line) return __ParsePlaylist(plname, filename, filenames, library)
def __rename(self, library): model = self.view.get_model() win = WritingWindow(self, len(model)) was_changed = [] skip_all = False self.view.freeze_child_notify() rows = [(row[0], row[1], row[2].decode('utf-8')) for row in model] for song, oldname, newname in rows: try: newname = util.fsnative(newname) library.rename(song, newname, changed=was_changed) except StandardError: util.print_exc() if skip_all: continue RESPONSE_SKIP_ALL = 1 buttons = (_("Ignore _All Errors"), RESPONSE_SKIP_ALL, gtk.STOCK_STOP, gtk.RESPONSE_CANCEL, _("_Continue"), gtk.RESPONSE_OK) msg = qltk.Message( gtk.MESSAGE_ERROR, win, _("Unable to rename file"), _("Renaming <b>%s</b> to <b>%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.") %( util.escape(util.fsdecode(oldname)), util.escape(util.fsdecode(newname))), buttons=gtk.BUTTONS_NONE) msg.add_buttons(*buttons) msg.set_default_response(gtk.RESPONSE_OK) resp = msg.run() skip_all |= (resp == RESPONSE_SKIP_ALL) # Preserve old behavior: shift-click is Ignore All mods = gtk.gdk.display_get_default().get_pointer()[3] skip_all |= mods & gtk.gdk.SHIFT_MASK library.reload(song, changed=was_changed) if resp != gtk.RESPONSE_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 run(self): resp = super(DeleteDialog, self).run() if self.__askonly: self.destroy() return resp if resp == 1 or resp == gtk.RESPONSE_DELETE_EVENT: return [] elif resp == 0: s = _("Moving %(current)d/%(total)d.") elif resp == 2: s = _("Deleting %(current)d/%(total)d.") else: return [] files = self.__files w = WaitLoadWindow(self, len(files), s) removed = [] if resp == 0: for filename in files: try: trash.trash(filename) except trash.TrashError: fn = util.escape(util.fsdecode(util.unexpand(filename))) ErrorMessage(self, _("Unable to move to trash"), (_("Moving <b>%s</b> to the trash failed.") % fn)).run() break removed.append(filename) w.step() else: for filename in files: try: os.unlink(filename) except EnvironmentError, s: try: s = unicode(s.strerror, const.ENCODING, 'replace') except TypeError: s = unicode(s.strerror[1], const.ENCODING, 'replace') s = "\n\n" + s fn = util.escape(util.fsdecode(util.unexpand(filename))) ErrorMessage( self, _("Unable to delete file"), (_("Deleting <b>%s</b> failed.") % fn) + s).run() break removed.append(filename) w.step()
def comma(self, key): value = self.__song.comma(key) if isinstance(value, str): value = util.fsdecode(value) elif not isinstance(value, unicode): if isinstance(value, float): value = "%.2f" % value value = unicode(value) for f in self.__formatters: value = f(key, value) return value
def __dict(self, song): dict = {} for key, value in (song or {}).items(): if not isinstance(value, basestring): value = unicode(value) elif isinstance(value, str): value = util.fsdecode(value) dict[key] = dbusutils.dbus_unicode_validate(value) if song: dict["~uri"] = song("~uri") return dict
def match(self, song): if isinstance(song, dict): song = util.fsdecode(song['~filename']) # only match on the last n pieces of a filename, dictated by pattern # this means no pattern may effectively cross a /, despite .* doing so sep = os.path.sep matchon = sep+sep.join(song.split(sep)[-self.slashes:]) match = self.pattern.search(matchon) # dicts for all! if match is None: return {} else: return match.groupdict()
def ParsePLS(filename, name="", library=None): plname = util.fsdecode(os.path.basename( os.path.splitext(filename)[0])).encode('utf-8') filenames = [] for line in file(filename): line = line.strip() if not line.lower().startswith("file"): continue else: try: line = line[line.index("=")+1:].strip() except ValueError: pass else: filenames.append(line) return __ParsePlaylist(plname, filename, filenames, library)
def search(self, data): for name in self.__names: val = data.get(name) or data.get("~" + name, "") if self.res.search(val): return True for name in self.__intern: if self.res.search(data(name)): return True for name in self.__fs: if self.res.search(fsdecode(data(name))): return True return False
def __preview(self, songs): model = self.view.get_model() if songs is None: songs = [row[0] for row in model] pattern = self.combo.child.get_text().decode("utf-8") try: pattern = FileFromPattern(pattern) 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 self.combo.child.get_text(): self.combo.prepend_text(self.combo.child.get_text()) self.combo.write(const.NBP) orignames = [song["~filename"] for song in songs] newnames = [util.fsdecode(util.fsencode(pattern.format(song))) for song in songs] for f in self.filters: if f.active: newnames = f.filter_list(orignames, newnames) model.clear() for song, newname in zip(songs, newnames): basename = util.fsdecode(song("~basename")) model.append(row=[song, basename, newname]) self.preview.set_sensitive(False) self.save.set_sensitive(bool(self.combo.child.get_text())) for song in songs: if not song.is_file: self.set_sensitive(False) break else: self.set_sensitive(True)
def sort_by_func(tag): """Returns a fast sort function for a specific tag (or pattern). Some keys are already in the sort cache, so we can use them.""" def artist_sort(song): return (song.sort_key[1][2]) if callable(tag): return lambda song: human(tag(song)) elif tag == "artistsort": return artist_sort elif tag in FILESYSTEM_TAGS: return lambda song: util.fsdecode(song(tag), note=False) elif tag.startswith("~#") and "~" not in tag[2:]: return lambda song: song(tag) return lambda song: human(song(tag))
def open_chooser(self, action): if not os.path.exists(self.last_dir): self.last_dir = const.HOME if action.get_name() == "AddFolders": chooser = FolderChooser(self, _("Add Music"), self.last_dir) cb = gtk.CheckButton(_("Watch this folder for new songs")) cb.set_active(not config.get("settings", "scan")) cb.show() chooser.set_extra_widget(cb) else: chooser = FileChooser( self, _("Add Music"), formats.filter, self.last_dir) cb = None fns = chooser.run() chooser.destroy() if fns: if action.get_name() == "AddFolders": self.last_dir = fns[0] copool.add(self.__library.scan, fns, funcid="library") else: self.last_dir = os.path.basename(fns[0]) for filename in map(os.path.realpath, map(util.fsnative, fns)): if filename in self.__library: continue song = self.__library.add_filename(filename) if not song: from traceback import format_exception_only as feo tb = feo(sys.last_type, sys.last_value) msg = _("%s could not be added to your library.\n\n") msg %= util.escape(util.fsdecode( os.path.basename(filename))) msg += util.escape("".join(tb).decode( const.ENCODING, "replace")) d = ErrorMessage(self, _("Unable to add song"), msg) d.label.set_selectable(True) d.run() continue if cb and cb.get_active(): dirs = util.split_scan_dirs(config.get("settings", "scan")) for fn in fns: if fn not in dirs: dirs.append(fn) dirs = ":".join(dirs) config.set("settings", "scan", dirs)
def __update(self, songs, total, model, save, revert): if songs is None: songs = [row[0] for row in model] else: songs = list(songs) songs.sort( key=lambda song: (song("~#track"), song("~basename"), song)) model.clear() total.set_value(len(songs)) for song in songs: if not song.can_change("tracknumber"): self.set_sensitive(False) break else: self.set_sensitive(True) for song in songs: basename = util.fsdecode(song("~basename")) model.append(row=[song, basename, song("tracknumber")]) save.set_sensitive(False) revert.set_sensitive(False)
def _file(self, song, box): def ftime(t): if t == 0: return _("Unknown") else: timestr = time.strftime("%c", time.localtime(t)) return timestr.decode(const.ENCODING) fn = util.fsdecode(util.unexpand(song["~filename"])) length = util.format_time_long(song.get("~#length", 0)) size = util.format_size( song.get("~#filesize") or util.size(song["~filename"])) mtime = ftime(util.mtime(song["~filename"])) bitrate = song.get("~#bitrate", 0) if bitrate != 0: bitrate = _("%d kbps") % int(bitrate) else: bitrate = False t = gtk.Table(4, 2) t.set_col_spacings(6) t.set_homogeneous(False) table = [(_("length"), length), (_("file size"), size), (_("modified"), mtime)] if bitrate: table.insert(1, (_("bitrate"), bitrate)) fnlab = Label(fn) fnlab.set_ellipsize(pango.ELLIPSIZE_MIDDLE) t.attach(fnlab, 0, 2, 0, 1, xoptions=gtk.FILL) for i, (l, r) in enumerate(table): l = "<b>%s</b>" % util.capitalize(util.escape(l) + ":") lab = Label() lab.set_markup(l) t.attach(lab, 0, 1, i + 1, i + 2, xoptions=gtk.FILL) t.attach(Label(r), 1, 2, i + 1, i + 2) box.pack_start(Frame(_("File"), t), expand=False, fill=False)
def _post(self, value, song): if value: fn = song.get("~filename", ".") ext = fn[fn.rfind("."):].lower() val_ext = value[-len(ext):].lower() if not ext == val_ext: value += ext.lower() if os.name == "nt": value = util.strip_win32_incompat_from_path(value) value = util.expanduser(value) # Limit each path section to 255 (bytes on linux, chars on win). # http://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits path, ext = os.path.splitext(value) path = map(util.fsnative, path.split(os.sep)) limit = [255] * len(path) limit[-1] -= len(util.fsnative(ext)) elip = lambda (p, l): (len(p) > l and p[:l-2] + "..") or p path = os.sep.join(map(elip, zip(path, limit))) value = util.fsdecode(path) + ext if os.sep in value and not os.path.isabs(value): raise ValueError("Pattern is not rooted") return value
def __preview(self, songs): if songs is None: songs = [row[0] for row in (self.view.get_model() or [])] if songs: pattern_text = self.combo.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 = gtk.ListStore( object, str, *([str] * len(pattern.headers))) for col in self.view.get_columns(): self.view.remove_column(col) col = gtk.TreeViewColumn(_('File'), gtk.CellRendererText(), text=1) col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) 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, i + 2) escaped_title = header.replace("_", "__") col = gtk.TreeViewColumn(escaped_title, render, text=i + 2) col.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE) self.view.append_column(col) for song in songs: basename = util.fsdecode(song("~basename")) row = [song, basename] match = pattern.match(song) for h in pattern.headers: text = match.get(h, '') for f in self.filters: if f.active: text = f.filter(h, text) if not song.multiple_values: text = u", ".join(text.split("\n")) row.append(text) model.append(row=row) # 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 copy(self, songlist, song): if self.__load_db() is None: return False track = gpod.itdb_track_new() # All values should be utf-8 encoded strings # Filepaths should be encoded with the fs encoding # Either combine tags with comma, or only take the first value if self['all_tags']: tag = song.comma else: tag = lambda key: (song.list(key) or ('',))[0] title = tag('title') if self['title_version'] and song('version'): title = " - ".join([title, song('version')]) track.title = util.encode(title) album = tag('album') if self['album_part'] and song('discsubtitle'): album = " - ".join([album, song('discsubtitle')]) track.album = util.encode(album) # String keys for key in ['artist', 'genre', 'grouping', 'composer', 'albumartist']: if hasattr(track, key): # albumartist since libgpod-0.4.2 setattr(track, key, util.encode(tag(key))) # Sort keys (since libgpod-0.5.0) for key in ['artist', 'album', 'albumartist']: if hasattr(track, 'sort_' + key): setattr(track, 'sort_' + key, util.encode(tag(key + 'sort'))) # Numeric keys for key in ['bitrate', 'playcount', 'year']: try: setattr(track, key, int(song('~#'+key))) except ValueError: continue # Numeric keys where the names differ for key, value in { 'cd_nr': song('~#disc'), 'cds': song('~#discs'), 'rating': min(100, song('~#rating') * 100), 'time_added': self.__mactime(time.time()), 'time_modified': self.__mactime(util.mtime(song('~filename'))), 'track_nr': song('~#track'), 'tracklen': song('~#length') * 1000, 'tracks': song('~#tracks'), 'size': util.size(song('~filename')), 'soundcheck': self.__soundcheck(song), }.items(): try: setattr(track, key, int(value)) except ValueError: continue track.filetype = song('~format') track.comment = util.encode(util.fsdecode(song('~filename'))) # Associate a cover with the track if self['covers']: cover = song.find_cover() if cover: # libgpod will copy the file later when the iTunesDB # is saved, so we have to keep a reference around in # case the cover is a temporary file. self.__covers.append(cover) gpod.itdb_track_set_thumbnails( track, util.fsencode(cover.name)) # Add the track to the master playlist gpod.itdb_track_add(self.__itdb, track, -1) master = gpod.itdb_playlist_mpl(self.__itdb) gpod.itdb_playlist_add_track(master, track, -1) # Copy the actual file if gpod.itdb_cp_track_to_ipod(track, song['~filename'], None) == 1: return IPodSong(track) else: return False
def _cdf(self, column, cell, model, iter, tag): value = model[iter][0].comma(tag) if not self._needs_update(value): return cell.set_property('text', util.unexpand(util.fsdecode(value)))
def cell_data(column, cell, model, iter): value = model[iter][0] if value is not None: cell.set_property('text', util.fsdecode( os.path.basename(value) or value))
def __save_files(self, save, revert, model, library): updated = {} deleted = {} added = {} renamed = {} for row in model: if row[EDITED] and not (row[DELETED] or row[RENAMED]): if row[ORIGVALUE] is not None: updated.setdefault(row[TAG], []) updated[row[TAG]].append((util.decode(row[VALUE]), util.decode(row[ORIGVALUE]))) else: added.setdefault(row[TAG], []) added[row[TAG]].append(util.decode(row[VALUE])) if row[EDITED] and row[DELETED]: if row[ORIGVALUE] is not None: deleted.setdefault(row[TAG], []) deleted[row[TAG]].append(util.decode(row[ORIGVALUE])) if row[EDITED] and row[RENAMED] and not row[DELETED]: renamed.setdefault(row[TAG], []) renamed[row[TAG]].append( (util.decode(row[ORIGTAG]), util.decode(row[VALUE]), util.decode(row[ORIGVALUE])) ) was_changed = [] songs = self.__songinfo.songs win = WritingWindow(self, len(songs)) for song in songs: if ( not song.valid() and not qltk.ConfirmAction( self, _("Tag may not be accurate"), _( "<b>%s</b> changed while the program was running. " "Saving without refreshing your library may " "overwrite other changes to the song.\n\n" "Save this song anyway?" ) % util.escape(util.fsdecode(song("~basename"))), ).run() ): break changed = False for key, values in updated.iteritems(): for (new_value, old_value) in values: new_value = util.unescape(new_value) if song.can_change(key): if old_value is None: song.add(key, new_value) else: song.change(key, old_value, new_value) changed = True for key, values in added.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key): song.add(key, value) changed = True for key, values in deleted.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key) and key in song: song.remove(key, value) changed = True save_rename = [] for new_tag, values in renamed.iteritems(): for old_tag, new_value, old_value in values: old_tag = util.unescape(old_tag) old_value = util.unescape(old_value) new_value = util.unescape(new_value) if song.can_change(new_tag) and song.can_change(old_tag) and old_tag in song: if not is_special(new_value): song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) elif is_missing(new_value): value = strip_missing(old_value) song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) else: save_rename.append((new_tag, song[old_tag])) song.remove(old_tag, None) changed = True for tag, value in save_rename: song.add(tag, value) if changed: try: song.write() except: util.print_exc() qltk.ErrorMessage( self, _("Unable to save song"), _( "Saving <b>%s</b> failed. The file " "may be read-only, corrupted, or you " "do not have permission to edit it." ) % (util.escape(util.fsdecode(song("~basename")))), ).run() library.reload(song, changed=was_changed) break was_changed.append(song) if win.step(): break win.destroy() library.changed(was_changed) for b in [save, revert]: b.set_sensitive(False)
def __save_files(self, save, revert, model, library): updated = {} deleted = {} added = {} renamed = {} for row in model: if row[EDITED] and not (row[DELETED] or row[RENAMED]): if row[ORIGVALUE] is not None: updated.setdefault(row[TAG], []) updated[row[TAG]].append( (decode(row[VALUE]), decode(row[ORIGVALUE]))) else: added.setdefault(row[TAG], []) added[row[TAG]].append(decode(row[VALUE])) if row[EDITED] and row[DELETED]: if row[ORIGVALUE] is not None: deleted.setdefault(row[TAG], []) deleted[row[TAG]].append(decode(row[ORIGVALUE])) if row[EDITED] and row[RENAMED] and not row[DELETED]: renamed.setdefault(row[TAG], []) renamed[row[TAG]].append( (decode(row[ORIGTAG]), decode(row[VALUE]), decode(row[ORIGVALUE]))) was_changed = set() songs = self.__songinfo.songs win = WritingWindow(self, len(songs)) win.show() for song in songs: if not song.valid() and not qltk.ConfirmAction( self, _("Tag may not be accurate"), _("<b>%s</b> changed while the program was running. " "Saving without refreshing your library may " "overwrite other changes to the song.\n\n" "Save this song anyway?") % util.escape(util.fsdecode(song("~basename")))).run(): break changed = False for key, values in updated.iteritems(): for (new_value, old_value) in values: new_value = util.unescape(new_value) if song.can_change(key): if old_value is None: song.add(key, new_value) else: song.change(key, old_value, new_value) changed = True for key, values in added.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key): song.add(key, value) changed = True for key, values in deleted.iteritems(): for value in values: value = util.unescape(value) if song.can_change(key) and key in song: song.remove(key, value) changed = True save_rename = [] for new_tag, values in renamed.iteritems(): for old_tag, new_value, old_value in values: old_tag = util.unescape(old_tag) old_value = util.unescape(old_value) new_value = util.unescape(new_value) if (song.can_change(new_tag) and song.can_change(old_tag) and old_tag in song): if not is_special(new_value): song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) elif is_missing(new_value): value = strip_missing(old_value) song.remove(old_tag, old_value) save_rename.append((new_tag, new_value)) else: save_rename.append((new_tag, song[old_tag])) song.remove(old_tag, None) changed = True for tag, value in save_rename: song.add(tag, value) if changed: try: song.write() except: util.print_exc() qltk.ErrorMessage( self, _("Unable to save song"), _("Saving <b>%s</b> failed. The file " "may be read-only, corrupted, or you " "do not have permission to edit it.") % (util.escape(fsdecode(song('~basename'))))).run() library.reload(song, changed=was_changed) break was_changed.add(song) if win.step(): break win.destroy() library.changed(was_changed) for b in [save, revert]: b.set_sensitive(False)
def __init__(self, library, songs, parent=None): super(SongProperties, self).__init__(dialog=False) self.set_transient_for(qltk.get_top_parent(parent)) default_width = 600 config_suffix = "" if len(songs) <= 1: default_width -= 200 config_suffix += "single" self.set_default_size(default_width, 400) self.enable_window_tracking("quodlibet_properties", size_suffix=config_suffix) self.auto_save_on_change = config.getboolean( 'editing', 'auto_save_changes', False) paned = gtk.HPaned() notebook = qltk.Notebook() pages = [] pages.extend([Ctr(self, library) for Ctr in [EditTags, TagsFromPath, RenameFiles]]) if len(songs) > 1: pages.append(TrackNumbers(self, library)) for page in pages: notebook.append_page(page) self.set_border_width(12) fbasemodel = gtk.ListStore(object, str) fmodel = gtk.TreeModelSort(fbasemodel) fview = HintedTreeView(fmodel) fview.connect('button-press-event', self.__pre_selection_changed) fview.set_rules_hint(True) selection = fview.get_selection() selection.set_mode(gtk.SELECTION_MULTIPLE) self.__save = None if len(songs) > 1: render = gtk.CellRendererText() c1 = gtk.TreeViewColumn(_('File'), render, text=1) render.set_property('ellipsize', pango.ELLIPSIZE_END) c1.set_sort_column_id(1) fview.append_column(c1) sw = gtk.ScrolledWindow() sw.add(fview) sw.set_shadow_type(gtk.SHADOW_IN) sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) sw.show_all() paned.pack1(sw, shrink=True, resize=True) # Invisible selections behave a little strangely. So, when # handling this selection, there's a lot of if len(model) == 1 # checks that "hardcode" the first row being selected. for song in songs: fbasemodel.append(row=[song, util.fsdecode(song("~basename"))]) self.connect_object('changed', SongProperties.__set_title, self) selection.select_all() paned.pack2(notebook, shrink=False, resize=True) csig = selection.connect('changed', self.__selection_changed) s1 = library.connect( 'changed', self.__refresh, fbasemodel, fview) s2 = library.connect( 'removed', self.__remove, fbasemodel, selection, csig) self.connect_object('destroy', library.disconnect, s1) self.connect_object('destroy', library.disconnect, s2) self.connect_object('changed', self.set_pending, None) self.emit('changed', songs) self.add(paned) paned.set_position(175) notebook.show() paned.show() self.show()