def test_cmd_inmulti(self): context = Metadata() # Test with single-value string context["foo"] = "First:A; Second:B; Third:C" # Tests with $in for comparison purposes self.assertScriptResultEquals("$in(%foo%,Second:B)", "1", context) self.assertScriptResultEquals("$in(%foo%,irst:A; Second:B; Thi)", "1", context) self.assertScriptResultEquals("$in(%foo%,First:A; Second:B; Third:C)", "1", context) # Base $inmulti tests self.assertScriptResultEquals("$inmulti(%foo%,Second:B)", "", context) self.assertScriptResultEquals("$inmulti(%foo%,irst:A; Second:B; Thi)", "", context) self.assertScriptResultEquals( "$inmulti(%foo%,First:A; Second:B; Third:C)", "1", context) # Test separator override but with existing separator - results should be same as base self.assertScriptResultEquals("$inmulti(%foo%,Second:B,; )", "", context) self.assertScriptResultEquals( "$inmulti(%foo%,irst:A; Second:B; Thi,; )", "", context) self.assertScriptResultEquals( "$inmulti(%foo%,First:A; Second:B; Third:C,; )", "1", context) # Test separator override self.assertScriptResultEquals("$inmulti(%foo%,First:A,:)", "", context) self.assertScriptResultEquals("$inmulti(%foo%,Second:B,:)", "", context) self.assertScriptResultEquals("$inmulti(%foo%,Third:C,:)", "", context) self.assertScriptResultEquals("$inmulti(%foo%,First,:)", "1", context) self.assertScriptResultEquals("$inmulti(%foo%,A; Second,:)", "1", context) self.assertScriptResultEquals("$inmulti(%foo%,B; Third,:)", "1", context) self.assertScriptResultEquals("$inmulti(%foo%,C,:)", "1", context) # Test with multi-values context["foo"] = ["First:A", "Second:B", "Third:C"] # Tests with $in for comparison purposes self.assertScriptResultEquals("$in(%foo%,Second:B)", "1", context) self.assertScriptResultEquals("$in(%foo%,irst:A; Second:B; Thi)", "1", context) self.assertScriptResultEquals("$in(%foo%,First:A; Second:B; Third:C)", "1", context) # Base $inmulti tests self.assertScriptResultEquals("$inmulti(%foo%,Second:B)", "1", context) self.assertScriptResultEquals("$inmulti(%foo%,irst:A; Second:B; Thi)", "", context) self.assertScriptResultEquals( "$inmulti(%foo%,First:A; Second:B; Third:C)", "", context) # Test separator override but with existing separator - results should be same as base self.assertScriptResultEquals("$inmulti(%foo%,Second:B,; )", "1", context) self.assertScriptResultEquals( "$inmulti(%foo%,irst:A; Second:B; Thi,; )", "", context) self.assertScriptResultEquals( "$inmulti(%foo%,First:A; Second:B; Third:C,; )", "", context) # Test separator override self.assertScriptResultEquals("$inmulti(%foo%,First:A,:)", "", context) self.assertScriptResultEquals("$inmulti(%foo%,Second:B,:)", "", context) self.assertScriptResultEquals("$inmulti(%foo%,Third:C,:)", "", context) self.assertScriptResultEquals("$inmulti(%foo%,First,:)", "1", context) self.assertScriptResultEquals("$inmulti(%foo%,A; Second,:)", "1", context) self.assertScriptResultEquals("$inmulti(%foo%,B; Third,:)", "1", context) self.assertScriptResultEquals("$inmulti(%foo%,C,:)", "1", context)
def test_artist(self): m = Metadata() artist_to_metadata(self.json_doc, m) self.assertEqual(m, {})
def test_can_open_and_save(self): metadata = save_and_load_metadata(self.filename, Metadata()) self.assertTrue(metadata['~format'])
def test_recording(self): m = Metadata() t = Track("1") recording_to_metadata(self.json_doc, m, t) self.assertEqual(m, {})
def test_track(self): m = Metadata() medium_to_metadata(self.json_doc, m) self.assertEqual(m['discnumber'], '1') self.assertEqual(m['media'], '12" Vinyl') self.assertEqual(m['totaltracks'], '10')
def _load(self, filename): log.debug("Loading file %r", filename) config = get_config() 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 != sanitize_key( config.setting['rating_user_email']): continue name = '~rating' try: value = str( round((float(value) * (config.setting['rating_steps'] - 1)))) except ValueError: log.warning('Invalid rating value in %r: %s', filename, value) 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": try: image = mutagen.flac.Picture( base64.standard_b64decode(value)) coverartimage = TagCoverArtImage( file=filename, tag=name, types=types_from_id3(image.type), comment=image.desc, support_types=True, data=image.data, ) except (CoverArtImageError, TypeError, ValueError, mutagen.flac.error) as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.images.append(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.images.append(coverartimage) # Read the unofficial COVERART tags, for backward compatibility 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, TypeError, ValueError) as e: log.error('Cannot load image from %r: %s' % (filename, e)) else: metadata.images.append(coverartimage) except KeyError: pass self._info(metadata, file) return metadata
def _finalize_loading(self, error): if error: self.metadata.clear() self.status = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() totalalbumtracks = 0 absolutetracknumber = 0 va = self._new_metadata['musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node.medium_list[0].medium: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) totalalbumtracks += int(mm["totaltracks"]) for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if "pregap" in medium_node.children: track = self._finalize_loading_track(medium_node.pregap[0], mm, artists, va, absolutetracknumber) track.metadata['~pregap'] = "1" for track_node in medium_node.track_list[0].track: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber) if "data_track_list" in medium_node.children: for track_node in medium_node.data_track_list[0].track: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber) track.metadata['~datatrack'] = "1" totalalbumtracks = str(totalalbumtracks) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node self._tracks_loaded = True if not self._requests: # Prepare parser for user's script if config.setting["enable_tagger_script"]: script = config.setting["tagger_script"] if script: parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(script, track.metadata) except: self.error_append(traceback.format_exc()) # Strip leading/trailing whitespace track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(script, self._new_metadata) except: self.error_append(traceback.format_exc()) self._new_metadata.strip_whitespace() for track in self.tracks: for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.status = None self.match_files(self.unmatched_files.files) self.update() self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { 'id': self.id, 'artist': self.metadata['albumartist'], 'album': self.metadata['album'] }, timeout=3000 ) for func in self._after_load_callbacks: func() self._after_load_callbacks = []
def test_preserve_leading_and_trailing_whitespace(self): metadata = Metadata() metadata['artist'] = 'The Artist' filename = script_to_filename(' %artist% ', metadata) self.assertEqual(' The Artist ', filename)
def test_plain_filename(self): metadata = Metadata() filename = script_to_filename('AlbumArt', metadata) self.assertEqual('AlbumArt', filename)
def test_remove_null_chars(self): metadata = Metadata() filename = script_to_filename('a\x00b\x00', metadata) self.assertEqual('ab', filename)
def test_remove_tabs_and_linebreaks_chars(self): metadata = Metadata() filename = script_to_filename('a\tb\nc', metadata) self.assertEqual('abc', filename)
def _finalize_loading(self, error): if error: self.metadata.clear() self.status = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() return if self._requests > 0: return if not self._tracks_loaded: artists = set() totalalbumtracks = 0 absolutetracknumber = 0 va = self._new_metadata['musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node['media']: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) discpregap = False for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if "pregap" in medium_node: discpregap = True absolutetracknumber += 1 track = self._finalize_loading_track(medium_node['pregap'], mm, artists, va, absolutetracknumber, discpregap) track.metadata['~pregap'] = "1" track_count = medium_node['track-count'] if track_count: tracklist_node = medium_node['tracks'] for track_node in tracklist_node: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) if "data-tracks" in medium_node: for track_node in medium_node['data-tracks']: absolutetracknumber += 1 track = self._finalize_loading_track(track_node, mm, artists, va, absolutetracknumber, discpregap) track.metadata['~datatrack'] = "1" totalalbumtracks = string_(absolutetracknumber) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node self._tracks_loaded = True if not self._requests: for track in self._new_tracks: track.orig_metadata.copy(track.metadata) self.enable_update_metadata_images(False) # Prepare parser for user's script for s_name, s_text in enabled_tagger_scripts_texts(): parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(s_text, track.metadata) except: log.exception("Failed to run tagger script %s on track", s_name) track.metadata.strip_whitespace() # Run tagger script for the album itself try: parser.eval(s_text, self._new_metadata) except: log.exception("Failed to run tagger script %s on album", s_name) self._new_metadata.strip_whitespace() for track in self.tracks: track.metadata_images_changed.connect(self.update_metadata_images) for file in list(track.linked_files): file.move(self.unmatched_files) self.metadata = self._new_metadata self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.status = None self.match_files(self.unmatched_files.files) self.enable_update_metadata_images(True) self.update() self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { 'id': self.id, 'artist': self.metadata['albumartist'], 'album': self.metadata['album'] }, timeout=3000 ) for func in self._after_load_callbacks: func() self._after_load_callbacks = []
def parse_artists(self, artists): for node in artists: artist = Metadata() artist_to_metadata(node, artist) artist['score'] = node['score'] self.search_results.append(artist)
def test_cmd_set_multi_valued(self): context = Metadata() context["source"] = ["multi", "valued"] self.parser.eval("$set(test,%source%)", context) self.assertEqual(context.getall("test"), ["multi; valued"]) # list has only a single value
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, 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 == "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 test_preserve_backslash(self): metadata = Metadata() metadata['artist'] = 'AC\\/DC' filename = script_to_filename('%artist%', metadata) self.assertEqual('AC__DC' if IS_WIN else 'AC\\_DC', filename)
def parse_artists_from_xml(self, artist_xml): for node in artist_xml: artist = Metadata() artist_to_metadata(node, artist) self.search_results.append(artist)
def test_file_metadata(self): metadata = Metadata() file = File('somepath/somefile.mp3') self.assertEqual('', script_to_filename('$has_file()', metadata)) self.assertEqual( '1', script_to_filename('$has_file()', metadata, file=file))
def test_recording(self): m = Metadata() t = Track("1") parsed_recording = parse_recording(self.json_doc) recording_to_metadata(parsed_recording, m, t) self.assertEqual(m, {})
def _load(self, filename): log.debug("Loading file %r", filename) self.__casemap = {} file = MP4(encode_filename(filename)) tags = file.tags or {} metadata = Metadata() for name, values in tags.items(): name_lower = name.lower() 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], value) elif name in self.__freeform_tags: tag_name = self.__freeform_tags[name] _add_text_values_to_metadata(metadata, tag_name, values) elif name_lower in self.__freeform_tags_ci: tag_name = self.__freeform_tags_ci[name_lower] self.__casemap[tag_name] = name _add_text_values_to_metadata(metadata, tag_name, values) 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"] = values[0][0] metadata["totaltracks"] = values[0][1] elif name == "disk": metadata["discnumber"] = values[0][0] metadata["totaldiscs"] = 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.images.append(coverartimage) # Read other freeform tags always case insensitive elif name.startswith('----:com.apple.iTunes:'): tag_name = name_lower[22:] self.__casemap[tag_name] = name[22:] if (name not in self.__r_text_tags and name not in self.__r_bool_tags and name not in self.__r_int_tags and name not in self.__r_freeform_tags and name_lower not in self.__r_freeform_tags_ci and name not in self.__other_supported_tags): _add_text_values_to_metadata(metadata, tag_name, values) self._info(metadata, file) return metadata
def test_release(self): m = Metadata() a = Album("1") release_to_metadata(self.json_doc, m, a) self.assertEqual(m, {})
def file_save_image(filename, image): f = picard.formats.open_(filename) metadata = Metadata(images=[image]) f._save(filename, metadata)
def test_recording_instrument_keep_case(self): m = Metadata() t = Track("1") recording_to_metadata(self.json_doc, m, t) self.assertEqual(m['performer:EWI'], 'Michael Brecker')
def _finalize_loading(self, error): if error: self.metadata.clear() self.status = _("[could not load album %s]") % self.id del self._new_metadata del self._new_tracks self.update() if not self._requests: self.loaded = True for func, always in self._after_load_callbacks: if always: func() return if self._requests > 0: return if not self._tracks_loaded: artists = set() all_media = [] absolutetracknumber = 0 va = self._new_metadata[ 'musicbrainz_albumartistid'] == VARIOUS_ARTISTS_ID djmix_ars = {} if hasattr(self._new_metadata, "_djmix_ars"): djmix_ars = self._new_metadata._djmix_ars for medium_node in self._release_node['media']: mm = Metadata() mm.copy(self._new_metadata) medium_to_metadata(medium_node, mm) format = medium_node.get('format') if format: all_media.append(format) for dj in djmix_ars.get(mm["discnumber"], []): mm.add("djmixer", dj) if va: mm["compilation"] = "1" else: del mm["compilation"] if 'discs' in medium_node: discids = [disc.get('id') for disc in medium_node['discs']] mm['~musicbrainz_discids'] = discids mm['musicbrainz_discid'] = list( self._discids.intersection(discids)) if "pregap" in medium_node: absolutetracknumber += 1 mm['~discpregap'] = '1' extra_metadata = { '~pregap': '1', '~absolutetracknumber': absolutetracknumber, } self._finalize_loading_track(medium_node['pregap'], mm, artists, extra_metadata) track_count = medium_node['track-count'] if track_count: tracklist_node = medium_node['tracks'] for track_node in tracklist_node: absolutetracknumber += 1 extra_metadata = { '~absolutetracknumber': absolutetracknumber, } self._finalize_loading_track(track_node, mm, artists, extra_metadata) if "data-tracks" in medium_node: for track_node in medium_node['data-tracks']: absolutetracknumber += 1 extra_metadata = { '~datatrack': '1', '~absolutetracknumber': absolutetracknumber, } self._finalize_loading_track(track_node, mm, artists, extra_metadata) totalalbumtracks = absolutetracknumber self._new_metadata['~totalalbumtracks'] = totalalbumtracks # Generate a list of unique media, but keep order of first appearance self._new_metadata['media'] = " / ".join( list(OrderedDict.fromkeys(all_media))) for track in self._new_tracks: track.metadata["~totalalbumtracks"] = totalalbumtracks if len(artists) > 1: track.metadata["~multiartist"] = "1" del self._release_node del self._release_artist_nodes self._tracks_loaded = True if not self._requests: self.enable_update_metadata_images(False) for track in self._new_tracks: track.orig_metadata.copy(track.metadata) track.metadata_images_changed.connect( self.update_metadata_images) # Prepare parser for user's script for s_name, s_text in enabled_tagger_scripts_texts(): parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: parser.eval(s_text, track.metadata) except ScriptError: log.exception( "Failed to run tagger script %s on track", s_name) track.metadata.strip_whitespace() track.scripted_metadata.update(track.metadata) # Run tagger script for the album itself try: parser.eval(s_text, self._new_metadata) except ScriptError: log.exception("Failed to run tagger script %s on album", s_name) self._new_metadata.strip_whitespace() unmatched_files = [ file for track in self.tracks for file in track.files ] self.metadata = self._new_metadata self.orig_metadata.copy(self.metadata) self.orig_metadata.images.clear() self.tracks = self._new_tracks del self._new_metadata del self._new_tracks self.loaded = True self.status = None self.match_files(unmatched_files + self.unmatched_files.files) self.enable_update_metadata_images(True) self.update() self.tagger.window.set_statusbar_message( N_('Album %(id)s loaded: %(artist)s - %(album)s'), { 'id': self.id, 'artist': self.metadata['albumartist'], 'album': self.metadata['album'] }, timeout=3000) for func, always in self._after_load_callbacks: func() self._after_load_callbacks = [] if self.item.isSelected(): self.tagger.window.refresh_metadatabox()
def test_track(self): m = Metadata() medium_to_metadata(self.json_doc, m) self.assertEqual(m, {})
def test_compare_lengths(self): m1 = Metadata() m1.length = 360 m2 = Metadata() m2.length = 300 self.assertAlmostEqual(m1.compare(m2), 0.998)
def test_release_group(self): m = Metadata() r = ReleaseGroup("1") release_group_to_metadata(self.json_doc, m, r) self.assertEqual(m, {})
def test_compare_tracknumber_difference(self): m1 = Metadata() m1["tracknumber"] = "1" m2 = Metadata() m2["tracknumber"] = "2" self.assertEqual(m1.compare(m2), 0)
def test_lyrcis_with_description(self): metadata = Metadata({'lyrics:foo': 'bar'}) loaded_metadata = save_and_load_metadata(self.filename, metadata) self.assertEqual(metadata['lyrics:foo'], loaded_metadata['lyrics'])
def test_cmd_unset_prefix(self): context = Metadata() context['title'] = u'Foo' context['~rating'] = u'4' self.parser.eval("$unset(_rating)", context) self.assertNotIn('~rating', context)