diff --git a/NEWS.txt b/NEWS.txt index 92dcf0b01..330f20fbe 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,3 +1,16 @@ +Version 0.15 - 2011-07-17 + * Added options for using standardized track, release, and artist metadata. + * Added preferred release format support. + * Expanded preferred release country support to allow multiple countries. + * Added support for tagging non-album tracks (standalone recordings). + * Plugins can now be installed via drag and drop, or a file browser. + * Added several new tags: %_originaldate%, %_recordingcomment%, and %_releasecomment% + * Changes to request queuing: added separate high and low priority queues for each host. + * Tagger scripts now run after metadata plugins finish (#5850) + * The "compilation" tag can now be $unset or modified via tagger script. + * Added a shortcut (Ctrl+I) for Edit->Details. + * Miscellaneous bug fixes. + Version 0.15beta1 - 2011-05-29 * Support for the NGS web service @@ -118,7 +131,7 @@ Version 0.10 - 2008-07-27 * Fixed crash when reading CD TOC on 64-bit systems * Fixed handling of MP4 files with no metadata * Change the hotkey for help to the right key for OS X - * Replace special characters after tagger script evalutaion to allow + * Replace special characters after tagger script evalutaion to allow special characters being replaced by tagger script * Actually ignore 'ignored (folksonomy) tags' * Remove dependency on Mutagen 1.13, version 1.11 is enough now @@ -218,7 +231,7 @@ Version 0.9.0alpha12 - 2007-07-29 environment variable "PICARD_DEBUG". * For plugins: - metadata["~#length"] is now metadata.length - - metadata["~#artwork"] is now metadata.images + - metadata["~#artwork"] is now metadata.images * New Features: * Save embedded images to MP4 files. * Added option to select release events for albums. diff --git a/contrib/plugins/no_release.py b/contrib/plugins/no_release.py new file mode 100644 index 000000000..f3b628079 --- /dev/null +++ b/contrib/plugins/no_release.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +PLUGIN_NAME = u'No release' +PLUGIN_AUTHOR = u'Johannes Weißl' +PLUGIN_DESCRIPTION = '''Do not store specific release information in releases of unknown origin.''' +PLUGIN_VERSION = '0.1' +PLUGIN_API_VERSIONS = ['0.15'] + +from PyQt4 import QtCore, QtGui + +from picard.album import Album +from picard.metadata import register_album_metadata_processor, register_track_metadata_processor +from picard.ui.options import register_options_page, OptionsPage +from picard.ui.itemviews import BaseAction, register_album_action +from picard.config import BoolOption, TextOption + +class Ui_NoReleaseOptionsPage(object): + def setupUi(self, NoReleaseOptionsPage): + NoReleaseOptionsPage.setObjectName('NoReleaseOptionsPage') + NoReleaseOptionsPage.resize(394, 300) + self.verticalLayout = QtGui.QVBoxLayout(NoReleaseOptionsPage) + self.verticalLayout.setObjectName('verticalLayout') + self.groupBox = QtGui.QGroupBox(NoReleaseOptionsPage) + self.groupBox.setObjectName('groupBox') + self.vboxlayout = QtGui.QVBoxLayout(self.groupBox) + self.vboxlayout.setObjectName('vboxlayout') + self.norelease_enable = QtGui.QCheckBox(self.groupBox) + self.norelease_enable.setObjectName('norelease_enable') + self.vboxlayout.addWidget(self.norelease_enable) + self.label = QtGui.QLabel(self.groupBox) + self.label.setObjectName('label') + self.vboxlayout.addWidget(self.label) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName('horizontalLayout') + self.norelease_strip_tags = QtGui.QLineEdit(self.groupBox) + self.norelease_strip_tags.setObjectName('norelease_strip_tags') + self.horizontalLayout.addWidget(self.norelease_strip_tags) + self.vboxlayout.addLayout(self.horizontalLayout) + self.verticalLayout.addWidget(self.groupBox) + spacerItem = QtGui.QSpacerItem(368, 187, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + + self.retranslateUi(NoReleaseOptionsPage) + QtCore.QMetaObject.connectSlotsByName(NoReleaseOptionsPage) + + def retranslateUi(self, NoReleaseOptionsPage): + self.groupBox.setTitle(QtGui.QApplication.translate('NoReleaseOptionsPage', 'No release', None, QtGui.QApplication.UnicodeUTF8)) + self.norelease_enable.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Enable plugin for all releases by default'), None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Tags to strip (comma-separated)'), None, QtGui.QApplication.UnicodeUTF8)) + +def strip_release_specific_metadata(tagger, metadata): + strip_tags = tagger.config.setting['norelease_strip_tags'] + strip_tags = [tag.strip() for tag in strip_tags.split(',')] + for tag in strip_tags: + if tag in metadata: + del metadata[tag] + +class NoReleaseAction(BaseAction): + NAME = _('Remove specific release information...') + def callback(self, objs): + for album in objs: + if isinstance(album, Album): + strip_release_specific_metadata(self.tagger, album.metadata) + for track in album.tracks: + strip_release_specific_metadata(self.tagger, track.metadata) + for file in track.linked_files: + track.update_file_metadata(file) + album.update() + +class NoReleaseOptionsPage(OptionsPage): + NAME = 'norelease' + TITLE = 'No release' + PARENT = 'plugins' + + options = [ + BoolOption('setting', 'norelease_enable', False), + TextOption('setting', 'norelease_strip_tags', 'asin,barcode,catalognumber,date,label,media,releasecountry,releasestatus'), + ] + + def __init__(self, parent=None): + super(NoReleaseOptionsPage, self).__init__(parent) + self.ui = Ui_NoReleaseOptionsPage() + self.ui.setupUi(self) + + def load(self): + self.ui.norelease_strip_tags.setText(self.config.setting['norelease_strip_tags']) + self.ui.norelease_enable.setChecked(self.config.setting['norelease_enable']) + + def save(self): + self.config.setting['norelease_strip_tags'] = unicode(self.ui.norelease_strip_tags.text()) + self.config.setting['norelease_enable'] = self.ui.norelease_enable.isChecked() + +def NoReleaseAlbumProcessor(tagger, metadata, release): + if tagger.config.setting['norelease_enable']: + strip_release_specific_metadata(tagger, metadata) + +def NoReleaseTrackProcessor(tagger, metadata, track, release): + if tagger.config.setting['norelease_enable']: + strip_release_specific_metadata(tagger, metadata) + +register_album_metadata_processor(NoReleaseAlbumProcessor) +register_track_metadata_processor(NoReleaseTrackProcessor) +register_album_action(NoReleaseAction()) +register_options_page(NoReleaseOptionsPage) diff --git a/picard/__init__.py b/picard/__init__.py index d71888b7c..99729f377 100644 --- a/picard/__init__.py +++ b/picard/__init__.py @@ -17,7 +17,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -version_info = (0, 15, 0, 'beta', 2) +version_info = (0, 15, 0, 'final', 0) if version_info[3] == 'final': if version_info[2] == 0: diff --git a/picard/album.py b/picard/album.py index 73d04a1ab..c6d7d689b 100644 --- a/picard/album.py +++ b/picard/album.py @@ -19,8 +19,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import traceback +from collections import deque from PyQt4 import QtCore -from picard.metadata import Metadata, run_album_metadata_processors +from picard.metadata import Metadata, run_album_metadata_processors, run_track_metadata_processors from picard.dataobj import DataObject from picard.file import File from picard.track import Track @@ -28,8 +29,8 @@ from picard.script import ScriptParser from picard.ui.item import Item from picard.util import format_time, partial, translate_artist, queue, mbid_validate from picard.cluster import Cluster -from picard.mbxml import release_to_metadata, track_to_metadata -from picard.const import RELEASE_FORMATS, VARIOUS_ARTISTS_ID +from picard.mbxml import release_to_metadata, medium_to_metadata, track_to_metadata, media_formats_from_node, label_info_from_node +from picard.const import VARIOUS_ARTISTS_ID class Album(DataObject, Item): @@ -38,12 +39,17 @@ class Album(DataObject, Item): DataObject.__init__(self, id) self.metadata = Metadata() self.tracks = [] + self.format_str = "" + self.tracks_str = "" self.loaded = False + self.load_task = None self.rgloaded = False + self.rgid = None self._files = 0 self._requests = 0 self._discid = discid self._after_load_callbacks = queue.Queue() + self._metadata_plugins = deque() self.other_versions = [] self.unmatched_files = Cluster(_("Unmatched Files"), special=True, related_album=self, hide_if_empty=True) @@ -78,13 +84,11 @@ class Album(DataObject, Item): m.length = 0 release_to_metadata(release_node, m, config=self.config, album=self) + self.format_str = media_formats_from_node(release_node.medium_list[0]) + self.rgid = release_node.release_group[0].id if self._discid: m['musicbrainz_discid'] = self._discid - if not self.rgloaded: - releasegroupid = release_node.release_group[0].id - self.tagger.xmlws.get_release_group_by_id(releasegroupid, self._release_group_request_finished) - # 'Translate' artist name if self.config.setting['translate_artist_names']: m['albumartist'] = m['artist'] = translate_artist(m['artist'], m['artistsort']) @@ -93,88 +97,55 @@ class Album(DataObject, Item): if m['musicbrainz_artistid'] == VARIOUS_ARTISTS_ID: m['albumartistsort'] = m['artistsort'] = m['albumartist'] = m['artist'] = self.config.setting['va_name'] - # Album metadata plugins - try: - run_album_metadata_processors(self, m, release_node) - except: - self.log.error(traceback.format_exc()) - - # Prepare parser for user's script - if self.config.setting["enable_tagger_script"]: - script = self.config.setting["tagger_script"] - parser = ScriptParser() - else: - script = parser = None - - # Strip leading/trailing whitespace - m.strip_whitespace() - ignore_tags = [s.strip() for s in self.config.setting['ignore_tags'].split(',')] - artists = set() + first_artist = None + compilation = False + track_counts = [] m['totaldiscs'] = release_node.medium_list[0].count - for medium in release_node.medium_list[0].medium: - discnumber = medium.position[0].text - track_list = medium.track_list[0] - totaltracks = track_list.count - discsubtitle = medium.title[0].text if "title" in medium.children else "" - format = medium.format[0].text if "format" in medium.children else "" + plugins = partial(run_album_metadata_processors, self, m, release_node) + self._metadata_plugins.append(plugins) - for node in track_list.track: - t = Track(node.recording[0].id, self) + for medium_node in release_node.medium_list[0].medium: + mm = Metadata() + mm.copy(m) + medium_to_metadata(medium_node, mm) + track_counts.append(mm['totaltracks']) + + for track_node in medium_node.track_list[0].track: + t = Track(track_node.recording[0].id, self) self._new_tracks.append(t) # Get track metadata tm = t.metadata - tm.copy(m) - tm['discnumber'] = discnumber - tm['discsubtitle'] = discsubtitle - tm['totaltracks'] = totaltracks - if format: tm['media'] = format - - track_to_metadata(node, config=self.config, track=t) - t._customize_metadata(node, release_node, script, parser, ignore_tags) - - artists.add(tm['musicbrainz_artistid']) + tm.copy(mm) + track_to_metadata(track_node, t, self.config) m.length += tm.length - if len(artists) > 1: - for t in self._new_tracks: - t.metadata['compilation'] = '1' + artist_id = tm['musicbrainz_artistid'] + if compilation is False: + if first_artist is None: + first_artist = artist_id + if first_artist != artist_id: + compilation = True + for track in self._new_tracks: + track.metadata['compilation'] = '1' + else: + tm['compilation'] = '1' - if script: - # Run tagger script for the album itself - try: - parser.eval(script, m) - except: - self.log.error(traceback.format_exc()) + t._customize_metadata(ignore_tags) + plugins = partial(run_track_metadata_processors, self, tm, release_node, track_node) + self._metadata_plugins.append(plugins) + + self.tracks_str = " + ".join(track_counts) return True - def _parse_release_group(self, document): - releases = document.metadata[0].release_group[0].release_list[0].release - for release in releases: - version = {} - version["mbid"] = release.id - if "date" in release.children: - version["date"] = release.date[0].text - if "country" in release.children: - version["country"] = release.country[0].text - version["totaltracks"] = [int(m.track_list[0].count) for m in release.medium_list[0].medium] - formats = {} - for medium in release.medium_list[0].medium: - if "format" in medium.children: - f = medium.format[0].text - if f in formats: formats[f] += 1 - else: formats[f] = 1 - if formats: - version["media"] = " + ".join(["%s%s" % (str(j)+u"×" if j>1 else "", RELEASE_FORMATS[i]) - for i, j in formats.items()]) - self.other_versions.append(version) - self.other_versions.sort(key=lambda x: x["date"]) - def _release_request_finished(self, document, http, error): + if self.load_task is None: + return + self.load_task = None parsed = False try: if error: @@ -200,6 +171,20 @@ class Album(DataObject, Item): if parsed or error: self._finalize_loading(error) + def _parse_release_group(self, document): + for node in document.metadata[0].release_list[0].release: + v = {} + v["mbid"] = node.id + v["date"] = node.date[0].text if "date" in node.children else "" + v["country"] = node.country[0].text if "country" in node.children else "" + labels, catnums = label_info_from_node(node.label_info_list[0]) + v["labels"] = ", ".join(set(labels)) + v["catnums"] = ", ".join(set(catnums)) + v["tracks"] = " + ".join([m.track_list[0].count for m in node.medium_list[0].medium]) + v["format"] = media_formats_from_node(node.medium_list[0]) + self.other_versions.append(v) + self.other_versions.sort(key=lambda x: x["date"]) + def _release_group_request_finished(self, document, http, error): try: if error: @@ -212,6 +197,7 @@ class Album(DataObject, Item): self.log.error(traceback.format_exc()) finally: self.rgloaded = True + self.emit(QtCore.SIGNAL("release_group_loaded")) def _finalize_loading(self, error): if error: @@ -220,22 +206,58 @@ class Album(DataObject, Item): del self._new_metadata del self._new_tracks self.update() - else: - if not self._requests: - for track in self.tracks: - for file in list(track.linked_files): - file.move(self.unmatched_files) - self.metadata = self._new_metadata - self.tracks = self._new_tracks - del self._new_metadata - del self._new_tracks - self.loaded = True - self.match_files(self.unmatched_files.files) - self.update() - self.tagger.window.set_statusbar_message('Album %s loaded', self.id, timeout=3000) - while self._after_load_callbacks.qsize() > 0: - func = self._after_load_callbacks.get() - func() + return + + # Run metadata plugins + while self._metadata_plugins: + try: + self._metadata_plugins.popleft()() + except: + self.log.error(traceback.format_exc()) + + if not self._requests: + # Prepare parser for user's script + script = None + if self.config.setting["enable_tagger_script"]: + script = self.config.setting["tagger_script"] + parser = ScriptParser() + + for track in self._new_tracks: + # Update the track with new album metadata, in case it + # was modified by plugins. + for key, values in self._new_metadata.rawitems(): + track.metadata[key] = values[:] + # Run tagger script for each track + if script: + try: + parser.eval(script, track.metadata) + except: + self.log.error(traceback.format_exc()) + # Strip leading/trailing whitespace + track.metadata.strip_whitespace() + + # Run tagger script for the album itself + if script: + try: + parser.eval(script, self._new_metadata) + except: + self.log.error(traceback.format_exc()) + self._new_metadata.strip_whitespace() + + for track in self.tracks: + for file in list(track.linked_files): + file.move(self.unmatched_files) + self.metadata = self._new_metadata + self.tracks = self._new_tracks + del self._new_metadata + del self._new_tracks + self.loaded = True + self.match_files(self.unmatched_files.files) + self.update() + self.tagger.window.set_statusbar_message('Album %s loaded', self.id, timeout=3000) + while self._after_load_callbacks.qsize() > 0: + func = self._after_load_callbacks.get() + func() def load(self): if self._requests: @@ -264,8 +286,9 @@ class Album(DataObject, Item): if self.config.setting['enable_ratings']: require_authentication = True inc += ['user-ratings'] - self.tagger.xmlws.get_release_by_id(self.id, self._release_request_finished, inc=inc, - mblogin=require_authentication) + self.load_task = self.tagger.xmlws.get_release_by_id( + self.id, self._release_request_finished, inc=inc, + mblogin=require_authentication) def run_when_loaded(self, func): if self.loaded: @@ -273,6 +296,11 @@ class Album(DataObject, Item): else: self._after_load_callbacks.put(func) + def stop_loading(self): + if self.load_task: + self.tagger.xmlws.remove_task(self.load_task) + self.load_task = None + def update(self, update_tracks=True): self.tagger.emit(QtCore.SIGNAL("album_updated"), self, update_tracks) @@ -361,7 +389,7 @@ class Album(DataObject, Item): if not self.tracks: return False for track in self.tracks: - if len(track.linked_files) != 1: + if track.num_linked_files != 1: return False else: return True @@ -421,3 +449,6 @@ class NatAlbum(Album): for file in track.linked_files: track.update_file_metadata(file) super(NatAlbum, self).update(update_tracks) + + def _finalize_loading(self, error): + self.update() diff --git a/picard/const.py b/picard/const.py index 90d841623..3dde4065e 100644 --- a/picard/const.py +++ b/picard/const.py @@ -44,6 +44,7 @@ VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' # Release formats RELEASE_FORMATS = { u'CD': N_('CD'), + u'CD-R': N_('CD-R'), u'HDCD': N_('HDCD'), u'Vinyl': N_('Vinyl'), u'7" Vinyl': N_('7" Vinyl'), diff --git a/picard/disc.py b/picard/disc.py index 859b36e67..1421d2956 100644 --- a/picard/disc.py +++ b/picard/disc.py @@ -82,7 +82,7 @@ def _openLibrary(): # Check to see if we're running in a Mac OS X bundle. if sys.platform == 'darwin': try: - libDiscId = ctypes.cdll.LoadLibrary('../Frameworks/libdiscid.1.dylib') + libDiscId = ctypes.cdll.LoadLibrary('../Frameworks/libdiscid.0.dylib') _setPrototypes(libDiscId) return libDiscId except OSError, e: @@ -105,7 +105,7 @@ def _openLibrary(): if sys.platform == 'linux2': libName = 'libdiscid.so.0' elif sys.platform == 'darwin': - libName = 'libdiscid.1.dylib' + libName = 'libdiscid.0.dylib' elif sys.platform == 'win32': libName = 'discid.dll' else: diff --git a/picard/file.py b/picard/file.py index b08cf6949..627c1bdce 100644 --- a/picard/file.py +++ b/picard/file.py @@ -329,6 +329,7 @@ class File(LockableObject, Item): if parent != self.parent: self.log.debug("Moving %r from %r to %r", self, self.parent, parent) self.clear_lookup_task() + self.tagger._ofa.stop_analyze(self) if self.parent: self.clear_pending() self.parent.remove_file(self) @@ -537,11 +538,6 @@ class File(LockableObject, Item): else: self.tagger.move_file_to_nat(self, track.id, node=track) - def lookup_trackid(self, trackid): - """ Try to identify the file using the trackid. """ - self.clear_lookup_task() - self.lookup_task = self.tagger.xmlws.get_track_by_id(trackid, partial(self._lookup_finished, 'trackid')) - def lookup_puid(self, puid): """ Try to identify the file using the PUID. """ self.tagger.window.set_statusbar_message(N_("Looking up the PUID for file %s..."), self.filename) diff --git a/picard/formats/id3.py b/picard/formats/id3.py index d1ea27dfd..1f11007b9 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -274,7 +274,7 @@ class ID3File(File): count = 0 # Convert rating to range between 0 and 255 - rating = int(values[0]) * 255 / (settings['rating_steps'] - 1) + rating = int(round(float(values[0]) * 255 / (settings['rating_steps'] - 1))) tags.add(id3.POPM(email=settings['rating_user_email'], rating=rating, count=count)) elif name in self.__rtranslate: frameid = self.__rtranslate[name] diff --git a/picard/mbxml.py b/picard/mbxml.py index bebaad67d..fbea87cec 100644 --- a/picard/mbxml.py +++ b/picard/mbxml.py @@ -20,8 +20,11 @@ import re import unicodedata from picard.util import format_time, translate_artist +from picard.const import RELEASE_FORMATS +AMAZON_ASIN_URL_REGEX = re.compile(r'^http://(?:www.)?(.*?)(?:\:[0-9]+)?/.*/([0-9B][0-9A-Z]{9})(?:[^0-9A-Z]|$)') + _artist_rel_types = { "composer": "composer", "conductor": "conductor", @@ -69,7 +72,7 @@ def _relations_to_metadata(relation_lists, m, config): if relation_list.target_type == 'artist': for relation in relation_list.relation: value = relation.artist[0].name[0].text - if config and config.setting['translate_artist_names']: + if config.setting['translate_artist_names']: value = translate_artist(value, relation.artist[0].sort_name[0].text) reltype = relation.type attribs = [] @@ -86,33 +89,30 @@ def _relations_to_metadata(relation_lists, m, config): name = _artist_rel_types[reltype] except KeyError: continue - m.add(name, value) + if value not in m[name]: + m.add(name, value) elif relation_list.target_type == 'work': for relation in relation_list.relation: if relation.type == 'performance': work = relation.work[0] if 'relation_list' in work.children: _relations_to_metadata(work.relation_list, m, config) - # TODO: Release, Track, URL relations + elif relation_list.target_type == 'url': + for relation in relation_list.relation: + if relation.type == 'amazon asin': + url = relation.target[0].text + match = AMAZON_ASIN_URL_REGEX.match(url) + if match is not None and 'asin' not in m: + m['asin'] = match.group(2) -def _set_artist_item(m, release, albumname, name, value): - if release: - m[albumname] = value - if name not in m: - m[name] = value - else: - m[name] = value - - -def artist_credit_from_node(node, config=None): +def artist_credit_from_node(node, config): artist = "" artistsort = "" - standardize_name = config and config.setting["standardize_artists"] for credit in node.name_credit: a = credit.artist[0] artistsort += a.sort_name[0].text - if 'name' in credit.children and not standardize_name: + if 'name' in credit.children and not config.setting["standardize_artists"]: artist += credit.name[0].text else: artist += a.name[0].text @@ -122,12 +122,17 @@ def artist_credit_from_node(node, config=None): return (artist, artistsort) -def artist_credit_to_metadata(node, m=None, release=None, config=None): +def artist_credit_to_metadata(node, m, config, release=False): ids = [n.artist[0].id for n in node.name_credit] - _set_artist_item(m, release, 'musicbrainz_albumartistid', 'musicbrainz_artistid', ids) artist, artistsort = artist_credit_from_node(node, config) - _set_artist_item(m, release, 'albumartist', 'artist', artist) - _set_artist_item(m, release, 'albumartistsort', 'artistsort', artistsort) + if release: + m["musicbrainz_albumartistid"] = ids + m["albumartist"] = artist + m["albumartistsort"] = artistsort + else: + m["musicbrainz_artistid"] = ids + m["artist"] = artist + m["artistsort"] = artistsort def label_info_from_node(node): @@ -142,26 +147,41 @@ def label_info_from_node(node): return (labels, catalog_numbers) -def track_to_metadata(node, track, config=None): +def media_formats_from_node(node): + formats = {} + for medium in node.medium: + if "format" in medium.children: + text = medium.format[0].text + formats[text] = formats.get(text, 0) + 1 + if formats: + return " + ".join([ + (str(j) + u"×" if j > 1 else "") + RELEASE_FORMATS.get(i, i) + for i, j in formats.items()]) + else: + return "" + + +def track_to_metadata(node, track, config): m = track.metadata recording_to_metadata(node.recording[0], track, config) + transl = m['releasestatus'] == "pseudo-release" # overwrite with data we have on the track - standardize_title = config and config.setting["standardize_tracks"] - standardize_artist = config and config.setting["standardize_artists"] for name, nodes in node.children.iteritems(): if not nodes: continue - if name == 'title' and not standardize_title: - m['title'] = nodes[0].text - if name == 'position': + if name == 'title': + if not config.setting["standardize_tracks"] or transl: + m['title'] = nodes[0].text + elif name == 'position': m['tracknumber'] = nodes[0].text elif name == 'length' and nodes[0].text: m.length = int(nodes[0].text) - elif name == 'artist_credit' and not standardize_artist: - artist_credit_to_metadata(nodes[0], m, config=config) + elif name == 'artist_credit': + if not config.setting["standardize_artists"] or transl: + artist_credit_to_metadata(nodes[0], m, config) -def recording_to_metadata(node, track, config=None): +def recording_to_metadata(node, track, config): m = track.metadata m.length = 0 m['musicbrainz_trackid'] = node.attribs['id'] @@ -175,11 +195,9 @@ def recording_to_metadata(node, track, config=None): elif name == 'disambiguation': m['~recordingcomment'] = nodes[0].text elif name == 'artist_credit': - artist_credit_to_metadata(nodes[0], m, config=config) - if name == 'relation_list': + artist_credit_to_metadata(nodes[0], m, config) + elif name == 'relation_list': _relations_to_metadata(nodes, m, config) - elif name == 'release_list' and nodes[0].count != '0': - release_to_metadata(nodes[0].release[0], m) elif name == 'tag_list': add_folksonomy_tags(nodes[0], track) elif name == 'user_tag_list': @@ -189,28 +207,44 @@ def recording_to_metadata(node, track, config=None): elif name == 'user_rating': m['~rating'] = nodes[0].text -def _should_standardise_title(config): - return config and config.setting["standardize_releases"] -def release_to_metadata(node, m, config=None, album=None): +def medium_to_metadata(node, m): + for name, nodes in node.children.iteritems(): + if not nodes: + continue + if name == 'position': + m['discnumber'] = nodes[0].text + elif name == 'track_list': + m['totaltracks'] = nodes[0].count + elif name == 'title': + m['discsubtitle'] = nodes[0].text + elif name == 'format': + m['media'] = nodes[0].text + + +def release_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release' node.""" m['musicbrainz_albumid'] = node.attribs['id'] + if "status" in node.children: + m['releasestatus'] = node.status[0].text.lower() + transl = m['releasestatus'] == "pseudo-release" + for name, nodes in node.children.iteritems(): if not nodes: continue if name == 'release_group': release_group_to_metadata(nodes[0], m, config, album) - elif name == 'status': - m['releasestatus'] = nodes[0].text.lower() - elif name == 'title' and not _should_standardise_title(config): - m['album'] = nodes[0].text + elif name == 'title': + if not config.setting["standardize_releases"] or transl: + m['album'] = nodes[0].text elif name == 'disambiguation': m['~releasecomment'] = nodes[0].text elif name == 'asin': m['asin'] = nodes[0].text elif name == 'artist_credit': - artist_credit_to_metadata(nodes[0], m, True, config=config) + if not config.setting["standardize_artists"] or transl: + artist_credit_to_metadata(nodes[0], m, config, release=True) elif name == 'date': m['date'] = nodes[0].text elif name == 'country': @@ -231,20 +265,29 @@ def release_to_metadata(node, m, config=None, album=None): elif name == 'user_tag_list': add_user_folksonomy_tags(nodes[0], album) -def release_group_to_metadata(node, m, config=None, album=None): + +def release_group_to_metadata(node, m, config, album=None): """Make metadata dict from a XML 'release-group' node taken from inside a 'release' node.""" if 'type' in node.attribs: m['releasetype'] = node.type.lower() - if _should_standardise_title(config): - m['album'] = node.title[0].text - + transl = m['releasestatus'] == "pseudo-release" + for name, nodes in node.children.iteritems(): if not nodes: continue - if name == 'tag_list': + if name == 'title': + if config.setting["standardize_releases"] and not transl: + m['album'] = node.title[0].text + elif name == 'artist_credit': + if config.setting["standardize_artists"] and not transl: + artist_credit_to_metadata(nodes[0], m, config, release=True) + elif name == 'first_release_date': + m['~originaldate'] = nodes[0].text + elif name == 'tag_list': add_folksonomy_tags(nodes[0], album) elif name == 'user_tag_list': - add_user_folksonomy_tags(nodes[0], album) + add_user_folksonomy_tags(nodes[0], album) + def add_folksonomy_tags(node, obj): if obj and 'tag' in node.children: diff --git a/picard/musicdns/__init__.py b/picard/musicdns/__init__.py index 2d1820dd8..ac1282451 100644 --- a/picard/musicdns/__init__.py +++ b/picard/musicdns/__init__.py @@ -35,6 +35,7 @@ class OFA(QtCore.QObject): self.log.warning( "Libofa not found! Fingerprinting will be disabled.") self._decoders = [] + self._analyze_tasks = {} plugins = ["avcodec", "directshow", "quicktime", "gstreamer"] for name in plugins: try: @@ -86,7 +87,8 @@ class OFA(QtCore.QObject): def _lookup_fingerprint(self, next, filename, result=None, error=None): try: file = self.tagger.files[filename] - except (KeyError): + del self._analyze_tasks[file] + except KeyError: # The file has been removed. do nothing return @@ -127,7 +129,20 @@ class OFA(QtCore.QObject): return # calculate fingerprint if ofa is not None: - self.tagger.analyze_queue.put(file.filename) + if file not in self._analyze_tasks: + task = (partial(self.calculate_fingerprint, file.filename), + partial(self._lookup_fingerprint, self.tagger._lookup_puid, file.filename), + QtCore.Qt.LowEventPriority + 1) + self._analyze_tasks[file] = task + self.tagger.analyze_queue.put(task) return # no PUID next(result=None) + + def stop_analyze(self, file): + try: + task = self._analyze_tasks[file] + self.tagger.analyze_queue.remove(task) + del self._analyze_tasks[file] + except: + pass diff --git a/picard/tagger.py b/picard/tagger.py index 5d5d49665..f5c90d16f 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -29,6 +29,7 @@ import signal import sys import traceback import time +from collections import deque # Install gettext "noop" function. import __builtin__ @@ -84,7 +85,6 @@ from picard.util import ( mbid_validate ) from picard.webservice import XmlWebService -from picard.mbxml import recording_to_metadata class Tagger(QtGui.QApplication): @@ -127,25 +127,17 @@ class Tagger(QtGui.QApplication): self.thread_pool = thread.ThreadPool(self) self.load_queue = queue.Queue() - self.load_queue.run_item = thread.generic_run_item - self.save_queue = queue.Queue() - self.save_queue.run_item = thread.generic_run_item - self.analyze_queue = queue.Queue() - self.analyze_queue.run_item = analyze_thread_run_item - self.analyze_queue.next = self._lookup_puid - self.other_queue = queue.Queue() - self.other_queue.run_item = thread.generic_run_item threads = self.thread_pool.threads - threads.append(thread.Thread(self.thread_pool, [self.load_queue, - self.other_queue])) - threads.append(thread.Thread(self.thread_pool, [self.save_queue])) - threads.append(thread.Thread(self.thread_pool, [self.other_queue, - self.load_queue])) - threads.append(thread.Thread(self.thread_pool, [self.analyze_queue])) + threads.append(thread.Thread(self.thread_pool, self.load_queue)) + threads.append(thread.Thread(self.thread_pool, self.load_queue)) + threads.append(thread.Thread(self.thread_pool, self.save_queue)) + threads.append(thread.Thread(self.thread_pool, self.other_queue)) + threads.append(thread.Thread(self.thread_pool, self.other_queue)) + threads.append(thread.Thread(self.thread_pool, self.analyze_queue)) self.thread_pool.start() self.stopping = False @@ -181,7 +173,6 @@ class Tagger(QtGui.QApplication): # Initialize fingerprinting self._ofa = musicdns.OFA() self._ofa.init() - self.analyze_queue.ofa = self._ofa # Load plugins self.pluginmanager = PluginManager() @@ -327,7 +318,7 @@ class Tagger(QtGui.QApplication): else: self.move_file_to_album(file, albumid) elif mbid_validate(trackid): - file.lookup_trackid(trackid) + self.move_file_to_nat(file, trackid) elif self.config.setting['analyze_new_files']: self.analyze([file]) @@ -348,17 +339,16 @@ class Tagger(QtGui.QApplication): file.load(self._file_loaded) def process_directory_listing(self, root, queue, result=None, error=None): - delay = 10 try: # Read directory listing if result is not None and error is None: files = [] - directories = [] + directories = deque() try: for path in result: path = os.path.join(root, path) if os.path.isdir(path): - directories.append(path) + directories.appendleft(path) else: try: files.append(decode_filename(path)) @@ -368,25 +358,22 @@ class Tagger(QtGui.QApplication): finally: if files: self.add_files(files) - delay = min(25 * len(files), 500) - queue = directories + queue + queue.extendleft(directories) finally: # Scan next directory in the queue try: - path = queue.pop(0) + path = queue.popleft() except IndexError: pass else: - func = partial(self.other_queue.put, - (partial(os.listdir, path), - partial(self.process_directory_listing, - path, queue), - QtCore.Qt.LowEventPriority)) - QtCore.QTimer.singleShot(delay, func) + self.other_queue.put(( + partial(os.listdir, path), + partial(self.process_directory_listing, path, queue), + QtCore.Qt.LowEventPriority)) def add_directory(self, path): path = encode_filename(path) self.other_queue.put((partial(os.listdir, path), - partial(self.process_directory_listing, path, []), + partial(self.process_directory_listing, path, deque()), QtCore.Qt.LowEventPriority)) def get_file_by_id(self, id): @@ -496,13 +483,14 @@ class Tagger(QtGui.QApplication): for file in files: if self.files.has_key(file.filename): file.clear_lookup_task() - self.analyze_queue.remove(file.filename) + self._ofa.stop_analyze(file) del self.files[file.filename] file.remove(from_parent) def remove_album(self, album): """Remove the specified album.""" self.log.debug("Removing %r", album) + album.stop_loading() self.remove_files(self.get_files_from_objects([album])) self.albums.remove(album) self.emit(QtCore.SIGNAL("album_removed"), album) @@ -643,18 +631,6 @@ class Tagger(QtGui.QApplication): def num_pending_files(self): return len([file for file in self.files.values() if file.state == File.PENDING]) -def analyze_thread_run_item(thread, queue, filename): - next = partial(queue.ofa._lookup_fingerprint, queue.next, filename) - priority = QtCore.Qt.LowEventPriority + 1 - try: - result = queue.ofa.calculate_fingerprint(filename) - except: - import traceback - thread.log.error(traceback.format_exc()) - thread.to_main(next, priority, error=sys.exc_info()[1]) - else: - thread.to_main(next, priority, result=result) - def help(): print """Usage: %s [OPTIONS] [FILE] [FILE] ... diff --git a/picard/track.py b/picard/track.py index d96fdb965..1127b59cd 100644 --- a/picard/track.py +++ b/picard/track.py @@ -19,7 +19,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from PyQt4 import QtCore -from picard.metadata import Metadata, run_track_metadata_processors +from picard.metadata import Metadata from picard.dataobj import DataObject from picard.util import format_time, translate_artist, asciipunct, partial from picard.mbxml import recording_to_metadata @@ -42,6 +42,7 @@ class Track(DataObject): DataObject.__init__(self, id) self.album = album self.linked_files = [] + self.num_linked_files = 0 self.metadata = Metadata() def __repr__(self): @@ -50,6 +51,7 @@ class Track(DataObject): def add_file(self, file): if file not in self.linked_files: self.linked_files.append(file) + self.num_linked_files += 1 self.album._add_file(self, file) self.update_file_metadata(file) @@ -69,6 +71,7 @@ class Track(DataObject): if file not in self.linked_files: return self.linked_files.remove(file) + self.num_linked_files -= 1 file.metadata.copy(file.saved_metadata) self.album._remove_file(self, file) self.update() @@ -84,7 +87,7 @@ class Track(DataObject): yield file def is_linked(self): - return len(self.linked_files)>0 + return self.num_linked_files > 0 def can_save(self): """Return if this object can be saved.""" @@ -118,7 +121,7 @@ class Track(DataObject): return False def similarity(self): - if len(self.linked_files) == 1: + if self.num_linked_files == 1: return self.linked_files[0].similarity else: return 1 @@ -134,7 +137,7 @@ class Track(DataObject): else: return m[column], similarity - def _customize_metadata(self, node, release, script, parser, ignore_tags=None): + def _customize_metadata(self, ignore_tags=None): tm = self.metadata # 'Translate' artist name @@ -154,21 +157,6 @@ class Track(DataObject): if self.config.setting['convert_punctuation']: tm.apply_func(asciipunct) - # Track metadata plugins - try: - run_track_metadata_processors(self, tm, release, node) - except: - self.log.error(traceback.format_exc()) - - if script: - # Run TaggerScript - try: - parser.eval(script, tm) - except: - self.log.error(traceback.format_exc()) - # Strip leading/trailing whitespace - tm.strip_whitespace() - def _convert_folksonomy_tags_to_genre(self, ignore_tags): # Combine release and track tags tags = dict(self.folksonomy_tags) @@ -218,7 +206,18 @@ class NonAlbumTrack(Track): return super(NonAlbumTrack, self).column(column) def load(self): - self.tagger.xmlws.get_track_by_id(self.id, partial(self._recording_request_finished)) + inc = ["artist-credits"] + mblogin = False + if self.config.setting["folksonomy_tags"]: + if self.config.setting["only_my_tags"]: + mblogin = True + inc += ["user-tags"] + else: + inc += ["tags"] + if self.config.setting["enable_ratings"]: + mblogin = True + inc += ["user-ratings"] + self.tagger.xmlws.get_track_by_id(self.id, partial(self._recording_request_finished), inc, mblogin=mblogin) def _recording_request_finished(self, document, http, error): if error: @@ -239,7 +238,7 @@ class NonAlbumTrack(Track): parser = ScriptParser() else: script = parser = None - self._customize_metadata(recording, None, script, parser) + self._customize_metadata(recording) self.loaded = True if self.callback: self.callback() diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index 553693601..16c44c2bb 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -24,7 +24,7 @@ from picard.album import Album, NatAlbum from picard.cluster import Cluster, ClusterList, UnmatchedFiles from picard.file import File from picard.track import Track, NonAlbumTrack -from picard.util import encode_filename, icontheme, partial +from picard.util import encode_filename, icontheme, partial, webbrowser2 from picard.config import Option, TextOption from picard.plugin import ExtensionPoint from picard.const import RELEASE_COUNTRIES @@ -332,7 +332,7 @@ class BaseTreeView(QtGui.QTreeWidget): self.connect(self, QtCore.SIGNAL("doubleClicked(QModelIndex)"), self.activate_item) - def switch_release_version(self, album): + def _switch_release_version(self, album): index = self.sender().data().toInt()[0] album.switch_release_version(album.other_versions[index]) @@ -347,7 +347,7 @@ class BaseTreeView(QtGui.QTreeWidget): if isinstance(obj, Track): menu.addAction(self.window.edit_tags_action) plugin_actions = list(_track_actions) - if len(obj.linked_files) == 1: + if obj.num_linked_files == 1: plugin_actions.extend(_file_actions) if isinstance(obj, NonAlbumTrack): menu.addAction(self.window.refresh_action) @@ -369,31 +369,36 @@ class BaseTreeView(QtGui.QTreeWidget): menu.addAction(self.window.save_action) menu.addAction(self.window.remove_action) - if isinstance(obj, Album) and not isinstance(obj, NatAlbum): + if isinstance(obj, Album) and not isinstance(obj, NatAlbum) and obj.loaded: releases_menu = QtGui.QMenu(_("&Other versions"), menu) - self._switch_release_version = partial(self.switch_release_version, obj) - for i, version in enumerate(obj.other_versions): - name = [] - if "date" in version: - name.append(version["date"]) - if "country" in version: - try: name.append(RELEASE_COUNTRIES[version["country"]]) - except KeyError: name.append(version["country"]) - if "media" in version: - name.append(version["media"]) - version_name = " / ".join(name).replace('&', '&&') - action = releases_menu.addAction(version_name or _('[no release info]')) - action.setData(QtCore.QVariant(i)) - action.setCheckable(True) - if obj.id == version["mbid"]: - action.setChecked(True) - self.connect(action, QtCore.SIGNAL("triggered(bool)"), self._switch_release_version) - if releases_menu.isEmpty(): - text = _('No other versions') if obj.rgloaded else _('Loading...') - action = releases_menu.addAction(text) - action.setEnabled(False) menu.addSeparator() menu.addMenu(releases_menu) + loading = releases_menu.addAction(_('Loading...')) + loading.setEnabled(False) + + def _add_other_versions(): + releases_menu.removeAction(loading) + switch_release_version = partial(self._switch_release_version, obj) + actions = [] + for i, version in enumerate(obj.other_versions): + keys = ("date", "country", "labels", "catnums", "tracks", "format") + name = " / ".join([version[k] for k in keys if version[k]]).replace("&", "&&") + if name == version["tracks"]: + name = "%s / %s" % (_('[no release info]'), name) + action = releases_menu.addAction(name) + action.setData(QtCore.QVariant(i)) + action.setCheckable(True) + if obj.id == version["mbid"]: + action.setChecked(True) + self.connect(action, QtCore.SIGNAL("triggered(bool)"), switch_release_version) + + if not obj.rgloaded: + if obj.rgid: + self.connect(obj, QtCore.SIGNAL("release_group_loaded"), _add_other_versions) + kwargs = {"release-group": obj.rgid, "limit": 100} + self.tagger.xmlws.browse_releases(obj._release_group_request_finished, **kwargs) + else: + _add_other_versions() if plugin_actions: plugin_menu = QtGui.QMenu(_("&Plugins"), menu) @@ -438,7 +443,16 @@ class BaseTreeView(QtGui.QTreeWidget): def mimeTypes(self): """List of MIME types accepted by this view.""" - return ["text/uri-list", "application/picard.file-list", "application/picard.album-list"] + return ["text/uri-list", + "application/picard.file-list", + "application/picard.album-list"] + + def dragEnterEvent(self, event): + if event.mimeData().hasUrls(): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + else: + event.acceptProposedAction() def startDrag(self, supportedActions): """Start drag, *without* using pixmap.""" @@ -551,7 +565,7 @@ class BaseTreeView(QtGui.QTreeWidget): # application/picard.album-list albums = data.data("application/picard.album-list") if albums: - albums = [self.tagger.get_album_by_id(albumsId) for albumsId in str(albums).split("\n")] + albums = [self.tagger.load_album(id) for id in str(albums).split("\n")] self.drop_albums(albums, target) handled = True return handled @@ -625,7 +639,7 @@ class AlbumTreeView(BaseTreeView): except KeyError: self.log.debug("Item for %r not found", track) return - if len(track.linked_files) == 1: + if track.num_linked_files == 1: file = track.linked_files[0] color = self.track_colors[file.state] icon = self.panel.decide_file_icon(file) @@ -641,7 +655,7 @@ class AlbumTreeView(BaseTreeView): #Add linked files (there will either be 0 or >1) oldnum = item.childCount() - newnum = len(track.linked_files) + newnum = track.num_linked_files # remove old items if oldnum > newnum: for i in range(oldnum - newnum): @@ -735,3 +749,4 @@ class AlbumTreeView(BaseTreeView): self.panel.unregister_object(album) if album == self.tagger.nats: self.tagger.nats = None + diff --git a/picard/ui/mainwindow.py b/picard/ui/mainwindow.py index 765b056cd..653482101 100644 --- a/picard/ui/mainwindow.py +++ b/picard/ui/mainwindow.py @@ -251,8 +251,7 @@ class MainWindow(QtGui.QMainWindow): self.exit_action = QtGui.QAction(_(u"E&xit"), self) # TR: Keyboard shortcut for "Exit" self.exit_action.setShortcut(QtGui.QKeySequence(_(u"Ctrl+Q"))) - self.connect(self.exit_action, QtCore.SIGNAL("triggered()"), - self.close) + self.connect(self.exit_action, QtCore.SIGNAL("triggered()"), self.close) self.remove_action = QtGui.QAction(icontheme.lookup('list-remove'), _(u"&Remove"), self) self.remove_action.setStatusTip(_(u"Remove selected files/albums")) @@ -407,10 +406,8 @@ class MainWindow(QtGui.QMainWindow): self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon) else: self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) - self.cd_lookup_action.setEnabled(len(get_cdrom_drives()) > 0) - def create_toolbar(self): self.toolbar = toolbar = self.addToolBar(_(u"&Toolbar")) self.update_toolbar_style() @@ -663,14 +660,14 @@ class MainWindow(QtGui.QMainWindow): statusBar += _(" (Error: %s)") % obj.error file = obj elif isinstance(obj, Track): - if len(obj.linked_files) == 1: + if obj.num_linked_files == 1: file = obj.linked_files[0] orig_metadata = file.orig_metadata metadata = file.metadata statusBar = "%s (%d%%)" % (file.filename, file.similarity * 100) if file.state == file.ERROR: statusBar += _(" (Error: %s)") % file.error - elif len(obj.linked_files) == 0: + elif obj.num_linked_files == 0: metadata = obj.metadata else: metadata = obj.metadata diff --git a/picard/ui/options/plugins.py b/picard/ui/options/plugins.py index 35a813eda..8f00b662a 100644 --- a/picard/ui/options/plugins.py +++ b/picard/ui/options/plugins.py @@ -49,8 +49,9 @@ class PluginsOptionsPage(OptionsPage): self.ui.setupUi(self) self.items = {} self.connect(self.ui.plugins, QtCore.SIGNAL("itemSelectionChanged()"), self.change_details) - self.ui.plugins.__class__.mimeTypes = self.mimeTypes - self.ui.plugins.__class__.dropEvent = self.dropEvent + self.ui.plugins.mimeTypes = self.mimeTypes + self.ui.plugins.dropEvent = self.dropEvent + self.ui.plugins.dragEnterEvent = self.dragEnterEvent if sys.platform == "win32": self.loader="file:///%s" else: @@ -154,6 +155,10 @@ class PluginsOptionsPage(OptionsPage): def mimeTypes(self): return ["text/uri-list"] + def dragEnterEvent(self, event): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + def dropEvent(self, event): for path in [os.path.normpath(unicode(u.toLocalFile())) for u in event.mimeData().urls()]: self.install_plugin(path) diff --git a/picard/util/thread.py b/picard/util/thread.py index ca3768ac0..68a0e5293 100644 --- a/picard/util/thread.py +++ b/picard/util/thread.py @@ -18,6 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sys +import traceback from picard.util.queue import Queue from PyQt4 import QtCore @@ -36,54 +37,36 @@ class ProxyToMainEvent(QtCore.QEvent): class Thread(QtCore.QThread): - def __init__(self, parent, queues): + def __init__(self, parent, queue): QtCore.QThread.__init__(self, parent) - self.queues = queues + self.queue = queue self.stopping = False def stop(self): self.stopping = True - self.queues[0].put(None) - - def get_job(self): - for queue in self.queues: - if queue.qsize() > 0: - return (queue, queue.get()) - return (self.queues[0], self.queues[0].get()) + self.queue.put(None) def run(self): while not self.stopping: - queue, item = self.get_job() + item = self.queue.get() if item is None: continue - queue.run_item(self, queue, item) - self.usleep(100) - - def run_item(thread, item): - func, next, priority = item - try: - result = func() - except: - import traceback - self.log.error(traceback.format_exc()) - self.to_main(next, priority, error=sys.exc_info()[1]) - else: - self.to_main(next, priority, result=result) + self.run_item(item) + + def run_item(self, item): + func, next, priority = item + try: + result = func() + except: + self.log.error(traceback.format_exc()) + self.to_main(next, priority, error=sys.exc_info()[1]) + else: + self.to_main(next, priority, result=result) def to_main(self, func, priority, *args, **kwargs): event = ProxyToMainEvent(func, args, kwargs) QtCore.QCoreApplication.postEvent(self.parent(), event, priority) -def generic_run_item(thread, queue, item): - func, next, priority = item - try: - result = func() - except: - import traceback - thread.log.error(traceback.format_exc()) - thread.to_main(next, priority, error=sys.exc_info()[1]) - else: - thread.to_main(next, priority, result=result) class ThreadPool(QtCore.QObject): @@ -91,7 +74,7 @@ class ThreadPool(QtCore.QObject): def __init__(self, parent=None): QtCore.QObject.__init__(self, parent) - self.threads = [] + self.threads = [] ThreadPool.instance = self def start(self): @@ -99,23 +82,18 @@ class ThreadPool(QtCore.QObject): thread.start(QtCore.QThread.LowPriority) def stop(self): + queues = set() for thread in self.threads: thread.stop() - - # FIXME: if a queue is in more than 1 thread, unlock will be called - # more than once. - for thread in self.threads: - for queue in thread.queues: - queue.unlock() - #for thread in self.threads: - # self.log.debug("Waiting for %r", thread) - # thread.wait() + queues.add(thread.queue) + for queue in queues: + queue.unlock() def event(self, event): if isinstance(event, ProxyToMainEvent): - try: event.call() + try: + event.call() except: - import traceback self.log.error(traceback.format_exc()) return True return False diff --git a/picard/webservice.py b/picard/webservice.py index e55c71071..24d00220f 100644 --- a/picard/webservice.py +++ b/picard/webservice.py @@ -27,6 +27,7 @@ import os import sys import re import traceback +import time from collections import deque, defaultdict from PyQt4 import QtCore, QtNetwork, QtXml from picard import version_string @@ -145,7 +146,7 @@ class XmlWebService(QtCore.QObject): send = self._request_methods[method] reply = send(request, data) if data is not None else send(request) key = (host, port) - self._last_request_times[key] = QtCore.QTime.currentTime() + self._last_request_times[key] = time.time() self._active_requests[reply] = (request, handler, xml) return True @@ -209,10 +210,10 @@ class XmlWebService(QtCore.QObject): queue = self._high_priority_queues.get(key) or self._low_priority_queues.get(key) if not queue: continue - now = QtCore.QTime.currentTime() + now = time.time() last = self._last_request_times.get(key) request_delay = REQUEST_DELAY[key] - last_ms = last.msecsTo(now) if last is not None else request_delay + last_ms = (now - last) * 1000 if last is not None else request_delay if last_ms >= request_delay: self.log.debug("Last request to %s was %d ms ago, starting another one", key, last_ms) d = request_delay @@ -256,19 +257,15 @@ class XmlWebService(QtCore.QObject): def _get_by_id(self, entitytype, entityid, handler, inc=[], params=[], priority=False, important=False, mblogin=False): host = self.config.setting["server_host"] port = self.config.setting["server_port"] - path = "/ws/2/%s/%s?inc=%s&%s" % (entitytype, entityid, "+".join(inc), "&".join(params)) + path = "/ws/2/%s/%s?inc=%s" % (entitytype, entityid, "+".join(inc)) + if params: path += "&" + "&".join(params) return self.get(host, port, path, handler, priority=priority, important=important, mblogin=mblogin) - def get_release_group_by_id(self, releasegroupid, handler, priority=True, important=False): - inc = ['releases', 'media'] - return self._get_by_id('release-group', releasegroupid, handler, inc, priority=priority, important=important) - def get_release_by_id(self, releaseid, handler, inc=[], priority=True, important=False, mblogin=False): return self._get_by_id('release', releaseid, handler, inc, priority=priority, important=important, mblogin=mblogin) - def get_track_by_id(self, trackid, handler, priority=False, important=False): - inc = ['releases', 'release-groups', 'media', 'artist-credits'] - return self._get_by_id('recording', trackid, handler, inc, priority=priority, important=important) + def get_track_by_id(self, trackid, handler, inc=[], priority=True, important=False, mblogin=False): + return self._get_by_id('recording', trackid, handler, inc, priority=priority, important=important, mblogin=mblogin) def lookup_puid(self, puid, handler, priority=False, important=False): inc = ['releases', 'release-groups', 'media', 'artist-credits'] @@ -303,6 +300,17 @@ class XmlWebService(QtCore.QObject): def find_tracks(self, handler, **kwargs): return self._find('recording', handler, kwargs) + def _browse(self, entitytype, handler, kwargs, inc=[], priority=False, important=False): + host = self.config.setting["server_host"] + port = self.config.setting["server_port"] + params = "&".join(["%s=%s" % (k, v) for k, v in kwargs.items()]) + path = "/ws/2/%s?%s&inc=%s" % (entitytype, params, "+".join(inc)) + return self.get(host, port, path, handler, priority=priority, important=important) + + def browse_releases(self, handler, priority=True, important=True, **kwargs): + inc = ["media", "labels"] + return self._browse("release", handler, kwargs, inc, priority=priority, important=important) + def submit_puids(self, puids, handler): path = '/ws/2/recording/?client=' + USER_AGENT_STRING recordings = ''.join(['' % i for i in puids.items()]) @@ -328,3 +336,4 @@ class XmlWebService(QtCore.QObject): def download(self, host, port, path, handler, priority=False, important=False): return self.get(host, port, path, handler, xml=False, priority=priority, important=important) + diff --git a/setup.py b/setup.py index 4efc1ea34..ffb18e10a 100755 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ try: 'optimize' : 2, 'argv_emulation' : True, 'iconfile' : 'picard.icns', - 'frameworks' : ['libofa.0.dylib', 'libiconv.2.dylib', 'libdiscid.1.dylib'], + 'frameworks' : ['libofa.0.dylib', 'libiconv.2.dylib', 'libdiscid.0.dylib'], 'includes' : ['sip', 'PyQt4.Qt', 'picard.util.astrcmp', 'picard.musicdns.ofa', 'picard.musicdns.avcodec'], 'excludes' : ['pydoc'], 'plist' : { 'CFBundleName' : 'MusicBrainz Picard', diff --git a/test/test_mbxml.py b/test/test_mbxml.py index 66bce5f00..13fd84060 100644 --- a/test/test_mbxml.py +++ b/test/test_mbxml.py @@ -3,6 +3,13 @@ from picard.metadata import Metadata from picard.mbxml import track_to_metadata, release_to_metadata from picard.webservice import XmlNode +class config: + setting = { + "standardize_tracks": False, + "standardize_artists": False, + "standardize_releases": False + } + class XmlNode(object): def __init__(self, text=u'', children={}, attribs={}): @@ -50,7 +57,7 @@ class TrackTest(unittest.TestCase): }) track = Track() m = track.metadata = Metadata() - track_to_metadata(node, track) + track_to_metadata(node, track, config) self.failUnlessEqual('123', m['musicbrainz_trackid']) self.failUnlessEqual('456; 789', m['musicbrainz_artistid']) self.failUnlessEqual('Foo', m['title']) @@ -94,7 +101,7 @@ class ReleaseTest(unittest.TestCase): })] }) m = Metadata() - release_to_metadata(release, m) + release_to_metadata(release, m, config) self.failUnlessEqual('123', m['musicbrainz_albumid']) self.failUnlessEqual('456; 789', m['musicbrainz_artistid']) self.failUnlessEqual('456; 789', m['musicbrainz_albumartistid'])