From eddee11fa3e45e61d1f5d44a56b69faad1c49dba Mon Sep 17 00:00:00 2001 From: Sambhav Kothari Date: Fri, 16 Dec 2016 21:29:36 +0530 Subject: [PATCH] PICARD-546,287: Add support for tag removal --- picard/file.py | 6 ++++-- picard/formats/apev2.py | 36 ++++++++++++++++++++++++++++++++++++ picard/formats/asf.py | 23 +++++++++++++++++++++++ picard/formats/id3.py | 25 +++++++++++++++++++++++++ picard/formats/mp4.py | 25 +++++++++++++++++++++++++ picard/formats/vorbis.py | 31 +++++++++++++++++++++++++++++++ picard/metadata.py | 15 +++++++++++++-- picard/ui/metadatabox.py | 15 +++++++++++---- 8 files changed, 168 insertions(+), 8 deletions(-) diff --git a/picard/file.py b/picard/file.py index 6c8376c63..4d4ac8495 100644 --- a/picard/file.py +++ b/picard/file.py @@ -141,11 +141,13 @@ class File(QtCore.QObject, Item): values = self.orig_metadata.getall(tag) if values: saved_metadata[tag] = values + deleted_tags = self.metadata.deleted_tags self.metadata.copy(metadata) + self.metadata.deleted_tags = deleted_tags for tag, values in saved_metadata.iteritems(): self.metadata.set(tag, values) - - self.metadata["acoustid_id"] = acoustid + if acoustid: + self.metadata["acoustid_id"] = acoustid def has_error(self): return self.state == File.ERROR diff --git a/picard/formats/apev2.py b/picard/formats/apev2.py index 573d04261..4f64837fc 100644 --- a/picard/formats/apev2.py +++ b/picard/formats/apev2.py @@ -18,6 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from __future__ import absolute_import +import re import mutagen.apev2 import mutagen.monkeysaudio import mutagen.musepack @@ -163,9 +164,44 @@ class APEv2File(File): tags['Cover Art (Front)'] = mutagen.apev2.APEValue(cover_filename + '\0' + image.data, mutagen.apev2.BINARY) break # can't save more than one item with the same name # (mp3tags does this, but it's against the specs) + + + for tag in metadata.deleted_tags: + real_name = str(self._get_tag_name(tag)) + if real_name in ('Lyrics','Comment','Performer'): + tag_type = "\(%s\)" % tag.split(':',1)[1] + for item in tags.get(real_name): + if re.search(tag_type,item): + tags.get(real_name).remove(item) + elif tag in ('totaltracks', 'totaldiscs'): + tagstr = real_name.lower() + 'number' + try: + tags[real_name] = metadata[tagstr] + except: + pass + else: + del tags[real_name] + + tags.save(encode_filename(filename)) + def _get_tag_name(self, name): + if name.startswith('lyrics:'): + return 'Lyrics' + elif name == 'date': + return 'Year' + elif name == ('tracknumber','totaltracks'): + return 'Track' + elif name == ('discnumber', 'totaldiscs'): + return 'Disc' + elif name.startswith('performer:') or name.startswith('comment:'): + return name.split(':',1)[0].title() + elif name in self.__rtranslate: + return self.__rtranslate[name] + else: + return name.title() + class MusepackFile(APEv2File): """Musepack file.""" diff --git a/picard/formats/asf.py b/picard/formats/asf.py index e452ecc61..2a70c22db 100644 --- a/picard/formats/asf.py +++ b/picard/formats/asf.py @@ -209,7 +209,30 @@ class ASFFile(File): continue name = self.__TRANS[name] file.tags[name] = map(unicode, values) + + for tag in metadata.deleted_tags: + real_name = self._get_tag_name(tag) + if real_name and real_name in file.tags: + if tag == 'totaldiscs': + try: + file.tags[real_name] = map(unicode,metadata['discnumber']) + except: + pass + else: + del file.tags[real_name] + file.save() def supports_tag(self, name): return name in self.__TRANS + + + def _get_tag_name(self, name): + + if name.startswith('lyrics'): + return 'lyrics' + elif name == 'totaldiscs': + return self.__TRANS['discnumber'] + else: + return self.__TRANS[name] + diff --git a/picard/formats/id3.py b/picard/formats/id3.py index e93baf673..bfab645dc 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -397,6 +397,19 @@ class ID3File(File): if tipl.people: tags.add(tipl) + self._build_inverse_dic() + + for tag in metadata.deleted_tags: + real_name = self._get_tag_name(tag) + log.debug(real_name) + log.debug(tag) + if real_name == 'POPM': + for key, frame in tags.items(): + if frame.FrameID == 'POPM' and frame.email == config.setting['rating_user_email']: + del tags[key] + elif real_name in tags: + del tags[real_name] + self._save_tags(tags, encode_filename(filename)) if self._IsMP3 and config.setting["remove_ape_from_mp3"]: @@ -404,6 +417,18 @@ class ID3File(File): mutagen.apev2.delete(encode_filename(filename)) except: pass + def _build_inverse_dic(self): + self.__itranslate = {} + for key, value in self.__translate.items(): + self.__itranslate[value] = key + for key, value in self.__translate_freetext.items(): + self.__itranslate[value] = key + + def _get_tag_name(self,name): + if name in self.__itranslate: + return self.__itranslate[name] + elif name == '~rating': + return 'POPM' def _get_file(self, filename): raise NotImplementedError() diff --git a/picard/formats/mp4.py b/picard/formats/mp4.py index 7fe09c9bb..4c58a4288 100644 --- a/picard/formats/mp4.py +++ b/picard/formats/mp4.py @@ -210,6 +210,11 @@ class MP4File(File): if covr: file.tags["covr"] = covr + for tag in metadata.deleted_tags: + real_name = self._get_tag_name(tag) + if real_name and real_name in file.tags: + del file.tags[real_name] + file.save() def supports_tag(self, name): @@ -218,6 +223,26 @@ class MP4File(File): or name in self.__other_supported_tags\ or name.startswith('lyrics:') + def _get_tag_name(self, name): + if name.startswith('lyrics:'): + return 'lyrics' + if name in self.__r_text_tags: + return self.__r_text_tags[name] + elif name in self.__r_bool_tags: + return self.__r_bool_tags[name] + elif name in self.__r_int_tags: + return self.__r_int_tags[name] + elif name in self.__r_freeform_tags: + return self.__r_freeform_tags[name] + elif name == "musicip_fingerprint": + return "----:com.apple.iTunes:fingerprint" + elif name == "tracknumber": + return "trkn" + elif name == "discnumber": + return "disk" + else: + return None + def _info(self, metadata, file): super(MP4File, self)._info(metadata, file) if hasattr(file.info, 'codec_description') and file.info.codec_description: diff --git a/picard/formats/vorbis.py b/picard/formats/vorbis.py index 44998c0de..03051972b 100644 --- a/picard/formats/vorbis.py +++ b/picard/formats/vorbis.py @@ -18,6 +18,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import base64 +import re import mutagen.flac import mutagen.ogg import mutagen.oggflac @@ -209,6 +210,17 @@ class VCommentFile(File): base64.standard_b64encode(picture.write())) file.tags.update(tags) + for tag in metadata.deleted_tags: + real_name = self._get_tag_name(tag) + if real_name and real_name in file.tags: + if real_name == 'performer' or real_name == 'comment': + tag_type = "\(%s\)" % tag.split(':',1)[1] + for item in file.tags.get(real_name): + if re.search(tag_type,item): + file.tags.get(real_name).remove(item) + else: + del file.tags[real_name] + kwargs = {} if is_flac and config.setting["remove_id3_from_flac"]: kwargs["deleteid3"] = True @@ -217,6 +229,25 @@ class VCommentFile(File): except TypeError: file.save() + def _get_tag_name(self, name): + if name == '~rating': + if config.setting['rating_user_email']: + return 'rating:%s' % config.setting['rating_user_email'] + else: + return 'rating' + elif name.startswith("~"): + return None + elif name.startswith('lyrics:'): + return 'lyrics' + elif name.startswith('performer:') or name.startswith('comment:'): + return name.split(':', 1)[0] + elif name == 'musicip_fingerprint': + return 'fingerprint' + elif name in self.__rtranslate: + return self.__rtranslate[name] + else: + return name + class FLACFile(VCommentFile): diff --git a/picard/metadata.py b/picard/metadata.py index 18464de98..9aa3e5f12 100644 --- a/picard/metadata.py +++ b/picard/metadata.py @@ -46,6 +46,7 @@ class Metadata(dict): def __init__(self): super(Metadata, self).__init__() self.images = [] + self.deleted_tags = set() self.length = 0 def append_image(self, coverartimage): @@ -222,11 +223,13 @@ class Metadata(dict): self.images = other.images[:] if other.length: self.length = other.length + self.deleted_tags.update(other.deleted_tags) def clear(self): dict.clear(self) self.images = [] self.length = 0 + self.deleted_tags = set() def getall(self, name): return dict.get(self, name, []) @@ -243,15 +246,17 @@ class Metadata(dict): def set(self, name, values): dict.__setitem__(self, name, values) + if name in self.deleted_tags: + self.deleted_tags.remove(name) def __setitem__(self, name, values): if not isinstance(values, list): values = [values] values = filter(None, map(unicode, values)) if len(values): - dict.__setitem__(self, name, values) + self.set(name, values) else: - self.pop(name, None) + self.delete(name) def add(self, name, value): if value or value == 0: @@ -261,6 +266,12 @@ class Metadata(dict): if value not in self.getall(name): self.add(name, value) + def delete(self, name): + if name in self: + self.pop(name, None) + self.deleted_tags.add(name) + + def iteritems(self): for name, values in dict.iteritems(self): for value in values: diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 4cdd2e133..6c1a7f4d7 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -111,16 +111,15 @@ class TagDiff(object): else: return orig != new - def add(self, tag, orig_values, new_values, removable): + def add(self, tag, orig_values, new_values, removable=False): if orig_values: self.orig.add(tag, orig_values) if new_values: self.new.add(tag, new_values) - if orig_values and not new_values: + if (orig_values and not new_values) or removed: self.status[tag] |= TagStatus.Removed - removable = False elif new_values and not orig_values: self.status[tag] |= TagStatus.Added removable = True @@ -412,7 +411,8 @@ class MetadataBox(QtGui.QTableWidget): new_values = list(orig_values or [""]) existing_tags.add(name) - tag_diff.add(name, orig_values, new_values, clear_existing_tags) + removed = name in new_metadata.deleted_tags + tag_diff.add(name, orig_values, new_values, True, removed) tag_diff.add("~length", str(orig_metadata.length), str(new_metadata.length), False) @@ -491,6 +491,13 @@ class MetadataBox(QtGui.QTableWidget): new_item.setFlags(orig_flags if length else new_flags) self.set_item_value(new_item, self.tag_diff.new, name) + font = new_item.font() + if result.tag_status(name) == TagStatus.Removed: + font.setStrikeOut(True) + else: + font.setStrikeOut(False) + new_item.setFont(font) + color = self.colors.get(result.tag_status(name), self.colors[TagStatus.NoChange]) orig_item.setForeground(color)