def _test_cover_art(self, filename): self._set_up(filename) try: # Use reasonable large data > 64kb. # This checks a mutagen error with ASF files. tests = { 'jpg': { 'mime': 'image/jpeg', 'data': self.jpegdata + "a" * 1024 * 128 }, 'png': { 'mime': 'image/png', 'data': self.pngdata + "a" * 1024 * 128 }, } for t in tests: f = picard.formats.open(self.filename) metadata = Metadata() imgdata = tests[t]['data'] metadata.append_image( CoverArtImage( data=imgdata ) ) f._save(self.filename, metadata) f = picard.formats.open(self.filename) loaded_metadata = f._load(self.filename) image = loaded_metadata.images[0] self.assertEqual(image.mimetype, tests[t]['mime']) self.assertEqual(image.data, imgdata) finally: self._tear_down()
def _test_cover_art(self, filename): self._set_up(filename) try: # Use reasonable large data > 64kb. # This checks a mutagen error with ASF files. tests = { 'jpg': { 'mime': 'image/jpeg', 'data': self.jpegdata + b"a" * 1024 * 128 }, 'png': { 'mime': 'image/png', 'data': self.pngdata + b"a" * 1024 * 128 }, } for t in tests: f = picard.formats.open_(self.filename) metadata = Metadata() imgdata = tests[t]['data'] metadata.append_image(CoverArtImage(data=imgdata)) f._save(self.filename, metadata) f = picard.formats.open_(self.filename) loaded_metadata = f._load(self.filename) image = loaded_metadata.images[0] self.assertEqual(image.mimetype, tests[t]['mime']) self.assertEqual(image.data, imgdata) finally: self._tear_down()
def _load(self, filename): log.debug("Loading file %r", filename) file = ASF(encode_filename(filename)) metadata = Metadata() for name, values in file.tags.items(): if name == 'WM/Picture': for image in values: (mime, data, type, description) = unpack_image(image.value) try: coverartimage = TagCoverArtImage( file=filename, tag=name, types=types_from_id3(type), comment=description, support_types=True, data=data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) continue elif name not in self.__RTRANS: continue elif name == 'WM/SharedUserRating': # Rating in WMA ranges from 0 to 99, normalize this to the range 0 to 5 values[0] = int(round(int(unicode(values[0])) / 99.0 * (config.setting['rating_steps'] - 1))) name = self.__RTRANS[name] values = filter(bool, map(unicode, values)) if values: metadata[name] = values self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) metadata = Metadata() if file.tags: for origname, values in file.tags.items(): if origname.lower().startswith( "cover art") and values.kind == mutagen.apev2.BINARY: if b'\0' in values.value: descr, data = values.value.split(b'\0', 1) try: coverartimage = TagCoverArtImage( file=filename, tag=origname, data=data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) # skip EXTERNAL and BINARY values if values.kind != mutagen.apev2.TEXT: continue for value in values: name = origname if name == "Year": name = "date" value = sanitize_date(value) elif name == "Track": name = "tracknumber" track = value.split("/") if len(track) > 1: metadata["totaltracks"] = track[1] value = track[0] elif name == "Disc": name = "discnumber" disc = value.split("/") if len(disc) > 1: metadata["totaldiscs"] = disc[1] value = disc[0] elif name == 'Performer' or name == 'Comment': name = name.lower() + ':' if value.endswith(')'): start = value.rfind(' (') if start > 0: name += value[start + 2:-1] value = value[:start] elif name in self.__translate: name = self.__translate[name] else: name = name.lower() metadata.add(name, value) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) metadata = Metadata() if file.tags: for origname, values in file.tags.items(): if origname.lower().startswith("cover art") and values.kind == mutagen.apev2.BINARY: if '\0' in values.value: descr, data = values.value.split('\0', 1) try: coverartimage = TagCoverArtImage( file=filename, tag=origname, data=data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) # skip EXTERNAL and BINARY values if values.kind != mutagen.apev2.TEXT: continue for value in values: name = origname if name == "Year": name = "date" value = sanitize_date(value) elif name == "Track": name = "tracknumber" track = value.split("/") if len(track) > 1: metadata["totaltracks"] = track[1] value = track[0] elif name == "Disc": name = "discnumber" disc = value.split("/") if len(disc) > 1: metadata["totaldiscs"] = disc[1] value = disc[0] elif name == 'Performer' or name == 'Comment': name = name.lower() + ':' if value.endswith(')'): start = value.rfind(' (') if start > 0: name += value[start + 2:-1] value = value[:start] elif name in self.__translate: name = self.__translate[name] else: name = name.lower() metadata.add(name, value) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = MP4(encode_filename(filename)) tags = file.tags if tags is None: file.add_tags() metadata = Metadata() for name, values in tags.items(): if name in self.__text_tags: for value in values: metadata.add(self.__text_tags[name], value) elif name in self.__bool_tags: metadata.add(self.__bool_tags[name], values and '1' or '0') elif name in self.__int_tags: for value in values: metadata.add(self.__int_tags[name], unicode(value)) elif name in self.__freeform_tags: for value in values: value = value.strip("\x00").decode("utf-8", "replace") metadata.add(self.__freeform_tags[name], value) elif name == "----:com.apple.iTunes:fingerprint": for value in values: value = value.strip("\x00").decode("utf-8", "replace") if value.startswith("MusicMagic Fingerprint"): metadata.add("musicip_fingerprint", value[22:]) elif name == "trkn": metadata["tracknumber"] = str(values[0][0]) metadata["totaltracks"] = str(values[0][1]) elif name == "disk": metadata["discnumber"] = str(values[0][0]) metadata["totaldiscs"] = str(values[0][1]) elif name == "covr": for value in values: if value.imageformat not in (value.FORMAT_JPEG, value.FORMAT_PNG): continue try: coverartimage = TagCoverArtImage( file=filename, tag=name, data=value, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = MP4(encode_filename(filename)) tags = file.tags if tags is None: file.add_tags() metadata = Metadata() for name, values in tags.items(): if name in self.__text_tags: for value in values: metadata.add(self.__text_tags[name], value) elif name in self.__bool_tags: metadata.add(self.__bool_tags[name], values and '1' or '0') elif name in self.__int_tags: for value in values: metadata.add(self.__int_tags[name], string_(value)) elif name in self.__freeform_tags: for value in values: value = value.decode("utf-8", "replace").strip("\x00") metadata.add(self.__freeform_tags[name], value) elif name == "----:com.apple.iTunes:fingerprint": for value in values: value = value.decode("utf-8", "replace").strip("\x00") if value.startswith("MusicMagic Fingerprint"): metadata.add("musicip_fingerprint", value[22:]) elif name == "trkn": metadata["tracknumber"] = string_(values[0][0]) metadata["totaltracks"] = string_(values[0][1]) elif name == "disk": metadata["discnumber"] = string_(values[0][0]) metadata["totaldiscs"] = string_(values[0][1]) elif name == "covr": for value in values: if value.imageformat not in (value.FORMAT_JPEG, value.FORMAT_PNG): continue try: coverartimage = TagCoverArtImage( file=filename, tag=name, data=value, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = ASF(encode_filename(filename)) metadata = Metadata() for name, values in file.tags.items(): if name == 'WM/Picture': for image in values: (mime, data, type_, description) = unpack_image(image.value) try: coverartimage = TagCoverArtImage( file=filename, tag=name, types=types_from_id3(type_), comment=description, support_types=True, data=data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) continue elif name not in self.__RTRANS: continue elif name == 'WM/SharedUserRating': # Rating in WMA ranges from 0 to 99, normalize this to the range 0 to 5 values[0] = int( round( int(string_(values[0])) / 99.0 * (config.setting['rating_steps'] - 1))) elif name == 'WM/PartOfSet': disc = string_(values[0]).split("/") if len(disc) > 1: metadata["totaldiscs"] = disc[1] values[0] = disc[0] name = self.__RTRANS[name] values = [string_(value) for value in values if value] if values: metadata[name] = values self._info(metadata, file) return metadata
def _test_cover_art(self, filename): self._set_up(filename) try: source_types = ["front", "booklet"] # Use reasonable large data > 64kb. # This checks a mutagen error with ASF files. tests = [ CoverArtImage(data=self.jpegdata + b"a" * 1024 * 128, types=source_types), CoverArtImage(data=self.pngdata + b"a" * 1024 * 128, types=source_types), ] for test in tests: f = picard.formats.open_(self.filename) metadata = Metadata() metadata.append_image(test) f._save(self.filename, metadata) f = picard.formats.open_(self.filename) loaded_metadata = f._load(self.filename) image = loaded_metadata.images[0] self.assertEqual(test.mimetype, image.mimetype) self.assertEqual(test, image) finally: self._tear_down()
def _cover_metadata(self): imgdata = self.jpegdata metadata = Metadata() metadata.append_image( TagCoverArtImage(file="a", tag="a", data=imgdata + "a", support_types=True, types=[u"booklet", u"front"]) ) metadata.append_image( TagCoverArtImage(file="b", tag="b", data=imgdata + "b", support_types=True, types=[u"back"]) ) metadata.append_image( TagCoverArtImage(file="c", tag="c", data=imgdata + "c", support_types=True, types=[u"front"]) ) metadata.append_image(TagCoverArtImage(file="d", tag="d", data=imgdata + "d")) metadata.append_image(TagCoverArtImage(file="e", tag="e", data=imgdata + "e", is_front=False)) metadata.append_image(TagCoverArtImage(file="f", tag="f", data=imgdata + "f", types=[u"front"])) metadata.append_image(TagCoverArtImage(file="g", tag="g", data=imgdata + "g", types=[u"back"], is_front=True)) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._get_file(encode_filename(filename)) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T') or frameid == 'GRP1': for text in frame.text: if text: metadata.add(name, string_(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), string_(text)) else: metadata.add(name, string_(frame)) elif frameid == 'TIT1': itunes_compatible = config.setting[ 'itunes_compatible_grouping'] name = 'work' if itunes_compatible else 'grouping' for text in frame.text: if text: metadata.add(name, string_(text)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self._tipl_roles and name: metadata.add(self._tipl_roles[role], name) else: metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': name = frame.desc if name in self.__translate_freetext: name = self.__translate_freetext[name] elif ((name in self.__rtranslate) != (name in self.__rtranslate_freetext)): # If the desc of a TXXX frame conflicts with the name of a # Picard tag, load it into ~id3:TXXX:desc rather than desc. # # This basically performs an XOR, making sure that 'name' # is in __rtranslate or __rtranslate_freetext, but not # both. (Being in both implies we support reading it both # ways.) Currently, the only tag in both is license. name = '~id3:TXXX:' + name for text in frame.text: metadata.add(name, string_(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += ':%s' % frame.desc metadata.add(name, string_(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_recordingid'] = frame.data.decode( 'ascii', 'ignore') elif frameid in self.__tag_re_parse.keys(): m = self.__tag_re_parse[frameid].search(frame.text[0]) if m: for name, value in m.groupdict().items(): if value is not None: metadata[name] = value else: log.error("Invalid %s value '%s' dropped in %r", frameid, frame.text[0], filename) elif frameid == 'APIC': try: coverartimage = TagCoverArtImage( file=filename, tag=frameid, types=types_from_id3(frame.type), comment=frame.desc, support_types=True, data=frame.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting['rating_user_email']: rating = string_( int( round(frame.rating / 255.0 * (config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: sanitized = sanitize_date(metadata.getall('date')[0]) if sanitized: metadata['date'] = sanitized self._info(metadata, file) return metadata
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) file.tags = file.tags or {} metadata = Metadata() for origname, values in file.tags.items(): for value in values: name = origname if name == "date" or name == "originaldate": # YYYY-00-00 => YYYY value = sanitize_date(value) elif name == 'performer' or name == 'comment': # transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr" name += ':' if value.endswith(')'): start = len(value) - 2 count = 1 while count > 0 and start > 0: if value[start] == ')': count += 1 elif value[start] == '(': count -= 1 start -= 1 if start > 0: name += value[start + 2:-1] value = value[:start] elif name.startswith('rating'): try: name, email = name.split(':', 1) except ValueError: email = '' if email != config.setting['rating_user_email']: continue name = '~rating' value = string_( int( round((float(value) * (config.setting['rating_steps'] - 1))))) elif name == "fingerprint" and value.startswith( "MusicMagic Fingerprint"): name = "musicip_fingerprint" value = value[22:] elif name == "tracktotal": if "totaltracks" in file.tags: continue name = "totaltracks" elif name == "disctotal": if "totaldiscs" in file.tags: continue name = "totaldiscs" elif name == "metadata_block_picture": image = mutagen.flac.Picture( base64.standard_b64decode(value)) try: coverartimage = TagCoverArtImage( file=filename, tag=name, types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) continue elif name in self.__translate: name = self.__translate[name] metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: try: coverartimage = TagCoverArtImage( file=filename, tag='FLAC/PICTURE', types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) # Read the unofficial COVERART tags, for backward compatibillity only if "metadata_block_picture" not in file.tags: try: for data in file["COVERART"]: try: coverartimage = TagCoverArtImage( file=filename, tag='COVERART', data=base64.standard_b64decode(data)) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) except KeyError: pass self._info(metadata, file) return metadata
def _cover_metadata(self): imgdata = self.jpegdata metadata = Metadata() metadata.append_image( TagCoverArtImage( file='a', tag='a', data=imgdata + 'a', support_types=True, types=[u'booklet', u'front'], ) ) metadata.append_image( TagCoverArtImage( file='b', tag='b', data=imgdata + 'b', support_types=True, types=[u'back'], ) ) metadata.append_image( TagCoverArtImage( file='c', tag='c', data=imgdata + 'c', support_types=True, types=[u'front'], ) ) metadata.append_image( TagCoverArtImage( file='d', tag='d', data=imgdata + 'd', ) ) metadata.append_image( TagCoverArtImage( file='e', tag='e', data=imgdata + 'e', is_front=False ) ) metadata.append_image( TagCoverArtImage( file='f', tag='f', data=imgdata + 'f', types=[u'front'] ) ) metadata.append_image( TagCoverArtImage( file='g', tag='g', data=imgdata + 'g', types=[u'back'], is_front=True ) ) return metadata
def _file_save_image(filename, image): f = picard.formats.open_(filename) metadata = Metadata() metadata.append_image(image) f._save(filename, metadata)
def _cover_metadata(self): imgdata = self.jpegdata metadata = Metadata() metadata.append_image( TagCoverArtImage( file='a', tag='a', data=imgdata + b'a', support_types=True, types=[u'booklet', u'front'], )) metadata.append_image( TagCoverArtImage( file='b', tag='b', data=imgdata + b'b', support_types=True, types=[u'back'], )) metadata.append_image( TagCoverArtImage( file='c', tag='c', data=imgdata + b'c', support_types=True, types=[u'front'], )) metadata.append_image( TagCoverArtImage( file='d', tag='d', data=imgdata + b'd', )) metadata.append_image( TagCoverArtImage(file='e', tag='e', data=imgdata + b'e', is_front=False)) metadata.append_image( TagCoverArtImage(file='f', tag='f', data=imgdata + b'f', types=[u'front'])) metadata.append_image( TagCoverArtImage(file='g', tag='g', data=imgdata + b'g', types=[u'back'], is_front=True)) return metadata
class Cluster(QtCore.QObject, Item): # Weights for different elements when comparing a cluster to a release comparison_weights = { 'album': 17, 'albumartist': 6, 'totaltracks': 5, 'releasecountry': 2, 'format': 2, } def __init__(self, name, artist="", special=False, related_album=None, hide_if_empty=False): QtCore.QObject.__init__(self) self.item = None self.metadata = Metadata() self.metadata['album'] = name self.metadata['albumartist'] = artist self.metadata['totaltracks'] = 0 self.special = special self.hide_if_empty = hide_if_empty self.related_album = related_album self.files = [] self.lookup_task = None def __repr__(self): if self.related_album: return '<Cluster %s %r>' % (self.related_album.id, self.related_album.metadata[u"album"] + '/' + self.metadata['album']) return '<Cluster %r>' % self.metadata['album'] def __len__(self): return len(self.files) def _update_related_album(self): if self.related_album: self.related_album.update_metadata_images() self.related_album.update() def add_files(self, files): for file in files: self.metadata.length += file.metadata.length file._move(self) file.update(signal=False) cover = file.metadata.get_single_front_image() if cover and cover[0] not in self.metadata.images: self.metadata.append_image(cover[0]) self.files.extend(files) self.metadata['totaltracks'] = len(self.files) self.item.add_files(files) self._update_related_album() def add_file(self, file): self.metadata.length += file.metadata.length self.files.append(file) self.metadata['totaltracks'] = len(self.files) file._move(self) file.update(signal=False) cover = file.metadata.get_single_front_image() if cover and cover[0] not in self.metadata.images: self.metadata.append_image(cover[0]) self.item.add_file(file) self._update_related_album() def remove_file(self, file): self.metadata.length -= file.metadata.length self.files.remove(file) self.metadata['totaltracks'] = len(self.files) self.item.remove_file(file) if not self.special and self.get_num_files() == 0: self.tagger.remove_cluster(self) self.update_metadata_images() self._update_related_album() def update(self): if self.item: self.item.update() def get_num_files(self): return len(self.files) def iterfiles(self, save=False): for file in self.files: yield file def can_save(self): """Return if this object can be saved.""" if self.files: return True else: return False def can_remove(self): """Return if this object can be removed.""" return not self.special def can_edit_tags(self): """Return if this object supports tag editing.""" return True def can_analyze(self): """Return if this object can be fingerprinted.""" return any([_file.can_analyze() for _file in self.files]) def can_autotag(self): return True def can_refresh(self): return False def can_browser_lookup(self): return not self.special def can_view_info(self): if self.files: return True else: return False def is_album_like(self): return True def column(self, column): if column == 'title': return '%s (%d)' % (self.metadata['album'], len(self.files)) elif (column == '~length' and self.special) or column == 'album': return '' elif column == '~length': return format_time(self.metadata.length) elif column == 'artist': return self.metadata['albumartist'] return self.metadata[column] def _lookup_finished(self, document, http, error): self.lookup_task = None try: releases = document['releases'] except (KeyError, TypeError): releases = None mparms = {'album': self.metadata['album']} # no matches if not releases: self.tagger.window.set_statusbar_message( N_("No matching releases for cluster %(album)s"), mparms, timeout=3000) return # multiple matches -- calculate similarities to each of them match = sorted((self.metadata.compare_to_release( release, Cluster.comparison_weights) for release in releases), reverse=True, key=itemgetter(0))[0] if match[0] < config.setting['cluster_lookup_threshold']: self.tagger.window.set_statusbar_message( N_("No matching releases for cluster %(album)s"), mparms, timeout=3000) return self.tagger.window.set_statusbar_message( N_("Cluster %(album)s identified!"), mparms, timeout=3000) self.tagger.move_files_to_album(self.files, match[1]['id']) def lookup_metadata(self): """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 %(album)s..."), {'album': self.metadata['album']}) self.lookup_task = self.tagger.mb_api.find_releases( self._lookup_finished, artist=self.metadata['albumartist'], release=self.metadata['album'], tracks=string_(len(self.files)), limit=QUERY_LIMIT) def clear_lookup_task(self): if self.lookup_task: self.tagger.webservice.remove_task(self.lookup_task) self.lookup_task = None @staticmethod def cluster(files, threshold): artistDict = ClusterDict() albumDict = ClusterDict() tracks = [] for file in files: artist = file.metadata["albumartist"] or file.metadata["artist"] album = file.metadata["album"] # Improve clustering from directory structure if no existing tags # Only used for grouping and to provide cluster title / artist - not added to file tags. filename = file.filename if config.setting[ "windows_compatibility"] or sys.platform == "win32": filename = ntpath.splitdrive(filename)[1] album, artist = album_artist_from_path(filename, album, artist) # For each track, record the index of the artist and album within the clusters tracks.append((artistDict.add(artist), albumDict.add(album))) artist_cluster_engine = ClusterEngine(artistDict) artist_cluster_engine.cluster(threshold) album_cluster_engine = ClusterEngine(albumDict) album_cluster_engine.cluster(threshold) # Arrange tracks into albums albums = {} for i, track in enumerate(tracks): cluster = album_cluster_engine.getClusterFromId(track[1]) if cluster is not None: albums.setdefault(cluster, []).append(i) # Now determine the most prominent names in the cluster and build the # final cluster list for album_id, album in albums.items(): album_name = album_cluster_engine.getClusterTitle(album_id) artist_max = 0 artist_id = None artist_hist = {} for track_id in album: cluster = artist_cluster_engine.getClusterFromId( tracks[track_id][0]) if cluster is not None: cnt = artist_hist.get(cluster, 0) + 1 if cnt > artist_max: artist_max = cnt artist_id = cluster artist_hist[cluster] = cnt if artist_id is None: artist_name = "Various Artists" else: artist_name = artist_cluster_engine.getClusterTitle(artist_id) yield album_name, artist_name, (files[i] for i in album) def update_metadata_images(self): update_metadata_images(self)
def _load(self, filename): log.debug("Loading file %r", filename) file = self._get_file(encode_filename(filename)) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T'): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": # If file is ID3v2.3, TIPL tag could contain TMCL # so we will test for TMCL values and add to TIPL if not TMCL for role, name in frame.people: if role in self._tipl_roles and name: metadata.add(self._tipl_roles[role], name) else: metadata.add('performer:%s' % role, name) elif frameid == 'TXXX': name = frame.desc if name in self.__translate_freetext: name = self.__translate_freetext[name] elif ((name in self.__rtranslate) != (name in self.__rtranslate_freetext)): # If the desc of a TXXX frame conflicts with the name of a # Picard tag, load it into ~id3:TXXX:desc rather than desc. # # This basically performs an XOR, making sure that 'name' # is in __rtranslate or __rtranslate_freetext, but not # both. (Being in both implies we support reading it both # ways.) Currently, the only tag in both is license. name = '~id3:TXXX:' + name for text in frame.text: metadata.add(name, unicode(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += ':%s' % frame.desc metadata.add(name, unicode(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_recordingid'] = frame.data.decode('ascii', 'ignore') elif frameid in self.__tag_re_parse.keys(): m = self.__tag_re_parse[frameid].search(frame.text[0]) if m: for name, value in m.groupdict().iteritems(): if value is not None: metadata[name] = value else: log.error("Invalid %s value '%s' dropped in %r", frameid, frame.text[0], filename) elif frameid == 'APIC': try: coverartimage = TagCoverArtImage( file=filename, tag=frameid, types=types_from_id3(frame.type), comment=frame.desc, support_types=True, data=frame.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == config.setting['rating_user_email']: rating = unicode(int(round(frame.rating / 255.0 * (config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: sanitized = sanitize_date(metadata.getall('date')[0]) if sanitized: metadata['date'] = sanitized self._info(metadata, file) return metadata
def _load(self, filename): self.log.debug("Loading file %r", filename) file = self._File(encode_filename(filename), ID3=compatid3_dsf.CompatID3) tags = file.tags or {} # upgrade custom 2.3 frames to 2.4 for old, new in self.__upgrade.items(): if old in tags and new not in tags: f = tags.pop(old) tags.add(getattr(id3, new)(encoding=f.encoding, text=f.text)) metadata = Metadata() for frame in tags.values(): frameid = frame.FrameID if frameid in self.__translate: name = self.__translate[frameid] if frameid.startswith('T'): for text in frame.text: if text: metadata.add(name, unicode(text)) elif frameid == 'COMM': for text in frame.text: if text: metadata.add('%s:%s' % (name, frame.desc), unicode(text)) else: metadata.add(name, unicode(frame)) elif frameid == "TMCL": for role, name in frame.people: if role or name: metadata.add('performer:%s' % role, name) elif frameid == "TIPL": for role, name in frame.people: if role in self.__tipl_roles and name: metadata.add(self.__tipl_roles[role], name) elif frameid == 'TXXX': if frame.desc in self.__translate_freetext: name = self.__translate_freetext[frame.desc] else: name = str(frame.desc.lower()) for text in frame.text: metadata.add(name, unicode(text)) elif frameid == 'USLT': name = 'lyrics' if frame.desc: name += ':%s' % frame.desc metadata.add(name, unicode(frame.text)) elif frameid == 'UFID' and frame.owner == 'http://musicbrainz.org': metadata['musicbrainz_trackid'] = unicode(frame.data) elif frameid == 'TRCK': value = frame.text[0].split('/') if len(value) > 1: metadata['tracknumber'], metadata['totaltracks'] = value[:2] else: metadata['tracknumber'] = value[0] elif frameid == 'TPOS': value = frame.text[0].split('/') if len(value) > 1: metadata['discnumber'], metadata['totaldiscs'] = value[:2] else: metadata['discnumber'] = value[0] elif frameid == 'APIC': coverartimage = TagCoverArtImage(file=filename, tag=frameid, data=frame.data) metadata.append_image(coverartimage) elif frameid == 'POPM': # Rating in ID3 ranges from 0 to 255, normalize this to the range 0 to 5 if frame.email == self.config.setting['rating_user_email']: rating = unicode(int(round(frame.rating / 255.0 * (self.config.setting['rating_steps'] - 1)))) metadata.add('~rating', rating) if 'date' in metadata: metadata['date'] = sanitize_date(metadata.getall('date')[0]) self._info(metadata, file) return metadata
class Cluster(QtCore.QObject, Item): # Weights for different elements when comparing a cluster to a release comparison_weights = { 'album': 17, 'albumartist': 6, 'totaltracks': 5, 'releasecountry': 2, 'format': 2, } def __init__(self, name, artist="", special=False, related_album=None, hide_if_empty=False): QtCore.QObject.__init__(self) self.item = None self.metadata = Metadata() self.metadata['album'] = name self.metadata['albumartist'] = artist self.metadata['totaltracks'] = 0 self.special = special self.hide_if_empty = hide_if_empty self.related_album = related_album self.files = [] self.lookup_task = None def __repr__(self): return '<Cluster %r>' % self.metadata['album'] def __len__(self): return len(self.files) def _update_related_album(self): if self.related_album: self.related_album.update_metadata_images() self.related_album.update() def add_files(self, files): for file in files: self.metadata.length += file.metadata.length file._move(self) file.update(signal=False) cover = file.metadata.get_single_front_image() if cover and cover[0] not in self.metadata.images: self.metadata.append_image(cover[0]) self.files.extend(files) self.metadata['totaltracks'] = len(self.files) self.item.add_files(files) self._update_related_album() def add_file(self, file): self.metadata.length += file.metadata.length self.files.append(file) self.metadata['totaltracks'] = len(self.files) file._move(self) file.update(signal=False) cover = file.metadata.get_single_front_image() if cover and cover[0] not in self.metadata.images: self.metadata.append_image(cover[0]) self.item.add_file(file) self._update_related_album() def remove_file(self, file): self.metadata.length -= file.metadata.length self.files.remove(file) self.metadata['totaltracks'] = len(self.files) self.item.remove_file(file) if not self.special and self.get_num_files() == 0: self.tagger.remove_cluster(self) self.update_metadata_images() self._update_related_album() def update(self): if self.item: self.item.update() def get_num_files(self): return len(self.files) def iterfiles(self, save=False): for file in self.files: yield file def can_save(self): """Return if this object can be saved.""" if self.files: return True else: return False def can_remove(self): """Return if this object can be removed.""" return not self.special def can_edit_tags(self): """Return if this object supports tag editing.""" return True def can_analyze(self): """Return if this object can be fingerprinted.""" return any([_file.can_analyze() for _file in self.files]) def can_autotag(self): return True def can_refresh(self): return False def can_browser_lookup(self): return not self.special def can_view_info(self): if self.files: return True else: return False def is_album_like(self): return True def column(self, column): if column == 'title': return '%s (%d)' % (self.metadata['album'], len(self.files)) elif (column == '~length' and self.special) or column == 'album': return '' elif column == '~length': return format_time(self.metadata.length) elif column == 'artist': return self.metadata['albumartist'] return self.metadata[column] def _lookup_finished(self, document, http, error): self.lookup_task = None try: releases = document.metadata[0].release_list[0].release except (AttributeError, IndexError): releases = None mparms = { 'album': self.metadata['album'] } # no matches if not releases: self.tagger.window.set_statusbar_message( N_("No matching releases for cluster %(album)s"), mparms, timeout=3000 ) return # multiple matches -- calculate similarities to each of them match = sorted((self.metadata.compare_to_release( release, Cluster.comparison_weights) for release in releases), reverse=True, key=itemgetter(0))[0] if match[0] < config.setting['cluster_lookup_threshold']: self.tagger.window.set_statusbar_message( N_("No matching releases for cluster %(album)s"), mparms, timeout=3000 ) return self.tagger.window.set_statusbar_message( N_("Cluster %(album)s identified!"), mparms, timeout=3000 ) self.tagger.move_files_to_album(self.files, match[1].id) def lookup_metadata(self): """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 %(album)s..."), {'album': self.metadata['album']} ) self.lookup_task = self.tagger.xmlws.find_releases(self._lookup_finished, artist=self.metadata['albumartist'], release=self.metadata['album'], tracks=string_(len(self.files)), limit=QUERY_LIMIT) def clear_lookup_task(self): if self.lookup_task: self.tagger.xmlws.remove_task(self.lookup_task) self.lookup_task = None @staticmethod def cluster(files, threshold): artistDict = ClusterDict() albumDict = ClusterDict() tracks = [] for file in files: artist = file.metadata["albumartist"] or file.metadata["artist"] album = file.metadata["album"] # Improve clustering from directory structure if no existing tags # Only used for grouping and to provide cluster title / artist - not added to file tags. filename = file.filename if config.setting["windows_compatibility"] or sys.platform == "win32": filename = ntpath.splitdrive(filename)[1] album, artist = album_artist_from_path(filename, album, artist) # For each track, record the index of the artist and album within the clusters tracks.append((artistDict.add(artist), albumDict.add(album))) artist_cluster_engine = ClusterEngine(artistDict) artist_cluster_engine.cluster(threshold) album_cluster_engine = ClusterEngine(albumDict) album_cluster_engine.cluster(threshold) # Arrange tracks into albums albums = {} for i, track in enumerate(tracks): cluster = album_cluster_engine.getClusterFromId(track[1]) if cluster is not None: albums.setdefault(cluster, []).append(i) # Now determine the most prominent names in the cluster and build the # final cluster list for album_id, album in albums.items(): album_name = album_cluster_engine.getClusterTitle(album_id) artist_max = 0 artist_id = None artist_hist = {} for track_id in album: cluster = artist_cluster_engine.getClusterFromId( tracks[track_id][0]) if cluster is not None: cnt = artist_hist.get(cluster, 0) + 1 if cnt > artist_max: artist_max = cnt artist_id = cluster artist_hist[cluster] = cnt if artist_id is None: artist_name = "Various Artists" else: artist_name = artist_cluster_engine.getClusterTitle(artist_id) yield album_name, artist_name, (files[i] for i in album) def update_metadata_images(self): update_metadata_images(self)
def _load(self, filename): log.debug("Loading file %r", filename) file = self._File(encode_filename(filename)) file.tags = file.tags or {} metadata = Metadata() for origname, values in file.tags.items(): for value in values: name = origname if name == "date" or name == "originaldate": # YYYY-00-00 => YYYY value = sanitize_date(value) elif name == 'performer' or name == 'comment': # transform "performer=Joe Barr (Piano)" to "performer:Piano=Joe Barr" name += ':' if value.endswith(')'): start = len(value) - 2 count = 1 while count > 0 and start > 0: if value[start] == ')': count += 1 elif value[start] == '(': count -= 1 start -= 1 if start > 0: name += value[start + 2:-1] value = value[:start] elif name.startswith('rating'): try: name, email = name.split(':', 1) except ValueError: email = '' if email != config.setting['rating_user_email']: continue name = '~rating' value = unicode(int(round((float(value) * (config.setting['rating_steps'] - 1))))) elif name == "fingerprint" and value.startswith("MusicMagic Fingerprint"): name = "musicip_fingerprint" value = value[22:] elif name == "tracktotal": if "totaltracks" in file.tags: continue name = "totaltracks" elif name == "disctotal": if "totaldiscs" in file.tags: continue name = "totaldiscs" elif name == "metadata_block_picture": image = mutagen.flac.Picture(base64.standard_b64decode(value)) try: coverartimage = TagCoverArtImage( file=filename, tag=name, types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) continue elif name in self.__translate: name = self.__translate[name] metadata.add(name, value) if self._File == mutagen.flac.FLAC: for image in file.pictures: try: coverartimage = TagCoverArtImage( file=filename, tag='FLAC/PICTURE', types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) # Read the unofficial COVERART tags, for backward compatibillity only if not "metadata_block_picture" in file.tags: try: for data in file["COVERART"]: try: coverartimage = TagCoverArtImage( file=filename, tag='COVERART', data=base64.standard_b64decode(data) ) except CoverArtImageError as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.append_image(coverartimage) except KeyError: pass self._info(metadata, file) return metadata