mirror of
https://github.com/fergalmoran/picard.git
synced 2026-02-25 09:03:59 +00:00
Merge branch 'master' of github.com:musicbrainz/picard into mwiencek-master
This commit is contained in:
@@ -26,7 +26,7 @@ def utitle(string):
|
||||
for i in xrange(1, len(string)):
|
||||
s = string[i]
|
||||
# Special case apostrophe in the middle of a word.
|
||||
if u"'" == s and string[i-1].isalpha(): cap = False
|
||||
if s in u"’'" and string[i-1].isalpha(): cap = False
|
||||
elif iswbound(s): cap = True
|
||||
elif cap and s.isalpha():
|
||||
cap = False
|
||||
|
||||
@@ -94,18 +94,20 @@ class AcoustIDClient(QtCore.QObject):
|
||||
metadata_el = doc.append_child('metadata')
|
||||
acoustid_el = metadata_el.append_child('acoustid')
|
||||
recording_list_el = acoustid_el.append_child('recording_list')
|
||||
acoustid_id = None
|
||||
|
||||
results = document.response[0].results[0].children.get('result')
|
||||
if results:
|
||||
result = results[0]
|
||||
acoustid_id = result.id[0].text
|
||||
if 'recordings' in result.children:
|
||||
for recording in result.recordings[0].recording:
|
||||
parse_recording(recording)
|
||||
status = document.response[0].status[0].text
|
||||
if status == 'ok':
|
||||
results = document.response[0].results[0].children.get('result')
|
||||
if results:
|
||||
result = results[0]
|
||||
file.metadata['acoustid_id'] = result.id[0].text
|
||||
if 'recordings' in result.children:
|
||||
for recording in result.recordings[0].recording:
|
||||
parse_recording(recording)
|
||||
else:
|
||||
error_message = document.response[0].error[0].message[0].text
|
||||
self.log.error("Fingerprint lookup failed: %r", error_message)
|
||||
|
||||
if acoustid_id is not None:
|
||||
file.metadata['acoustid_id'] = acoustid_id
|
||||
next(doc, http, error)
|
||||
|
||||
def _lookup_fingerprint(self, next, filename, result=None, error=None):
|
||||
@@ -127,7 +129,7 @@ class AcoustIDClient(QtCore.QObject):
|
||||
file.acoustid_length = length
|
||||
self.tagger.acoustidmanager.add(file, None)
|
||||
params['fingerprint'] = fingerprint
|
||||
params['duration'] = str((file.metadata.length or 1000 * length) / 1000)
|
||||
params['duration'] = str(length)
|
||||
else:
|
||||
type, trackid = result
|
||||
params['trackid'] = trackid
|
||||
|
||||
@@ -32,6 +32,7 @@ from picard.script import ScriptParser
|
||||
from picard.ui.item import Item
|
||||
from picard.util import format_time, queue, mbid_validate, asciipunct
|
||||
from picard.cluster import Cluster
|
||||
from picard.collection import Collection, user_collections, load_user_collections
|
||||
from picard.mbxml import (
|
||||
release_group_to_metadata,
|
||||
release_to_metadata,
|
||||
@@ -117,6 +118,15 @@ class Album(DataObject, Item):
|
||||
|
||||
m['totaldiscs'] = release_node.medium_list[0].count
|
||||
|
||||
# Add album to collections
|
||||
if "collection_list" in release_node.children:
|
||||
for node in release_node.collection_list[0].collection:
|
||||
if node.editor[0].text.lower() == self.config.setting["username"].lower():
|
||||
if node.id not in user_collections:
|
||||
user_collections[node.id] = \
|
||||
Collection(node.id, node.name[0].text, node.release_list[0].count)
|
||||
user_collections[node.id].releases.add(self.id)
|
||||
|
||||
# Run album metadata plugins
|
||||
try:
|
||||
run_album_metadata_processors(self, m, release_node)
|
||||
@@ -270,7 +280,8 @@ class Album(DataObject, Item):
|
||||
self._new_tracks = []
|
||||
self._requests = 1
|
||||
require_authentication = False
|
||||
inc = ['release-groups', 'media', 'recordings', 'artist-credits', 'artists', 'aliases', 'labels', 'isrcs']
|
||||
inc = ['release-groups', 'media', 'recordings', 'artist-credits',
|
||||
'artists', 'aliases', 'labels', 'isrcs', 'collections']
|
||||
if self.config.setting['release_ars'] or self.config.setting['track_ars']:
|
||||
inc += ['artist-rels', 'release-rels', 'url-rels', 'recording-rels', 'work-rels']
|
||||
if self.config.setting['track_ars']:
|
||||
@@ -378,6 +389,9 @@ class Album(DataObject, Item):
|
||||
def can_refresh(self):
|
||||
return True
|
||||
|
||||
def can_view_info(self):
|
||||
return bool(self.loaded and self.metadata and self.metadata.images)
|
||||
|
||||
def is_album_like(self):
|
||||
return True
|
||||
|
||||
|
||||
@@ -162,7 +162,9 @@ class Cluster(QtCore.QObject, Item):
|
||||
self.tagger.move_files_to_album(self.files, match[1].id)
|
||||
|
||||
def lookup_metadata(self):
|
||||
""" Try to identify the cluster using the existing metadata. """
|
||||
"""Try to identify the cluster using the existing metadata."""
|
||||
if self.lookup_task:
|
||||
return
|
||||
self.tagger.window.set_statusbar_message(N_("Looking up the metadata for cluster %s..."), self.metadata['album'])
|
||||
self.lookup_task = self.tagger.xmlws.find_releases(self._lookup_finished,
|
||||
artist=self.metadata['albumartist'],
|
||||
@@ -278,6 +280,10 @@ class ClusterList(list, Item):
|
||||
def can_browser_lookup(self):
|
||||
return False
|
||||
|
||||
def lookup_metadata(self):
|
||||
for cluster in self:
|
||||
cluster.lookup_metadata()
|
||||
|
||||
|
||||
class ClusterDict(object):
|
||||
|
||||
|
||||
103
picard/collection.py
Normal file
103
picard/collection.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2013 Michael Wiencek
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from picard.util import partial
|
||||
|
||||
|
||||
user_collections = {}
|
||||
|
||||
|
||||
class Collection(QtCore.QObject):
|
||||
|
||||
def __init__(self, id, name, size):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.pending = set()
|
||||
self.size = int(size)
|
||||
self.releases = set()
|
||||
|
||||
def add_releases(self, ids, callback):
|
||||
ids = ids - self.pending
|
||||
if ids:
|
||||
self.pending.update(ids)
|
||||
self.tagger.xmlws.put_to_collection(self.id, list(ids),
|
||||
partial(self._add_finished, ids, callback))
|
||||
|
||||
def remove_releases(self, ids, callback):
|
||||
ids = ids - self.pending
|
||||
if ids:
|
||||
self.pending.update(ids)
|
||||
self.tagger.xmlws.delete_from_collection(self.id, list(ids),
|
||||
partial(self._remove_finished, ids, callback))
|
||||
|
||||
def _add_finished(self, ids, callback, document, reply, error):
|
||||
self.pending.difference_update(ids)
|
||||
if not error:
|
||||
count = len(ids)
|
||||
self.releases.update(ids)
|
||||
self.size += count
|
||||
callback()
|
||||
|
||||
self.tagger.window.set_statusbar_message(
|
||||
ungettext('Added %i release to collection "%s"',
|
||||
'Added %i releases to collection "%s"', count) % (count, self.name))
|
||||
|
||||
def _remove_finished(self, ids, callback, document, reply, error):
|
||||
self.pending.difference_update(ids)
|
||||
if not error:
|
||||
count = len(ids)
|
||||
self.releases.difference_update(ids)
|
||||
self.size -= count
|
||||
callback()
|
||||
|
||||
self.tagger.window.set_statusbar_message(
|
||||
ungettext('Removed %i release from collection "%s"',
|
||||
'Removed %i releases from collection "%s"', count) % (count, self.name))
|
||||
|
||||
|
||||
def load_user_collections(callback=None):
|
||||
tagger = QtCore.QObject.tagger
|
||||
|
||||
def request_finished(document, reply, error):
|
||||
if error:
|
||||
tagger.window.set_statusbar_message(_("Error loading collections: %s"), unicode(reply.errorString()))
|
||||
return
|
||||
collection_list = document.metadata[0].collection_list[0]
|
||||
if "collection" in collection_list.children:
|
||||
new_collections = set()
|
||||
|
||||
for node in collection_list.collection:
|
||||
new_collections.add(node.id)
|
||||
collection = user_collections.get(node.id)
|
||||
if collection is None:
|
||||
user_collections[node.id] = Collection(node.id, node.name[0].text, node.release_list[0].count)
|
||||
else:
|
||||
collection.name = node.name[0].text
|
||||
collection.size = int(node.release_list[0].count)
|
||||
|
||||
for id in set(user_collections.iterkeys()) - new_collections:
|
||||
del user_collections[id]
|
||||
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
setting = QtCore.QObject.config.setting
|
||||
if setting["username"] and setting["password"]:
|
||||
tagger.xmlws.get_collection_list(partial(request_finished))
|
||||
@@ -23,6 +23,7 @@ __builtin__.__dict__['N_'] = lambda a: a
|
||||
|
||||
# AcoustID client API key
|
||||
ACOUSTID_KEY = '0zClDiGo'
|
||||
ACOUSTID_HOST = 'api.acoustid.org'
|
||||
FPCALC_NAMES = ['fpcalc', 'pyfpcalc']
|
||||
|
||||
# Various Artists MBID
|
||||
|
||||
@@ -126,16 +126,16 @@ def _caa_json_downloaded(album, metadata, release, try_list, data, http, error):
|
||||
caa_types = QObject.config.setting["caa_image_types"].split()
|
||||
caa_types = map(unicode.lower, caa_types)
|
||||
for image in caa_data["images"]:
|
||||
if QObject.config.setting["caa_approved_only"] and not image["approved"]:
|
||||
continue
|
||||
if not image["types"] and 'unknown' in caa_types:
|
||||
_caa_append_image_to_trylist(try_list, image)
|
||||
imagetypes = map(unicode.lower, image["types"])
|
||||
for imagetype in imagetypes:
|
||||
if imagetype == "front":
|
||||
caa_front_found = True
|
||||
if imagetype in caa_types:
|
||||
if QObject.config.setting["caa_approved_only"]:
|
||||
if image["approved"]:
|
||||
_caa_append_image_to_trylist(try_list, image)
|
||||
else:
|
||||
_caa_append_image_to_trylist(try_list, image)
|
||||
_caa_append_image_to_trylist(try_list, image)
|
||||
break
|
||||
|
||||
if error or not caa_front_found:
|
||||
@@ -166,7 +166,40 @@ def coverart(album, metadata, release, try_list=None):
|
||||
# try_list will be None for the first call
|
||||
if try_list is None:
|
||||
try_list = []
|
||||
if QObject.config.setting['ca_provider_use_caa']:
|
||||
|
||||
# MB web service indicates if CAA has artwork
|
||||
# http://tickets.musicbrainz.org/browse/MBS-4536
|
||||
has_caa_artwork = False
|
||||
caa_types = map(unicode.lower,
|
||||
QObject.config.setting["caa_image_types"].split())
|
||||
|
||||
if 'cover_art_archive' in release.children:
|
||||
caa_node = release.children['cover_art_archive'][0]
|
||||
has_caa_artwork = bool(caa_node.artwork[0].text == 'true')
|
||||
|
||||
if len(caa_types) == 2 and ('front' in caa_types or 'back' in caa_types):
|
||||
# The OR cases are there to still download and process the CAA
|
||||
# JSON file if front or back is enabled but not in the CAA and
|
||||
# another type (that's neither front nor back) is enabled.
|
||||
# For example, if both front and booklet are enabled and the
|
||||
# CAA only has booklet images, the front element in the XML
|
||||
# from the webservice will be false (thus front_in_caa is False
|
||||
# as well) but it's still necessary to download the booklet
|
||||
# images by using the fact that back is enabled but there are
|
||||
# no back images in the CAA.
|
||||
front_in_caa = caa_node.front[0].text == 'true' or 'front' not in caa_types
|
||||
back_in_caa = caa_node.back[0].text == 'true' or 'back' not in caa_types
|
||||
has_caa_artwork = has_caa_artwork and (front_in_caa or back_in_caa)
|
||||
|
||||
elif len(caa_types) == 1 and ('front' in caa_types or 'back' in caa_types):
|
||||
front_in_caa = caa_node.front[0].text == 'true' and 'front' in caa_types
|
||||
back_in_caa = caa_node.back[0].text == 'true' and 'back' in caa_types
|
||||
has_caa_artwork = has_caa_artwork and (front_in_caa or back_in_caa)
|
||||
|
||||
if QObject.config.setting['ca_provider_use_caa'] and has_caa_artwork\
|
||||
and len(caa_types) > 0:
|
||||
QObject.log.debug("There are suitable images in the cover art archive for %s"
|
||||
% release.id)
|
||||
album._requests += 1
|
||||
album.tagger.xmlws.download(
|
||||
"coverartarchive.org", 80, "/release/%s/" %
|
||||
@@ -174,6 +207,8 @@ def coverart(album, metadata, release, try_list=None):
|
||||
partial(_caa_json_downloaded, album, metadata, release, try_list),
|
||||
priority=True, important=True)
|
||||
else:
|
||||
QObject.log.debug("There are no suitable images in the cover art archive for %s"
|
||||
% release.id)
|
||||
_fill_try_list(album, release, try_list)
|
||||
_walk_try_list(album, metadata, release, try_list)
|
||||
|
||||
@@ -248,7 +283,6 @@ def _process_asin_relation(try_list, relation):
|
||||
else:
|
||||
serverInfo = AMAZON_SERVER['amazon.com']
|
||||
host = serverInfo['server']
|
||||
print "HOST", host
|
||||
path_l = AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'L')
|
||||
path_m = AMAZON_IMAGE_PATH % (asin, serverInfo['id'], 'M')
|
||||
_try_list_append_image_url(try_list, QUrl("http://%s:%s" % (host, path_l)))
|
||||
|
||||
36
picard/coverartarchive.py
Normal file
36
picard/coverartarchive.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2013 Laurent Monin
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
# list of types from http://musicbrainz.org/doc/Cover_Art/Types
|
||||
# order of declaration is preserved in selection box
|
||||
CAA_TYPES = [
|
||||
{'name': "front", 'title': N_("Front")},
|
||||
{'name': "back", 'title': N_("Back")},
|
||||
{'name': "booklet", 'title': N_("Booklet")},
|
||||
{'name': "medium", 'title': N_("Medium")},
|
||||
{'name': "tray", 'title': N_("Tray")},
|
||||
{'name': "obi", 'title': N_("Obi")},
|
||||
{'name': "spine", 'title': N_("Spine")},
|
||||
{'name': "track", 'title': N_("Track")},
|
||||
{'name': "sticker", 'title': N_("Sticker")},
|
||||
{'name': "other", 'title': N_("Other")},
|
||||
{'name': "unknown", 'title': N_("Unknown")}, # pseudo type, used for the no type case
|
||||
]
|
||||
|
||||
CAA_TYPES_SEPARATOR = ' ' #separator to use when joining/splitting list of types
|
||||
@@ -127,18 +127,19 @@ class File(QtCore.QObject, Item):
|
||||
|
||||
def copy_metadata(self, metadata):
|
||||
acoustid = self.metadata["acoustid_id"]
|
||||
preserve = self.config.setting["preserved_tags"].strip()
|
||||
if preserve:
|
||||
saved_metadata = {}
|
||||
for tag in re.split(r"\s+", preserve):
|
||||
values = self.orig_metadata.getall(tag)
|
||||
if values:
|
||||
saved_metadata[tag] = values
|
||||
self.metadata.copy(metadata)
|
||||
for tag, values in saved_metadata.iteritems():
|
||||
self.metadata.set(tag, values)
|
||||
else:
|
||||
self.metadata.copy(metadata)
|
||||
preserve = self.config.setting["preserved_tags"].strip() + " ".join([
|
||||
"~bitrate", "~bits_per_sample", "~format", "~channels",
|
||||
"~filename", "~dirname", "~extension"])
|
||||
saved_metadata = {}
|
||||
|
||||
for tag in re.split(r"\s+", preserve):
|
||||
values = self.orig_metadata.getall(tag)
|
||||
if values:
|
||||
saved_metadata[tag] = values
|
||||
self.metadata.copy(metadata)
|
||||
for tag, values in saved_metadata.iteritems():
|
||||
self.metadata.set(tag, values)
|
||||
|
||||
self.metadata["acoustid_id"] = acoustid
|
||||
|
||||
def has_error(self):
|
||||
@@ -215,8 +216,8 @@ class File(QtCore.QObject, Item):
|
||||
self.base_filename = os.path.basename(new_filename)
|
||||
length = self.orig_metadata.length
|
||||
temp_info = {}
|
||||
for info in ('~#bitrate', '~#sample_rate', '~#channels',
|
||||
'~#bits_per_sample', '~format'):
|
||||
for info in ('~bitrate', '~sample_rate', '~channels',
|
||||
'~bits_per_sample', '~format'):
|
||||
temp_info[info] = self.orig_metadata[info]
|
||||
if self.config.setting["clear_existing_tags"]:
|
||||
self.orig_metadata.copy(self.metadata)
|
||||
@@ -248,13 +249,13 @@ class File(QtCore.QObject, Item):
|
||||
metadata[name] = sanitize_filename(metadata[name])
|
||||
format = format.replace("\t", "").replace("\n", "")
|
||||
filename = ScriptParser().eval(format, metadata, self)
|
||||
# replace incompatible characters
|
||||
if settings["windows_compatible_filenames"] or sys.platform == "win32":
|
||||
filename = replace_win32_incompat(filename)
|
||||
if settings["ascii_filenames"]:
|
||||
if isinstance(filename, unicode):
|
||||
filename = unaccent(filename)
|
||||
filename = replace_non_ascii(filename)
|
||||
# replace incompatible characters
|
||||
if settings["windows_compatible_filenames"] or sys.platform == "win32":
|
||||
filename = replace_win32_incompat(filename)
|
||||
# remove null characters
|
||||
filename = filename.replace("\x00", "")
|
||||
return filename
|
||||
@@ -462,13 +463,13 @@ class File(QtCore.QObject, Item):
|
||||
if hasattr(file.info, 'length'):
|
||||
metadata.length = int(file.info.length * 1000)
|
||||
if hasattr(file.info, 'bitrate') and file.info.bitrate:
|
||||
metadata['~#bitrate'] = file.info.bitrate / 1000.0
|
||||
metadata['~bitrate'] = file.info.bitrate / 1000.0
|
||||
if hasattr(file.info, 'sample_rate') and file.info.sample_rate:
|
||||
metadata['~#sample_rate'] = file.info.sample_rate
|
||||
metadata['~sample_rate'] = file.info.sample_rate
|
||||
if hasattr(file.info, 'channels') and file.info.channels:
|
||||
metadata['~#channels'] = file.info.channels
|
||||
metadata['~channels'] = file.info.channels
|
||||
if hasattr(file.info, 'bits_per_sample') and file.info.bits_per_sample:
|
||||
metadata['~#bits_per_sample'] = file.info.bits_per_sample
|
||||
metadata['~bits_per_sample'] = file.info.bits_per_sample
|
||||
metadata['~format'] = self.__class__.__name__.replace('File', '')
|
||||
self._add_path_to_metadata(metadata)
|
||||
|
||||
@@ -549,7 +550,9 @@ class File(QtCore.QObject, Item):
|
||||
self.tagger.move_file_to_nat(self, track.id, node=track)
|
||||
|
||||
def lookup_metadata(self):
|
||||
""" Try to identify the file using the existing metadata. """
|
||||
"""Try to identify the file using the existing metadata."""
|
||||
if self.lookup_task:
|
||||
return
|
||||
self.tagger.window.set_statusbar_message(N_("Looking up the metadata for file %s..."), self.filename)
|
||||
self.clear_lookup_task()
|
||||
metadata = self.metadata
|
||||
|
||||
@@ -30,9 +30,9 @@ class WAVFile(File):
|
||||
self.log.debug("Loading file %r", filename)
|
||||
f = wave.open(encode_filename(filename), "rb")
|
||||
metadata = Metadata()
|
||||
metadata['~#channels'] = f.getnchannels()
|
||||
metadata['~#bits_per_sample'] = f.getsampwidth() * 8
|
||||
metadata['~#sample_rate'] = f.getframerate()
|
||||
metadata['~channels'] = f.getnchannels()
|
||||
metadata['~bits_per_sample'] = f.getsampwidth() * 8
|
||||
metadata['~sample_rate'] = f.getframerate()
|
||||
metadata.length = 1000 * f.getnframes() / f.getframerate()
|
||||
metadata['~format'] = 'Microsoft WAVE'
|
||||
self._add_path_to_metadata(metadata)
|
||||
|
||||
@@ -312,7 +312,11 @@ def release_group_to_metadata(node, m, config, release_group=None):
|
||||
for name, nodes in node.children.iteritems():
|
||||
if not nodes:
|
||||
continue
|
||||
if name == 'first_release_date':
|
||||
if name == 'title':
|
||||
m['~releasegroup'] = nodes[0].text
|
||||
elif name == 'disambiguation':
|
||||
m['~releasegroupcomment'] = nodes[0].text
|
||||
elif name == 'first_release_date':
|
||||
m['originaldate'] = nodes[0].text
|
||||
elif name == 'tag_list':
|
||||
add_folksonomy_tags(nodes[0], release_group)
|
||||
|
||||
@@ -62,6 +62,7 @@ from picard.file import File
|
||||
from picard.formats import open as open_file
|
||||
from picard.track import Track, NonAlbumTrack
|
||||
from picard.releasegroup import ReleaseGroup
|
||||
from picard.collection import load_user_collections
|
||||
from picard.ui.mainwindow import MainWindow
|
||||
from picard.ui.itemviews import BaseTreeView
|
||||
from picard.plugin import PluginManager
|
||||
@@ -151,6 +152,8 @@ class Tagger(QtGui.QApplication):
|
||||
|
||||
self.xmlws = XmlWebService()
|
||||
|
||||
load_user_collections()
|
||||
|
||||
# Initialize fingerprinting
|
||||
self._acoustid = acoustid.AcoustIDClient()
|
||||
self._acoustid.init()
|
||||
@@ -557,7 +560,7 @@ class Tagger(QtGui.QApplication):
|
||||
|
||||
def autotag(self, objects):
|
||||
for obj in objects:
|
||||
if isinstance(obj, (File, Cluster)) and not obj.lookup_task:
|
||||
if obj.can_autotag():
|
||||
obj.lookup_metadata()
|
||||
|
||||
# =======================================================================
|
||||
|
||||
92
picard/ui/collectionmenu.py
Normal file
92
picard/ui/collectionmenu.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Picard, the next-generation MusicBrainz tagger
|
||||
# Copyright (C) 2013 Michael Wiencek
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from PyQt4 import QtCore, QtGui
|
||||
from picard.collection import user_collections, load_user_collections
|
||||
|
||||
|
||||
class CollectionMenu(QtGui.QMenu):
|
||||
|
||||
def __init__(self, albums, *args):
|
||||
QtGui.QMenu.__init__(self, *args)
|
||||
self.ids = set(a.id for a in albums)
|
||||
self.actions_by_id = {}
|
||||
self.separator = self.addSeparator()
|
||||
self.refresh_action = self.addAction(_("Refresh List"))
|
||||
self.update_collections()
|
||||
|
||||
def update_collections(self):
|
||||
for id, collection in user_collections.iteritems():
|
||||
action = self.actions_by_id.get(collection.id)
|
||||
if action:
|
||||
action.defaultWidget().updateText()
|
||||
else:
|
||||
action = QtGui.QWidgetAction(self)
|
||||
action.setDefaultWidget(CollectionCheckBox(self, collection))
|
||||
self.insertAction(self.separator, action)
|
||||
self.actions_by_id[collection.id] = action
|
||||
|
||||
for id, action in self.actions_by_id.items():
|
||||
if id not in user_collections:
|
||||
self.removeAction(action)
|
||||
del self.actions_by_id[id]
|
||||
|
||||
def refresh_list(self):
|
||||
self.refresh_action.setEnabled(False)
|
||||
load_user_collections(self.update_collections)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
# Not using self.refresh_action.triggered because it closes the menu
|
||||
if self.actionAt(event.pos()) == self.refresh_action and self.refresh_action.isEnabled():
|
||||
self.refresh_list()
|
||||
|
||||
|
||||
class CollectionCheckBox(QtGui.QCheckBox):
|
||||
|
||||
def __init__(self, menu, collection):
|
||||
self.menu = menu
|
||||
self.collection = collection
|
||||
QtGui.QCheckBox.__init__(self, self.label())
|
||||
|
||||
releases = collection.releases & menu.ids
|
||||
if len(releases) == len(menu.ids):
|
||||
self.setCheckState(QtCore.Qt.Checked)
|
||||
elif not releases:
|
||||
self.setCheckState(QtCore.Qt.Unchecked)
|
||||
else:
|
||||
self.setCheckState(QtCore.Qt.PartiallyChecked)
|
||||
|
||||
def nextCheckState(self):
|
||||
ids = self.menu.ids
|
||||
if ids & self.collection.pending:
|
||||
return
|
||||
diff = ids - self.collection.releases
|
||||
if diff:
|
||||
self.collection.add_releases(diff, self.updateText)
|
||||
self.setCheckState(QtCore.Qt.Checked)
|
||||
else:
|
||||
self.collection.remove_releases(ids & self.collection.releases, self.updateText)
|
||||
self.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
def updateText(self):
|
||||
self.setText(self.label())
|
||||
|
||||
def label(self):
|
||||
c = self.collection
|
||||
return ungettext("%s (%i release)", "%s (%i releases)", c.size) % (c.name, c.size)
|
||||
@@ -101,10 +101,15 @@ class CoverArtBox(QtGui.QGroupBox):
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.loadFromData(self.data["data"])
|
||||
if not pixmap.isNull():
|
||||
offx, offy, w, h = (1, 1, 121, 121)
|
||||
cover = QtGui.QPixmap(self.shadow)
|
||||
pixmap = pixmap.scaled(121, 121, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
pixmap = pixmap.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
|
||||
painter = QtGui.QPainter(cover)
|
||||
painter.drawPixmap(1, 1, pixmap)
|
||||
bgcolor = QtGui.QColor.fromRgb(0, 0, 0, 128)
|
||||
painter.fillRect(QtCore.QRectF(offx, offy, w, h), bgcolor)
|
||||
x = offx + (w - pixmap.width()) / 2
|
||||
y = offy + (h - pixmap.height()) / 2
|
||||
painter.drawPixmap(x, y, pixmap)
|
||||
painter.end()
|
||||
self.coverArt.setPixmap(cover)
|
||||
|
||||
@@ -151,7 +156,7 @@ class CoverArtBox(QtGui.QGroupBox):
|
||||
path = encode_filename(unicode(url.toLocalFile()))
|
||||
if os.path.exists(path):
|
||||
f = open(path, 'rb')
|
||||
mime = 'image/png' if '.png' in path.lower() else 'image/jpeg'
|
||||
mime = 'image/png' if path.lower().endswith('.png') else 'image/jpeg'
|
||||
data = f.read()
|
||||
f.close()
|
||||
self.load_remote_image(mime, data)
|
||||
|
||||
@@ -25,18 +25,50 @@ from picard.ui.ui_infodialog import Ui_InfoDialog
|
||||
|
||||
class InfoDialog(QtGui.QDialog):
|
||||
|
||||
def __init__(self, file, parent=None):
|
||||
def __init__(self, obj, parent=None):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.file = file
|
||||
self.obj = obj
|
||||
self.ui = Ui_InfoDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.buttonBox.accepted.connect(self.accept)
|
||||
self.ui.buttonBox.rejected.connect(self.reject)
|
||||
self.setWindowTitle(_("Info") + " - " + file.base_filename)
|
||||
self.load_info()
|
||||
self.setWindowTitle(_("Info"))
|
||||
self._display_tabs()
|
||||
|
||||
def load_info(self):
|
||||
file = self.file
|
||||
def _display_tabs(self):
|
||||
self._display_info_tab()
|
||||
self._display_artwork_tab()
|
||||
|
||||
def _display_artwork_tab(self):
|
||||
tab = self.ui.artwork_tab
|
||||
images = self.obj.metadata.images
|
||||
if not images:
|
||||
self.tab_hide(tab)
|
||||
return
|
||||
|
||||
for image in images:
|
||||
data = image["data"]
|
||||
item = QtGui.QListWidgetItem()
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.loadFromData(data)
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
item.setIcon(icon)
|
||||
self.ui.artwork_list.addItem(item)
|
||||
|
||||
def tab_hide(self, widget):
|
||||
tab = self.ui.tabWidget
|
||||
index = tab.indexOf(widget)
|
||||
tab.removeTab(index)
|
||||
|
||||
|
||||
class FileInfoDialog(InfoDialog):
|
||||
|
||||
def __init__(self, file, parent=None):
|
||||
InfoDialog.__init__(self, file, parent)
|
||||
self.setWindowTitle(_("Info") + " - " + file.base_filename)
|
||||
|
||||
def _display_info_tab(self):
|
||||
file = self.obj
|
||||
info = []
|
||||
info.append((_('Filename:'), file.filename))
|
||||
if '~format' in file.orig_metadata:
|
||||
@@ -54,14 +86,14 @@ class InfoDialog(QtGui.QDialog):
|
||||
pass
|
||||
if file.orig_metadata.length:
|
||||
info.append((_('Length:'), format_time(file.orig_metadata.length)))
|
||||
if '~#bitrate' in file.orig_metadata:
|
||||
info.append((_('Bitrate:'), '%s kbps' % file.orig_metadata['~#bitrate']))
|
||||
if '~#sample_rate' in file.orig_metadata:
|
||||
info.append((_('Sample rate:'), '%s Hz' % file.orig_metadata['~#sample_rate']))
|
||||
if '~#bits_per_sample' in file.orig_metadata:
|
||||
info.append((_('Bits per sample:'), str(file.orig_metadata['~#bits_per_sample'])))
|
||||
if '~#channels' in file.orig_metadata:
|
||||
ch = file.orig_metadata['~#channels']
|
||||
if '~bitrate' in file.orig_metadata:
|
||||
info.append((_('Bitrate:'), '%s kbps' % file.orig_metadata['~bitrate']))
|
||||
if '~sample_rate' in file.orig_metadata:
|
||||
info.append((_('Sample rate:'), '%s Hz' % file.orig_metadata['~sample_rate']))
|
||||
if '~bits_per_sample' in file.orig_metadata:
|
||||
info.append((_('Bits per sample:'), str(file.orig_metadata['~bits_per_sample'])))
|
||||
if '~channels' in file.orig_metadata:
|
||||
ch = file.orig_metadata['~channels']
|
||||
if ch == 1: ch = _('Mono')
|
||||
elif ch == 2: ch = _('Stereo')
|
||||
else: ch = str(ch)
|
||||
@@ -69,11 +101,13 @@ class InfoDialog(QtGui.QDialog):
|
||||
text = '<br/>'.join(map(lambda i: '<b>%s</b><br/>%s' % i, info))
|
||||
self.ui.info.setText(text)
|
||||
|
||||
for image in file.metadata.images:
|
||||
data = image["data"]
|
||||
item = QtGui.QListWidgetItem()
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.loadFromData(data)
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
item.setIcon(icon)
|
||||
self.ui.artwork_list.addItem(item)
|
||||
|
||||
class AlbumInfoDialog(InfoDialog):
|
||||
|
||||
def __init__(self, album, parent=None):
|
||||
InfoDialog.__init__(self, album, parent)
|
||||
self.setWindowTitle(_("Album Info"))
|
||||
|
||||
def _display_info_tab(self):
|
||||
tab = self.ui.info_tab
|
||||
self.tab_hide(tab)
|
||||
|
||||
@@ -28,6 +28,7 @@ from picard.util import encode_filename, icontheme, partial
|
||||
from picard.config import Option, TextOption
|
||||
from picard.plugin import ExtensionPoint
|
||||
from picard.ui.ratingwidget import RatingWidget
|
||||
from picard.ui.collectionmenu import CollectionMenu
|
||||
|
||||
|
||||
class BaseAction(QtGui.QAction):
|
||||
@@ -271,6 +272,8 @@ class BaseTreeView(QtGui.QTreeWidget):
|
||||
menu.addAction(self.window.analyze_action)
|
||||
plugin_actions = list(_file_actions)
|
||||
elif isinstance(obj, Album):
|
||||
if can_view_info:
|
||||
menu.addAction(self.window.view_info_action)
|
||||
menu.addAction(self.window.browser_lookup_action)
|
||||
menu.addSeparator()
|
||||
menu.addAction(self.window.refresh_action)
|
||||
@@ -279,24 +282,31 @@ class BaseTreeView(QtGui.QTreeWidget):
|
||||
menu.addAction(self.window.save_action)
|
||||
menu.addAction(self.window.remove_action)
|
||||
|
||||
bottom_separator = False
|
||||
|
||||
if isinstance(obj, Album) and not isinstance(obj, NatAlbum) and obj.loaded:
|
||||
releases_menu = QtGui.QMenu(_("&Other versions"), menu)
|
||||
menu.addSeparator()
|
||||
menu.addMenu(releases_menu)
|
||||
loading = releases_menu.addAction(_('Loading...'))
|
||||
loading.setEnabled(False)
|
||||
bottom_separator = True
|
||||
|
||||
def _add_other_versions():
|
||||
releases_menu.removeAction(loading)
|
||||
for version in obj.release_group.versions:
|
||||
action = releases_menu.addAction(version["name"])
|
||||
action.setCheckable(True)
|
||||
if obj.id == version["id"]:
|
||||
action.setChecked(True)
|
||||
action.triggered.connect(partial(obj.switch_release_version, version["id"]))
|
||||
if len(self.selectedIndexes()) == len(MainPanel.columns):
|
||||
def _add_other_versions():
|
||||
releases_menu.removeAction(loading)
|
||||
for version in obj.release_group.versions:
|
||||
action = releases_menu.addAction(version["name"])
|
||||
action.setCheckable(True)
|
||||
if obj.id == version["id"]:
|
||||
action.setChecked(True)
|
||||
action.triggered.connect(partial(obj.switch_release_version, version["id"]))
|
||||
|
||||
_add_other_versions() if obj.release_group.loaded else \
|
||||
obj.release_group.load_versions(_add_other_versions)
|
||||
_add_other_versions() if obj.release_group.loaded else \
|
||||
obj.release_group.load_versions(_add_other_versions)
|
||||
releases_menu.setEnabled(True)
|
||||
else:
|
||||
releases_menu.setEnabled(False)
|
||||
|
||||
if self.config.setting["enable_ratings"] and \
|
||||
len(self.window.selected_objects) == 1 and isinstance(obj, Track):
|
||||
@@ -306,6 +316,12 @@ class BaseTreeView(QtGui.QTreeWidget):
|
||||
menu.addAction(action)
|
||||
menu.addSeparator()
|
||||
|
||||
selected_albums = [a for a in self.window.selected_objects if type(a) == Album]
|
||||
if selected_albums:
|
||||
if not bottom_separator:
|
||||
menu.addSeparator()
|
||||
menu.addMenu(CollectionMenu(selected_albums, _("Collections"), menu))
|
||||
|
||||
if plugin_actions:
|
||||
plugin_menu = QtGui.QMenu(_("&Plugins"), menu)
|
||||
plugin_menu.setIcon(self.panel.icon_plugins)
|
||||
@@ -324,6 +340,7 @@ class BaseTreeView(QtGui.QTreeWidget):
|
||||
action_menu.addAction(action)
|
||||
|
||||
if isinstance(obj, Cluster) or isinstance(obj, ClusterList) or isinstance(obj, Album):
|
||||
menu.addSeparator()
|
||||
menu.addAction(self.expand_all_action)
|
||||
menu.addAction(self.collapse_all_action)
|
||||
|
||||
@@ -453,7 +470,9 @@ class BaseTreeView(QtGui.QTreeWidget):
|
||||
|
||||
def activate_item(self, index):
|
||||
obj = self.itemFromIndex(index).obj
|
||||
if obj.can_view_info():
|
||||
# Double-clicking albums should expand them. The album info can be
|
||||
# viewed by using the toolbar button.
|
||||
if not isinstance(obj, Album) and obj.can_view_info():
|
||||
self.window.view_info()
|
||||
|
||||
def add_cluster(self, cluster, parent_item=None):
|
||||
|
||||
@@ -24,6 +24,7 @@ import os.path
|
||||
|
||||
from picard.file import File
|
||||
from picard.track import Track
|
||||
from picard.album import Album
|
||||
from picard.config import Option, BoolOption, TextOption
|
||||
from picard.formats import supported_formats
|
||||
from picard.ui.coverartbox import CoverArtBox
|
||||
@@ -32,7 +33,7 @@ from picard.ui.metadatabox import MetadataBox
|
||||
from picard.ui.filebrowser import FileBrowser
|
||||
from picard.ui.tagsfromfilenames import TagsFromFileNamesDialog
|
||||
from picard.ui.options.dialog import OptionsDialog
|
||||
from picard.ui.infodialog import InfoDialog
|
||||
from picard.ui.infodialog import FileInfoDialog, AlbumInfoDialog
|
||||
from picard.ui.passworddialog import PasswordDialog
|
||||
from picard.util import icontheme, webbrowser2, find_existing_path
|
||||
from picard.util.cdrom import get_cdrom_drives
|
||||
@@ -676,8 +677,12 @@ been merged with that of single artist albums."""),
|
||||
return ret == QtGui.QMessageBox.Yes
|
||||
|
||||
def view_info(self):
|
||||
file = self.tagger.get_files_from_objects(self.selected_objects)[0]
|
||||
dialog = InfoDialog(file, self)
|
||||
if isinstance(self.selected_objects[0], Album):
|
||||
album = self.selected_objects[0]
|
||||
dialog = AlbumInfoDialog(album, self)
|
||||
else:
|
||||
file = self.tagger.get_files_from_objects(self.selected_objects)[0]
|
||||
dialog = FileInfoDialog(file, self)
|
||||
dialog.exec_()
|
||||
|
||||
def cluster(self):
|
||||
|
||||
@@ -21,6 +21,45 @@ from PyQt4 import QtCore, QtGui
|
||||
from picard.config import BoolOption, IntOption, TextOption
|
||||
from picard.ui.options import OptionsPage, register_options_page
|
||||
from picard.ui.ui_options_cover import Ui_CoverOptionsPage
|
||||
from picard.coverartarchive import CAA_TYPES, CAA_TYPES_SEPARATOR
|
||||
|
||||
|
||||
class CAATypesSelector(object):
|
||||
def __init__(self, widget, enabled_types=''):
|
||||
self.widget = widget
|
||||
self._enabled_types = enabled_types.split(CAA_TYPES_SEPARATOR)
|
||||
self._items = {}
|
||||
self._populate()
|
||||
|
||||
def _populate(self):
|
||||
for caa_type in CAA_TYPES:
|
||||
enabled = caa_type["name"] in self._enabled_types
|
||||
self._add_item(caa_type, enabled=enabled)
|
||||
|
||||
def _add_item(self, typ, enabled=False):
|
||||
item = QtGui.QListWidgetItem(self.widget)
|
||||
item.setText(typ['title'])
|
||||
tooltip = u"CAA: %(name)s" % typ
|
||||
item.setToolTip(tooltip)
|
||||
if enabled:
|
||||
state = QtCore.Qt.Checked
|
||||
else:
|
||||
state = QtCore.Qt.Unchecked
|
||||
item.setCheckState(state)
|
||||
self._items[item] = typ
|
||||
|
||||
def get_selected_types(self):
|
||||
types = []
|
||||
for item, typ in self._items.iteritems():
|
||||
if item.checkState() == QtCore.Qt.Checked:
|
||||
types.append(typ['name'])
|
||||
return types
|
||||
|
||||
def get_selected_types_as_string(self):
|
||||
return CAA_TYPES_SEPARATOR.join(self.get_selected_types())
|
||||
|
||||
def __str__(self):
|
||||
return self.get_selected_types_as_string()
|
||||
|
||||
|
||||
class CoverOptionsPage(OptionsPage):
|
||||
@@ -67,7 +106,10 @@ class CoverOptionsPage(OptionsPage):
|
||||
self.ui.gb_caa.setEnabled(self.config.setting["ca_provider_use_caa"])
|
||||
|
||||
self.ui.cb_image_size.setCurrentIndex(self.config.setting["caa_image_size"])
|
||||
self.ui.le_image_types.setText(self.config.setting["caa_image_types"])
|
||||
widget = self.ui.caa_types_selector_1
|
||||
self._selector = CAATypesSelector(widget, self.config.setting["caa_image_types"])
|
||||
self.config.setting["caa_image_types"] = \
|
||||
self._selector.get_selected_types_as_string()
|
||||
self.ui.cb_approved_only.setChecked(self.config.setting["caa_approved_only"])
|
||||
self.ui.cb_type_as_filename.setChecked(self.config.setting["caa_image_type_as_filename"])
|
||||
self.connect(self.ui.caprovider_caa, QtCore.SIGNAL("toggled(bool)"),
|
||||
@@ -88,7 +130,8 @@ class CoverOptionsPage(OptionsPage):
|
||||
self.ui.caprovider_whitelist.isChecked()
|
||||
self.config.setting["caa_image_size"] =\
|
||||
self.ui.cb_image_size.currentIndex()
|
||||
self.config.setting["caa_image_types"] = self.ui.le_image_types.text()
|
||||
self.config.setting["caa_image_types"] = \
|
||||
self._selector.get_selected_types_as_string()
|
||||
self.config.setting["caa_approved_only"] =\
|
||||
self.ui.cb_approved_only.isChecked()
|
||||
self.config.setting["caa_image_type_as_filename"] = \
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'ui/options_cover.ui'
|
||||
#
|
||||
# Created: Tue Nov 20 18:25:21 2012
|
||||
# by: PyQt4 UI code generator 4.9.5
|
||||
# Created: Tue Jan 22 12:56:46 2013
|
||||
# by: PyQt4 UI code generator 4.9.6
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -12,7 +12,16 @@ from PyQt4 import QtCore, QtGui
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
_fromUtf8 = lambda s: s
|
||||
def _fromUtf8(s):
|
||||
return s
|
||||
|
||||
try:
|
||||
_encoding = QtGui.QApplication.UnicodeUTF8
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig, _encoding)
|
||||
except AttributeError:
|
||||
def _translate(context, text, disambig):
|
||||
return QtGui.QApplication.translate(context, text, disambig)
|
||||
|
||||
class Ui_CoverOptionsPage(object):
|
||||
def setupUi(self, CoverOptionsPage):
|
||||
@@ -89,12 +98,22 @@ class Ui_CoverOptionsPage(object):
|
||||
self.label_2 = QtGui.QLabel(self.gb_caa)
|
||||
self.label_2.setObjectName(_fromUtf8("label_2"))
|
||||
self.verticalLayout_3.addWidget(self.label_2)
|
||||
self.le_image_types = QtGui.QLineEdit(self.gb_caa)
|
||||
self.le_image_types.setObjectName(_fromUtf8("le_image_types"))
|
||||
self.verticalLayout_3.addWidget(self.le_image_types)
|
||||
self.caa_types_help = QtGui.QLabel(self.gb_caa)
|
||||
self.caa_types_help.setObjectName(_fromUtf8("caa_types_help"))
|
||||
self.verticalLayout_3.addWidget(self.caa_types_help)
|
||||
self.caa_types_selector_1 = QtGui.QListWidget(self.gb_caa)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.caa_types_selector_1.sizePolicy().hasHeightForWidth())
|
||||
self.caa_types_selector_1.setSizePolicy(sizePolicy)
|
||||
self.caa_types_selector_1.setMaximumSize(QtCore.QSize(16777215, 80))
|
||||
self.caa_types_selector_1.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
self.caa_types_selector_1.setTabKeyNavigation(True)
|
||||
self.caa_types_selector_1.setProperty("showDropIndicator", False)
|
||||
self.caa_types_selector_1.setAlternatingRowColors(True)
|
||||
self.caa_types_selector_1.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
|
||||
self.caa_types_selector_1.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
self.caa_types_selector_1.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
|
||||
self.caa_types_selector_1.setObjectName(_fromUtf8("caa_types_selector_1"))
|
||||
self.verticalLayout_3.addWidget(self.caa_types_selector_1)
|
||||
self.cb_approved_only = QtGui.QCheckBox(self.gb_caa)
|
||||
self.cb_approved_only.setObjectName(_fromUtf8("cb_approved_only"))
|
||||
self.verticalLayout_3.addWidget(self.cb_approved_only)
|
||||
@@ -113,8 +132,18 @@ class Ui_CoverOptionsPage(object):
|
||||
self.retranslateUi(CoverOptionsPage)
|
||||
QtCore.QObject.connect(self.save_images_to_tags, QtCore.SIGNAL(_fromUtf8("clicked(bool)")), self.cb_embed_front_only.setEnabled)
|
||||
QtCore.QMetaObject.connectSlotsByName(CoverOptionsPage)
|
||||
CoverOptionsPage.setTabOrder(self.save_images_to_tags, self.save_images_to_files)
|
||||
CoverOptionsPage.setTabOrder(self.save_images_to_tags, self.cb_embed_front_only)
|
||||
CoverOptionsPage.setTabOrder(self.cb_embed_front_only, self.save_images_to_files)
|
||||
CoverOptionsPage.setTabOrder(self.save_images_to_files, self.cover_image_filename)
|
||||
CoverOptionsPage.setTabOrder(self.cover_image_filename, self.save_images_overwrite)
|
||||
CoverOptionsPage.setTabOrder(self.save_images_overwrite, self.caprovider_amazon)
|
||||
CoverOptionsPage.setTabOrder(self.caprovider_amazon, self.caprovider_cdbaby)
|
||||
CoverOptionsPage.setTabOrder(self.caprovider_cdbaby, self.caprovider_caa)
|
||||
CoverOptionsPage.setTabOrder(self.caprovider_caa, self.caprovider_whitelist)
|
||||
CoverOptionsPage.setTabOrder(self.caprovider_whitelist, self.cb_image_size)
|
||||
CoverOptionsPage.setTabOrder(self.cb_image_size, self.caa_types_selector_1)
|
||||
CoverOptionsPage.setTabOrder(self.caa_types_selector_1, self.cb_approved_only)
|
||||
CoverOptionsPage.setTabOrder(self.cb_approved_only, self.cb_type_as_filename)
|
||||
|
||||
def retranslateUi(self, CoverOptionsPage):
|
||||
self.rename_files.setTitle(_("Location"))
|
||||
@@ -134,7 +163,6 @@ class Ui_CoverOptionsPage(object):
|
||||
self.cb_image_size.setItemText(1, _("500 px"))
|
||||
self.cb_image_size.setItemText(2, _("Full size"))
|
||||
self.label_2.setText(_("Download only images of the following types:"))
|
||||
self.caa_types_help.setText(_("Types are separated by spaces, and are not case-sensitive."))
|
||||
self.cb_approved_only.setText(_("Download only approved images"))
|
||||
self.cb_type_as_filename.setText(_("Use the first image type as the filename. This will not change the filename of front images."))
|
||||
|
||||
|
||||
@@ -26,16 +26,18 @@ Asynchronous XML web service.
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import os.path
|
||||
from collections import deque, defaultdict
|
||||
from PyQt4 import QtCore, QtNetwork, QtXml
|
||||
from PyQt4.QtGui import QDesktopServices
|
||||
from PyQt4.QtCore import QUrl
|
||||
from picard import version_string
|
||||
from picard.util import partial
|
||||
from picard.const import ACOUSTID_KEY
|
||||
from picard.const import ACOUSTID_KEY, ACOUSTID_HOST
|
||||
|
||||
|
||||
REQUEST_DELAY = defaultdict(lambda: 1000)
|
||||
REQUEST_DELAY[('api.acoustid.org', 80)] = 333
|
||||
REQUEST_DELAY[(ACOUSTID_HOST, 80)] = 333
|
||||
REQUEST_DELAY[("coverartarchive.org", 80)] = 0
|
||||
USER_AGENT_STRING = 'MusicBrainz%%20Picard-%s' % version_string
|
||||
|
||||
@@ -111,6 +113,7 @@ class XmlWebService(QtCore.QObject):
|
||||
def __init__(self, parent=None):
|
||||
QtCore.QObject.__init__(self, parent)
|
||||
self.manager = QtNetwork.QNetworkAccessManager()
|
||||
self.set_cache()
|
||||
self.setup_proxy()
|
||||
self.manager.finished.connect(self._process_reply)
|
||||
self._last_request_times = {}
|
||||
@@ -128,6 +131,16 @@ class XmlWebService(QtCore.QObject):
|
||||
"DELETE": self.manager.deleteResource
|
||||
}
|
||||
|
||||
def set_cache(self, cache_size_in_mb=100):
|
||||
cache = QtNetwork.QNetworkDiskCache()
|
||||
location = QDesktopServices.storageLocation(QDesktopServices.CacheLocation)
|
||||
cache.setCacheDirectory(os.path.join(unicode(location), u'picard'))
|
||||
cache.setMaximumCacheSize(cache_size_in_mb * 1024 * 1024)
|
||||
self.manager.setCache(cache)
|
||||
self.log.debug("NetworkDiskCache dir: %s", cache.cacheDirectory())
|
||||
self.log.debug("NetworkDiskCache size: %s / %s", cache.cacheSize(),
|
||||
cache.maximumCacheSize())
|
||||
|
||||
def setup_proxy(self):
|
||||
self.proxy = QtNetwork.QNetworkProxy()
|
||||
if self.config.setting["use_proxy"]:
|
||||
@@ -138,13 +151,17 @@ class XmlWebService(QtCore.QObject):
|
||||
self.proxy.setPassword(self.config.setting["proxy_password"])
|
||||
self.manager.setProxy(self.proxy)
|
||||
|
||||
def _start_request(self, method, host, port, path, data, handler, xml, mblogin=False):
|
||||
def _start_request(self, method, host, port, path, data, handler, xml,
|
||||
mblogin=False, cacheloadcontrol=None):
|
||||
self.log.debug("%s http://%s:%d%s", method, host, port, path)
|
||||
url = QUrl.fromEncoded("http://%s:%d%s" % (host, port, path))
|
||||
if mblogin:
|
||||
url.setUserName(self.config.setting["username"])
|
||||
url.setPassword(self.config.setting["password"])
|
||||
request = QtNetwork.QNetworkRequest(url)
|
||||
if cacheloadcontrol is not None:
|
||||
request.setAttribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute,
|
||||
cacheloadcontrol)
|
||||
request.setRawHeader("User-Agent", "MusicBrainz-Picard/%s" % version_string)
|
||||
if data is not None:
|
||||
if method == "POST" and host == self.config.setting["server_host"]:
|
||||
@@ -176,10 +193,14 @@ class XmlWebService(QtCore.QObject):
|
||||
return
|
||||
error = int(reply.error())
|
||||
redirect = reply.attribute(QtNetwork.QNetworkRequest.RedirectionTargetAttribute).toUrl()
|
||||
self.log.debug("Received reply for %s: HTTP %d (%s)",
|
||||
fromCache = reply.attribute(QtNetwork.QNetworkRequest.SourceIsFromCacheAttribute).toBool()
|
||||
cached = ' (CACHED)' if fromCache else ''
|
||||
self.log.debug("Received reply for %s: HTTP %d (%s) %s",
|
||||
reply.request().url().toString(),
|
||||
reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute).toInt()[0],
|
||||
reply.attribute(QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute).toString())
|
||||
reply.attribute(QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute).toString(),
|
||||
cached
|
||||
)
|
||||
if handler is not None:
|
||||
if error:
|
||||
self.log.error("Network request error for %s: %s (QT code %d, HTTP code %d)",
|
||||
@@ -210,7 +231,8 @@ class XmlWebService(QtCore.QObject):
|
||||
redirect_port,
|
||||
# retain path, query string and anchors from redirect URL
|
||||
redirect.toString(QUrl.FormattingOption(QUrl.RemoveAuthority | QUrl.RemoveScheme)),
|
||||
handler, xml, priority=True, important=True)
|
||||
handler, xml, priority=True, important=True,
|
||||
cacheloadcontrol=request.attribute(QtNetwork.QNetworkRequest.CacheLoadControlAttribute))
|
||||
elif xml:
|
||||
xml_handler = XmlHandler()
|
||||
xml_handler.init()
|
||||
@@ -223,8 +245,10 @@ class XmlWebService(QtCore.QObject):
|
||||
handler(str(reply.readAll()), reply, error)
|
||||
reply.close()
|
||||
|
||||
def get(self, host, port, path, handler, xml=True, priority=False, important=False, mblogin=False):
|
||||
func = partial(self._start_request, "GET", host, port, path, None, handler, xml, mblogin)
|
||||
def get(self, host, port, path, handler, xml=True, priority=False,
|
||||
important=False, mblogin=False, cacheloadcontrol=None):
|
||||
func = partial(self._start_request, "GET", host, port, path, None,
|
||||
handler, xml, mblogin, cacheloadcontrol=cacheloadcontrol)
|
||||
return self.add_task(func, host, port, priority, important=important)
|
||||
|
||||
def post(self, host, port, path, data, handler, xml=True, priority=True, important=True, mblogin=True):
|
||||
@@ -369,7 +393,7 @@ class XmlWebService(QtCore.QObject):
|
||||
return '&'.join(filters)
|
||||
|
||||
def query_acoustid(self, handler, **args):
|
||||
host, port = 'api.acoustid.org', 80
|
||||
host, port = ACOUSTID_HOST, 80
|
||||
body = self._encode_acoustid_args(args)
|
||||
return self.post(host, port, '/v2/lookup', body, handler, mblogin=False)
|
||||
|
||||
@@ -381,10 +405,38 @@ class XmlWebService(QtCore.QObject):
|
||||
args['mbid.%d' % i] = str(submission.trackid)
|
||||
if submission.puid:
|
||||
args['puid.%d' % i] = str(submission.puid)
|
||||
host, port = 'api.acoustid.org', 80
|
||||
host, port = ACOUSTID_HOST, 80
|
||||
body = self._encode_acoustid_args(args)
|
||||
return self.post(host, port, '/v2/submit', body, handler, mblogin=False)
|
||||
|
||||
def download(self, host, port, path, handler, priority=False, important=False):
|
||||
return self.get(host, port, path, handler, xml=False, priority=priority, important=important)
|
||||
def download(self, host, port, path, handler, priority=False,
|
||||
important=False, cacheloadcontrol=None):
|
||||
return self.get(host, port, path, handler, xml=False, priority=priority,
|
||||
important=important, cacheloadcontrol=cacheloadcontrol)
|
||||
|
||||
def get_collection(self, id, handler, limit=100, offset=0):
|
||||
host, port = self.config.setting['server_host'], self.config.setting['server_port']
|
||||
path = "/ws/2/collection"
|
||||
if id is not None:
|
||||
inc = ["releases", "artist-credits", "media"]
|
||||
path += "/%s/releases?inc=%s&limit=%d&offset=%d" % (id, "+".join(inc), limit, offset)
|
||||
return self.get(host, port, path, handler, priority=True, important=True, mblogin=True)
|
||||
|
||||
def get_collection_list(self, handler):
|
||||
return self.get_collection(None, handler)
|
||||
|
||||
def _collection_request(self, id, releases):
|
||||
while releases:
|
||||
ids = ";".join(releases if len(releases) <= 400 else releases[:400])
|
||||
releases = releases[400:]
|
||||
yield "/ws/2/collection/%s/releases/%s?client=%s" % (id, ids, USER_AGENT_STRING)
|
||||
|
||||
def put_to_collection(self, id, releases, handler):
|
||||
host, port = self.config.setting['server_host'], self.config.setting['server_port']
|
||||
for path in self._collection_request(id, releases):
|
||||
self.put(host, port, path, "", handler)
|
||||
|
||||
def delete_from_collection(self, id, releases, handler):
|
||||
host, port = self.config.setting['server_host'], self.config.setting['server_port']
|
||||
for path in self._collection_request(id, releases):
|
||||
self.delete(host, port, path, handler)
|
||||
|
||||
15
setup.py
15
setup.py
@@ -250,9 +250,14 @@ class picard_build_ui(Command):
|
||||
|
||||
def run(self):
|
||||
from PyQt4 import uic
|
||||
_translate_re = re.compile(
|
||||
r'QtGui\.QApplication.translate\(.*?, (.*?), None, '
|
||||
r'QtGui\.QApplication\.UnicodeUTF8\)')
|
||||
_translate_re = (
|
||||
re.compile(
|
||||
r'QtGui\.QApplication.translate\(.*?, (.*?), None, '
|
||||
r'QtGui\.QApplication\.UnicodeUTF8\)'),
|
||||
re.compile(
|
||||
r'\b_translate\(.*?, (.*?), None\)')
|
||||
)
|
||||
|
||||
for uifile in glob.glob("ui/*.ui"):
|
||||
pyfile = "ui_%s.py" % os.path.splitext(os.path.basename(uifile))[0]
|
||||
pyfile = os.path.join("picard", "ui", pyfile)
|
||||
@@ -260,7 +265,9 @@ class picard_build_ui(Command):
|
||||
log.info("compiling %s -> %s", uifile, pyfile)
|
||||
tmp = StringIO()
|
||||
uic.compileUi(uifile, tmp)
|
||||
source = _translate_re.sub(r'_(\1)', tmp.getvalue())
|
||||
source = tmp.getvalue()
|
||||
for r in list(_translate_re):
|
||||
source = r.sub(r'_(\1)', source)
|
||||
f = open(pyfile, "w")
|
||||
f.write(source)
|
||||
f.close()
|
||||
|
||||
@@ -167,14 +167,41 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="le_image_types"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="caa_types_help">
|
||||
<property name="text">
|
||||
<string>Types are separated by spaces, and are not case-sensitive.</string>
|
||||
<widget class="QListWidget" name="caa_types_selector_1">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>80</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAsNeeded</enum>
|
||||
</property>
|
||||
<property name="tabKeyNavigation">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="verticalScrollMode">
|
||||
<enum>QAbstractItemView::ScrollPerPixel</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cb_approved_only">
|
||||
@@ -216,8 +243,18 @@
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>save_images_to_tags</tabstop>
|
||||
<tabstop>cb_embed_front_only</tabstop>
|
||||
<tabstop>save_images_to_files</tabstop>
|
||||
<tabstop>cover_image_filename</tabstop>
|
||||
<tabstop>save_images_overwrite</tabstop>
|
||||
<tabstop>caprovider_amazon</tabstop>
|
||||
<tabstop>caprovider_cdbaby</tabstop>
|
||||
<tabstop>caprovider_caa</tabstop>
|
||||
<tabstop>caprovider_whitelist</tabstop>
|
||||
<tabstop>cb_image_size</tabstop>
|
||||
<tabstop>caa_types_selector_1</tabstop>
|
||||
<tabstop>cb_approved_only</tabstop>
|
||||
<tabstop>cb_type_as_filename</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
|
||||
Reference in New Issue
Block a user