mirror of
https://github.com/fergalmoran/picard.git
synced 2026-02-23 08:06:46 +00:00
Merge from trunk
This commit is contained in:
17
NEWS.txt
17
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.
|
||||
|
||||
104
contrib/plugins/no_release.py
Normal file
104
contrib/plugins/no_release.py
Normal file
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
215
picard/album.py
215
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()
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
135
picard/mbxml.py
135
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] ...
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(['<recording id="%s"><puid-list><puid id="%s"/></puid-list></recording>' % 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)
|
||||
|
||||
|
||||
2
setup.py
2
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',
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user