diff --git a/picard/api.py b/picard/api.py index 52049bd44..fdccbe480 100644 --- a/picard/api.py +++ b/picard/api.py @@ -38,9 +38,6 @@ class IFileOpener(Interface): def get_supported_formats(self): pass - def can_open_file(self, filename): - pass - def open_file(self, filename): pass diff --git a/picard/file.py b/picard/file.py index a0a8cf129..593e20eab 100644 --- a/picard/file.py +++ b/picard/file.py @@ -27,25 +27,6 @@ from picard.util import LockableObject, needs_write_lock, needs_read_lock, encod class File(LockableObject): - NEW = 0 - CHANGED = 1 - TO_BE_SAVED = 2 - SAVED = 3 - - def __init__(self, filename): - LockableObject.__init__(self) - self.id = self.new_id() - self.filename = filename - self.base_filename = os.path.basename(filename) - self.state = File.NEW - self.orig_metadata = Metadata() - self.metadata = Metadata() - self.similarity = 1.0 - self.parent = None - - def __str__(self): - return '' % (self.id, self.base_filename) - __id_counter = 0 @staticmethod @@ -53,8 +34,43 @@ class File(LockableObject): File.__id_counter += 1 return File.__id_counter + PENDING = 0 + NORMAL = 1 + CHANGED = 2 + ERROR = 3 + SAVED = 4 + + def __init__(self, filename): + LockableObject.__init__(self) + self.id = self.new_id() + self.filename = filename + self.base_filename = os.path.basename(filename) + self.state = File.PENDING + + self.orig_metadata = Metadata() + self.user_metadata = Metadata() + self.server_metadata = Metadata() + self.metadata = self.user_metadata + + self.orig_metadata["~#length"] = 0 + self.orig_metadata["title"] = os.path.basename(self.filename) + + self.user_metadata.copy(self.orig_metadata) + self.server_metadata.copy(self.orig_metadata) + + self.similarity = 1.0 + self.parent = None + + def __str__(self): + return '' % (self.id, self.base_filename) + + def load(self): + """Save the metadata.""" + self.read() + self.state = File.NORMAL + def save(self): - """Save the file.""" + """Save the metadata.""" raise NotImplementedError def save_images(self): @@ -127,7 +143,6 @@ class File(LockableObject): similarity = metadata1.compare(metadata2) self.lock_for_write() self.similarity = similarity - self.state = self.CHANGED self.unlock() if signal: self.log.debug(u"Updating file %s", self) diff --git a/picard/plugins/picardmutagen/__init__.py b/picard/formats/__init__.py similarity index 87% rename from picard/plugins/picardmutagen/__init__.py rename to picard/formats/__init__.py index 285b9507b..b978fff04 100644 --- a/picard/plugins/picardmutagen/__init__.py +++ b/picard/formats/__init__.py @@ -79,19 +79,19 @@ mutagen._util.delete_bytes = _delete_bytes from picard.api import IFileOpener from picard.component import Component, implements -from picard.plugins.picardmutagen.asf import MutagenASFFile -from picard.plugins.picardmutagen.mp4 import MP4File -from picard.plugins.picardmutagen.id3 import ( +from picard.formats.asf import MutagenASFFile +from picard.formats.mp4 import MP4File +from picard.formats.id3 import ( MP3File, TrueAudioFile, ) -from picard.plugins.picardmutagen.apev2 import ( +from picard.formats.apev2 import ( MonkeysAudioFile, MusepackFile, OptimFROGFile, WavPackFile, ) -from picard.plugins.picardmutagen.vorbis import ( +from picard.formats.vorbis import ( FLACFile, OggFLACFile, OggSpeexFile, @@ -128,16 +128,8 @@ class MutagenComponent(Component): return [(key, value[1]) for key, value in self.__supported_formats.items()] - def can_open_file(self, filename): - for ext in self.__supported_formats.keys(): - if filename.lower().endswith(ext): - return True - return False - def open_file(self, filename): for ext in self.__supported_formats.keys(): if filename.lower().endswith(ext): - file = self.__supported_formats[ext][0](filename) - file.read() - return (file,) + return self.__supported_formats[ext][0](filename) return None diff --git a/picard/plugins/picardmutagen/apev2.py b/picard/formats/apev2.py similarity index 100% rename from picard/plugins/picardmutagen/apev2.py rename to picard/formats/apev2.py diff --git a/picard/plugins/picardmutagen/asf.py b/picard/formats/asf.py similarity index 97% rename from picard/plugins/picardmutagen/asf.py rename to picard/formats/asf.py index 0eabcf797..99fe7951b 100644 --- a/picard/plugins/picardmutagen/asf.py +++ b/picard/formats/asf.py @@ -21,7 +21,7 @@ from picard.file import File from picard.util import encode_filename -from picard.plugins.picardmutagen.mutagenext.asf import ASF +from picard.formats.mutagenext.asf import ASF class MutagenASFFile(File): diff --git a/picard/plugins/picardmutagen/id3.py b/picard/formats/id3.py similarity index 99% rename from picard/plugins/picardmutagen/id3.py rename to picard/formats/id3.py index 9427d06d7..bb16e1b1e 100644 --- a/picard/plugins/picardmutagen/id3.py +++ b/picard/formats/id3.py @@ -22,7 +22,7 @@ import mutagen.mp3 import mutagen.trueaudio from mutagen import id3 from picard.file import File -from picard.plugins.picardmutagen.mutagenext import compatid3 +from picard.formats.mutagenext import compatid3 from picard.util import encode_filename class ID3File(File): diff --git a/picard/plugins/picardmutagen/mp4.py b/picard/formats/mp4.py similarity index 100% rename from picard/plugins/picardmutagen/mp4.py rename to picard/formats/mp4.py diff --git a/picard/plugins/picardmutagen/mutagenext/__init__.py b/picard/formats/mutagenext/__init__.py similarity index 100% rename from picard/plugins/picardmutagen/mutagenext/__init__.py rename to picard/formats/mutagenext/__init__.py diff --git a/picard/plugins/picardmutagen/mutagenext/asf.py b/picard/formats/mutagenext/asf.py similarity index 100% rename from picard/plugins/picardmutagen/mutagenext/asf.py rename to picard/formats/mutagenext/asf.py diff --git a/picard/plugins/picardmutagen/mutagenext/compatid3.py b/picard/formats/mutagenext/compatid3.py similarity index 100% rename from picard/plugins/picardmutagen/mutagenext/compatid3.py rename to picard/formats/mutagenext/compatid3.py diff --git a/picard/plugins/picardmutagen/mutagenext/optimfrog.py b/picard/formats/mutagenext/optimfrog.py similarity index 100% rename from picard/plugins/picardmutagen/mutagenext/optimfrog.py rename to picard/formats/mutagenext/optimfrog.py diff --git a/picard/plugins/picardmutagen/vorbis.py b/picard/formats/vorbis.py similarity index 100% rename from picard/plugins/picardmutagen/vorbis.py rename to picard/formats/vorbis.py diff --git a/picard/plugins/__init__.py b/picard/plugins/__init__.py index 1bdbd1567..58067efcb 100644 --- a/picard/plugins/__init__.py +++ b/picard/plugins/__init__.py @@ -19,6 +19,5 @@ """Built-in plugins.""" -import picard.plugins.cuesheet -import picard.plugins.csv_opener -import picard.plugins.picardmutagen +#import picard.plugins.cuesheet +#import picard.plugins.csv_opener diff --git a/picard/tagger.py b/picard/tagger.py index 5627010d8..a475b9e22 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -31,6 +31,7 @@ import imp import picard.resources import picard.plugins +import picard.formats import picard.tagz from picard import musicdns @@ -248,53 +249,35 @@ class Tagger(QtGui.QApplication, ComponentManager, Component): formats.extend(opener.get_supported_formats()) return formats - def add_files(self, files): - """Load and add files.""" - files = map(os.path.normpath, files) - self.log.debug(u"Adding files %r", files) - filenames = [] - for filename in files: - not_found = True - for file in self.files: - if file.filename == filename: - not_found = False - break - if not_found: - for opener in self.file_openers: - if opener.can_open_file(filename): - filenames.append((filename, opener.open_file)) - if filenames: - self.thread_assist.spawn(self.__add_files_thread, filenames, - thread=self.load_thread) - - def __add_files_thread(self, filenames): - """Load the files.""" - files = [] - for filename, opener in filenames: - try: - files.extend(opener(filename)) - except: - import traceback; traceback.print_exc() - while files: - self.thread_assist.proxy_to_main(self.__add_files_finished, - files[:100]) - files = files[100:] - - def __add_files_finished(self, files): - """Add loaded files to the tagger.""" - for file in files: - self.files.append(file) - album_id = file.metadata["musicbrainz_albumid"] - if album_id: - album = self.get_album_by_id(album_id) - if not album: - album = self.load_album(album_id) - if album.loaded: - self.match_files_to_album([file], album) - else: - self._move_to_album.append((file, album)) - if not file.parent: + def add_files(self, filenames): + """Add files to the tagger.""" + self.log.debug(u"Adding files %r", filenames) + for filename in filenames: + filename = os.path.normpath(filename) + if self.get_file_by_filename(filename): + continue + for opener in self.file_openers: + file = opener.open_file(filename) + if not file: + continue file.move(self.unmatched_files) + self.files.append(file) + self.thread_assist.spawn( + self.__load_file_thread, file, thread=self.load_thread) + + def __load_file_thread(self, file): + """Load metadata from the file.""" + self.log.debug(u"Loading file %r", file.filename) + file.load() + self.thread_assist.proxy_to_main(self.__load_file_finished, file) + + def __load_file_finished(self, file): + """Move loaded file to right album/cluster.""" + file.update() + album_id = file.metadata["musicbrainz_albumid"] + if album_id: + album = self.load_album(album_id) + self.move_files_to_album([file], album) def add_directory(self, directory): """Add all files from the directory ``directory`` to the tagger.""" @@ -305,23 +288,21 @@ class Tagger(QtGui.QApplication, ComponentManager, Component): def __read_directory_thread(self, directory): directories = [encode_filename(directory)] while directories: - files = [] directory = directories.pop() self.log.debug(u"Reading directory %r", directory) - self.thread_assist.proxy_to_main(self.__set_status_bar_message, - N_("Reading directory %s ..."), - directory) + self.thread_assist.proxy_to_main( + self.__set_status_bar_message, + N_("Reading directory %s ..."), decode_filename(directory)) + filenames = [] for name in os.listdir(directory): name = os.path.join(directory, name) if os.path.isdir(name): directories.append(name) else: - files.append(decode_filename(name)) - while files: - self.thread_assist.proxy_to_main(self.add_files, files[:100]) - files = files[100:] - self.thread_assist.proxy_to_main(self.__set_status_bar_message, - N_("Done")) + filenames.append(decode_filename(name)) + if filenames: + self.thread_assist.proxy_to_main(self.add_files, filenames) + self.thread_assist.proxy_to_main(self.__clear_status_bar_message) def get_file_by_id(self, id): """Get file by a file ID.""" @@ -384,12 +365,9 @@ class Tagger(QtGui.QApplication, ComponentManager, Component): def save(self, objects): """Save the specified objects.""" - files = [] - for file in self.get_files_from_objects(objects): - file.state = File.TO_BE_SAVED - files.append(file) self.set_wait_cursor() - self.thread_assist.spawn(self.__save_thread, files) + self.thread_assist.spawn(self.__save_thread, + self.get_files_from_objects(objects)) def __rename_file(self, file): file.lock_for_read() @@ -464,6 +442,7 @@ class Tagger(QtGui.QApplication, ComponentManager, Component): failed = False try: file.save() + file.state = File.SAVED old_filename = self.__rename_file(file) if (self.config.setting["move_files"] and self.config.setting["move_additional_files"]): @@ -712,6 +691,9 @@ class Tagger(QtGui.QApplication, ComponentManager, Component): def analyze(self, objs): """Analyze the selected files.""" files = self.get_files_from_objects(objs) + for file in files: + file.state = File.PENDING + file.update() self.thread_assist.spawn(self.__analyze_thread, files, thread=self._analyze_thread) @@ -734,6 +716,8 @@ class Tagger(QtGui.QApplication, ComponentManager, Component): from picard.musicdns.webservice import TrackFilter, Query ws = self.get_web_service(host="ofa.musicdns.org", pathPrefix="/ofa") for file in files: + if file.state != File.PENDING: + continue file.lock_for_read() try: filename = file.filename @@ -776,6 +760,9 @@ class Tagger(QtGui.QApplication, ComponentManager, Component): self.__lookup_puid(file) else: self.log.debug("Fingerprint looked up, no PUID found.") + if file.state == File.PENDING: + file.state = File.NORMAL + file.update() def cluster(self, objs): """Group files with similar metadata to 'clusters'.""" diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 15befcfb3..e7e651431 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -28,15 +28,6 @@ from picard.config import TextOption __all__ = ["FileTreeView", "AlbumTreeView"] -def matchColor(similarity): - colors = ((255, 255, 255), (223, 125, 125)) - res = [0, 0, 0] - #similarity = (1 - similarity) * (1 - similarity) - similarity = 1 - similarity - for i in range(3): - res[i] = colors[0][i] + (colors[1][i] - colors[0][i]) * similarity - return QtGui.QColor(res[0], res[1], res[2]) - class BaseTreeView(QtGui.QTreeWidget): options = [ @@ -76,6 +67,8 @@ class BaseTreeView(QtGui.QTreeWidget): self.contextMenu.addAction(self.main_window.save_action) self.contextMenu.addAction(self.main_window.remove_action) + self.__file_state_colors[File.NORMAL] = self.tagger.palette().text().color() + self.objectToItem = {} self.itemToObject = {} @@ -115,6 +108,25 @@ class BaseTreeView(QtGui.QTreeWidget): def get_item_from_object(self, obj): return self.objectToItem[obj] + __file_state_colors = { + File.PENDING: QtGui.QColor(128, 128, 128), + File.NORMAL: QtGui.QColor(0, 0, 0), + File.CHANGED: QtGui.QColor(0, 0, 64), + File.ERROR: QtGui.QColor(128, 0, 0), + File.SAVED: QtGui.QColor(0, 128, 0), + } + + def get_file_state_color(self, state): + return self.__file_state_colors[state] + + def get_file_match_color(self, similarity): + c1 = (255, 255, 255) + c2 = (223, 125, 125) + return QtGui.QColor( + c2[0] + (c1[0] - c2[0]) * similarity, + c2[1] + (c1[1] - c2[1]) * similarity, + c2[2] + (c1[2] - c2[2]) * similarity) + def supportedDropActions(self): return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction @@ -275,9 +287,11 @@ class FileTreeView(BaseTreeView): item.setText(0, metadata[u"title"]) item.setText(1, format_time(metadata.get(u"~#length", 0))) item.setText(2, metadata[u"artist"]) - color = matchColor(file.similarity) + fg_color = self.get_file_state_color(file.state) + bg_color = self.get_file_match_color(file.similarity) for i in range(3): - item.setBackgroundColor(i, color) + item.setTextColor(i, fg_color) + item.setBackgroundColor(i, bg_color) def update_file(self, file): try: @@ -385,6 +399,7 @@ class AlbumTreeView(BaseTreeView): item = self.get_item_from_object(track) if track.is_linked(): file = track.linked_file + state = file.state if file.state == File.SAVED: similarity = 1.0 icon = self.icon_saved @@ -393,13 +408,16 @@ class AlbumTreeView(BaseTreeView): icon = self.matchIcons[int(similarity * 5 + 0.5)] else: similarity = 1 + state = File.NORMAL icon = self.noteIcon - - color = matchColor(similarity) + # Colors + fg_color = self.get_file_state_color(state) + bg_color = self.get_file_match_color(similarity) for i in range(3): - item.setBackgroundColor(i, color) + item.setTextColor(i, fg_color) + item.setBackgroundColor(i, bg_color) + # Icon item.setIcon(0, icon) - self._set_album_metadata(track.album) def add_album(self, album):