From 3c46364e5eadd5c2502a3386e3776cf78f82a352 Mon Sep 17 00:00:00 2001 From: ShubhamBhut Date: Thu, 21 Mar 2024 10:03:45 +0530 Subject: [PATCH] PICARD-187: Support for manually removing cover art PICARD-187: Support for manually removing cover art finishing PICARD-187 --- picard/formats/apev2.py | 5 +++ picard/formats/asf.py | 2 + picard/formats/id3.py | 5 +++ picard/formats/mp4.py | 3 ++ picard/formats/vorbis.py | 11 +++++- picard/ui/coverartbox.py | 82 +++++++++++++++++++++++++++++++++++++++- picard/util/imagelist.py | 11 ++++++ 7 files changed, 116 insertions(+), 3 deletions(-) diff --git a/picard/formats/apev2.py b/picard/formats/apev2.py index 777fd7c0f28..6a639eb43f0 100644 --- a/picard/formats/apev2.py +++ b/picard/formats/apev2.py @@ -263,6 +263,11 @@ def _remove_deleted_tags(self, metadata, tags): if real_name in tags: del tags[real_name] + if self.metadata.images.deleted: + for tag in tags: + if tag.lower().startswith('cover art'): + del tags[tag] + def _get_tag_name(self, name): if name in self.__casemap: return self.__casemap[name] diff --git a/picard/formats/asf.py b/picard/formats/asf.py index bf5784ee16f..8654587fee9 100644 --- a/picard/formats/asf.py +++ b/picard/formats/asf.py @@ -304,6 +304,8 @@ def _remove_deleted_tags(self, metadata, tags): real_name = self._get_tag_name(tag) if real_name and real_name in tags: del tags[real_name] + if self.metadata.images.deleted: + del tags['WM/Picture'] @classmethod def supports_tag(cls, name): diff --git a/picard/formats/id3.py b/picard/formats/id3.py index 94a5b7b67e7..f395424525f 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -659,6 +659,11 @@ def _remove_deleted_tags(self, metadata, tags): except KeyError: pass + if self.metadata.images.deleted: + for key, frame in tags.items(): + if frame.FrameID == 'APIC': + del tags[key] + @classmethod def supports_tag(cls, name): return ((name and not name.startswith('~') and name not in UNSUPPORTED_TAGS) diff --git a/picard/formats/mp4.py b/picard/formats/mp4.py index ddfc5335399..f2b521f1e96 100644 --- a/picard/formats/mp4.py +++ b/picard/formats/mp4.py @@ -340,6 +340,9 @@ def _remove_deleted_tags(self, metadata, tags): if tag not in {'totaltracks', 'totaldiscs'}: del tags[real_name] + if self.metadata.images.deleted: + del tags['covr'] + @classmethod def supports_tag(cls, name): return (name diff --git a/picard/formats/vorbis.py b/picard/formats/vorbis.py index 25ac86d5175..798360b2e94 100644 --- a/picard/formats/vorbis.py +++ b/picard/formats/vorbis.py @@ -321,7 +321,7 @@ def _save(self, filename, metadata): base64.b64encode(picture.write()).decode('ascii')) file.tags.update(tags) - + self._clear_cover_art(file) self._remove_deleted_tags(metadata, file.tags) kwargs = {} @@ -358,6 +358,15 @@ def _remove_deleted_tags(self, metadata, tags): del tags[tag] del tags[real_name] + def _clear_cover_art(self, file): + if self.metadata.images.deleted: + if self._File == mutagen.flac.FLAC: + file.clear_pictures() + else: + for tag in file: + if tag.lower().startswith('metadata_block_picture'): + del file.tags[tag] + def _get_tag_name(self, name): if name == '~rating': config = get_config() diff --git a/picard/ui/coverartbox.py b/picard/ui/coverartbox.py index 863e37bc4d0..46dbaad5926 100644 --- a/picard/ui/coverartbox.py +++ b/picard/ui/coverartbox.py @@ -349,6 +349,11 @@ def open_release_page(self): lookup.album_lookup(self.release) +def image_delete(obj, image): + obj.metadata.images.strip_selected_image(image) + obj.metadata_images_changed.emit() + + def set_image_replace(obj, coverartimage): obj.metadata.images.strip_front_images() obj.metadata.images.append(coverartimage) @@ -457,10 +462,11 @@ def update_metadata(self): orig_metadata = self.item.orig_metadata if not metadata or not metadata.images: - self.cover_art.set_metadata(orig_metadata) + self.cover_art.set_metadata(None) + self.orig_cover_art.set_metadata(None) else: self.cover_art.set_metadata(metadata) - self.orig_cover_art.set_metadata(orig_metadata) + self.orig_cover_art.set_metadata(orig_metadata) self.update_display() def fetch_remote_image(self, url, fallback_data=None): @@ -547,6 +553,71 @@ def load_remote_image(self, url, data): log.warning("Can't load image: %s", e) return + def delete_cover_art(self): + if not self.item or not self.item.metadata.images: + if not self.item.orig_metadata.images: + return + + cover_art_list = [image.source or _("Unnamed Cover Art") for image in self.item.metadata.images] + + selected_item, ok_pressed = QtWidgets.QInputDialog.getItem(self, _("Delete Cover Art"), + _("Select the cover art image to delete:"), + cover_art_list, 0, False) + if ok_pressed: + selected_image_index = cover_art_list.index(selected_item) + selected_image = self.item.metadata.images[selected_image_index] + try: + self.delete_cover_art_for_item(self.item, selected_image) + except CoverArtImageError as e: + log.error("Can't delete image: %s", e) + return + + def delete_cover_art_for_item(self, item, selected_image): + metadata = item.metadata + if not metadata or not metadata.images: + return + + debug_info = "Deleted %r from %r" + + if isinstance(item, Album): + item.enable_update_metadata_images(False) + for track in item.tracks: + track.enable_update_metadata_images(False) + image_delete(track, selected_image) + for file in item.iterfiles(): + image_delete(file, selected_image) + file.update(signal=False) + for track in item.tracks: + track.enable_update_metadata_images(True) + item.enable_update_metadata_images(True) + item.update(update_tracks=False) + elif isinstance(item, FileListItem): + parents = set() + item.enable_update_metadata_images(False) + image_delete(item, selected_image) + for file in item.iterfiles(): + for parent in iter_file_parents(file): + parent.enable_update_metadata_images(False) + parents.add(parent) + image_delete(file, selected_image) + file.update(signal=False) + for parent in parents: + image_delete(parent, selected_image) + parent.enable_update_metadata_images(True) + if isinstance(parent, Album): + parent.update(update_tracks=False) + else: + parent.update() + item.enable_update_metadata_images(True) + item.update() + elif isinstance(item, File): + image_delete(item, selected_image) + item.update() + else: + debug_info = "Unable to delete %r from %r" + + log.debug(debug_info, selected_image, item) + def _try_load_remote_image(self, url, data): coverartimage = CoverArtImage( url=url.toString(), @@ -636,6 +707,13 @@ def contextMenuEvent(self, event): show_more_details_action.triggered.connect(self.show_cover_art_info) menu.addAction(show_more_details_action) + delete_cover_art_action = QtGui.QAction(_("Delete cover art"), self.parent) + if self.item and self.item.can_show_coverart and self.item.metadata.images: + delete_cover_art_action.triggered.connect(self.delete_cover_art) + else: + delete_cover_art_action.setEnabled(False) + menu.addAction(delete_cover_art_action) + if self.orig_cover_art.isVisible(): name = _("Keep original cover art") use_orig_value_action = QtGui.QAction(name, self.parent) diff --git a/picard/util/imagelist.py b/picard/util/imagelist.py index 6ba0b5f1fdb..c1df9ad24b9 100644 --- a/picard/util/imagelist.py +++ b/picard/util/imagelist.py @@ -33,6 +33,7 @@ def __init__(self, iterable=()): self._images = list(iterable) self._hash_dict = {} self._changed = True + self._deleted = False def __len__(self): return len(self._images) @@ -50,6 +51,7 @@ def __delitem__(self, index): def insert(self, index, value): self._changed = True + self._deleted = False return self._images.insert(index, value) def __repr__(self): @@ -95,12 +97,21 @@ def strip_front_images(self): self._images = [image for image in self._images if not image.is_front_image()] self._changed = True + def strip_selected_image(self, image): + if image in self._images: + self._images.remove(image) + self._deleted = not self._images + def hash_dict(self): if self._changed: self._hash_dict = {img.datahash.hash(): img for img in self._images} self._changed = False return self._hash_dict + @property + def deleted(self): + return self._deleted + class ImageListState: def __init__(self):