From dd824e69b2138af611efac8e9a34d772ffb01921 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Mon, 10 Nov 2025 19:13:25 +0100 Subject: [PATCH 1/6] Clearart: Do not update files without an embedded image --- beetsplug/_utils/art.py | 7 +++++-- beetsplug/embedart.py | 1 + test/plugins/test_embedart.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/beetsplug/_utils/art.py b/beetsplug/_utils/art.py index 656c303ce5..264802ba54 100644 --- a/beetsplug/_utils/art.py +++ b/beetsplug/_utils/art.py @@ -210,5 +210,8 @@ def clear(log, lib, query): items = lib.items(query) log.info("Clearing album art from {} items", len(items)) for item in items: - log.debug("Clearing art for {}", item) - item.try_write(tags={"images": None}) + if mediafile.MediaFile(syspath(item.path)).images: + log.debug("Clearing art for {}", item) + item.try_write(tags={"images": None}) + else: + log.debug("No art to clean for {}", item) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index cbf40f5708..ab02f13b59 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -17,6 +17,7 @@ import os.path import tempfile from mimetypes import guess_extension +from unittest import mock import requests diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index d40025374a..3b07a6a729 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -17,6 +17,7 @@ import os.path import shutil import tempfile +import time import unittest from unittest.mock import MagicMock, patch @@ -225,10 +226,22 @@ def test_clear_art_with_yes_input(self): item = album.items()[0] self.io.addinput("y") self.run_command("embedart", "-f", self.small_artpath) + embedded_time = item.current_mtime() + time.sleep(1) + self.io.addinput("y") self.run_command("clearart") mediafile = MediaFile(syspath(item.path)) assert not mediafile.images + clear_time = item.current_mtime() + assert clear_time > embedded_time + time.sleep(1) + + # A run on a file without an image should not be modified + self.io.addinput("y") + self.run_command("clearart") + no_clear_time = item.current_mtime() + assert no_clear_time == clear_time def test_clear_art_with_no_input(self): self._setup_data() From 85168ba7fc0342dd3da551e3f8d3139b1c88e809 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Mon, 10 Nov 2025 19:20:42 +0100 Subject: [PATCH 2/6] Remove wrong import --- beetsplug/embedart.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index ab02f13b59..cbf40f5708 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -17,7 +17,6 @@ import os.path import tempfile from mimetypes import guess_extension -from unittest import mock import requests From 8889c4ab47f3bbe4f9fe1b4051bd13a88bbae22d Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Mon, 10 Nov 2025 22:38:37 +0100 Subject: [PATCH 3/6] Clear art on import --- beets/test/helper.py | 6 ++++-- beetsplug/_utils/art.py | 14 +++++++++----- beetsplug/embedart.py | 10 ++++++++++ test/plugins/test_embedart.py | 31 ++++++++++++++++++++++++++++++- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/beets/test/helper.py b/beets/test/helper.py index 3cb1e4c3ca..20ba4f4abb 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -364,15 +364,17 @@ def add_album_fixture( items.append(item) return self.lib.add_album(items) - def create_mediafile_fixture(self, ext="mp3", images=[]): + def create_mediafile_fixture(self, ext="mp3", images=[], target_dir=None): """Copy a fixture mediafile with the extension to `temp_dir`. `images` is a subset of 'png', 'jpg', and 'tiff'. For each specified extension a cover art image is added to the media file. """ + if not target_dir: + target_dir = self.temp_dir src = os.path.join(_common.RSRC, util.bytestring_path(f"full.{ext}")) - handle, path = mkstemp(dir=self.temp_dir) + handle, path = mkstemp(dir=target_dir) path = bytestring_path(path) os.close(handle) shutil.copyfile(syspath(src), syspath(path)) diff --git a/beetsplug/_utils/art.py b/beetsplug/_utils/art.py index 264802ba54..b11b30b958 100644 --- a/beetsplug/_utils/art.py +++ b/beetsplug/_utils/art.py @@ -206,12 +206,16 @@ def extract_first(log, outpath, items): return real_path +def clear_item(item, log): + if mediafile.MediaFile(syspath(item.path)).images: + log.debug("Clearing art for {}", item) + item.try_write(tags={"images": None}) + else: + log.debug("No art to clean for {}", item) + + def clear(log, lib, query): items = lib.items(query) log.info("Clearing album art from {} items", len(items)) for item in items: - if mediafile.MediaFile(syspath(item.path)).images: - log.debug("Clearing art for {}", item) - item.try_write(tags={"images": None}) - else: - log.debug("No art to clean for {}", item) + clear_item(item, log) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index cbf40f5708..08e63836c7 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -62,6 +62,7 @@ def __init__(self): "ifempty": False, "remove_art_file": False, "quality": 0, + "clearart_on_import": False, } ) @@ -82,6 +83,9 @@ def __init__(self): self.register_listener("art_set", self.process_album) + if self.config["clearart_on_import"].get(bool): + self.register_listener("import_task_files", self.import_task_files) + def commands(self): # Embed command. embed_cmd = ui.Subcommand( @@ -278,3 +282,9 @@ def remove_artfile(self, album): os.remove(syspath(album.artpath)) album.artpath = None album.store() + + def import_task_files(self, session, task): + """Automatically clearart of imported files.""" + for item in task.imported_items(): + self._log.debug("clearart-on-import {.filepath}", item) + art.clear_item(item, self._log) diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index 3b07a6a729..ae66fbc6f2 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -27,6 +27,7 @@ from beets import config, logging, ui from beets.test import _common from beets.test.helper import ( + ImportHelper, BeetsTestCase, FetchImageHelper, IOMixin, @@ -76,7 +77,9 @@ def wrapper(*args, **kwargs): return wrapper -class EmbedartCliTest(IOMixin, PluginMixin, FetchImageHelper, BeetsTestCase): +class EmbedartCliTest( + ImportHelper, IOMixin, PluginMixin, FetchImageHelper, BeetsTestCase +): plugin = "embedart" small_artpath = os.path.join(_common.RSRC, b"image-2x3.jpg") abbey_artpath = os.path.join(_common.RSRC, b"abbey.jpg") @@ -286,6 +289,32 @@ def test_embed_art_from_url_not_image(self): mediafile = MediaFile(syspath(item.path)) assert not mediafile.images + def test_clearart_on_import_disabled(self): + file_path = self.create_mediafile_fixture( + images=["jpg"], target_dir=self.import_path + ) + self.import_media.append(file_path) + with self.configure_plugin({"clearart_on_import": False}): + importer = self.setup_importer(autotag=False, write=True) + importer.run() + + item = self.lib.items()[0] + assert MediaFile(os.path.join(item.path)).images + + def test_clearart_on_import_enabled(self): + file_path = self.create_mediafile_fixture( + images=["jpg"], target_dir=self.import_path + ) + self.import_media.append(file_path) + # Force re-init the plugin to register the listener + self.unload_plugins() + with self.configure_plugin({"clearart_on_import": True}): + importer = self.setup_importer(autotag=False, write=True) + importer.run() + + item = self.lib.items()[0] + assert not MediaFile(os.path.join(item.path)).images + class DummyArtResizer(ArtResizer): """An `ArtResizer` which pretends that ImageMagick is available, and has From 95f21b6e429ecbf68f5a399cf6498256aa3f544d Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Mon, 10 Nov 2025 22:38:58 +0100 Subject: [PATCH 4/6] Remove log if no art to clear --- beetsplug/_utils/art.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/beetsplug/_utils/art.py b/beetsplug/_utils/art.py index b11b30b958..fce650c5b5 100644 --- a/beetsplug/_utils/art.py +++ b/beetsplug/_utils/art.py @@ -210,8 +210,6 @@ def clear_item(item, log): if mediafile.MediaFile(syspath(item.path)).images: log.debug("Clearing art for {}", item) item.try_write(tags={"images": None}) - else: - log.debug("No art to clean for {}", item) def clear(log, lib, query): From d11c074a859b6bcef05cad8a3947bd09651608ec Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Mon, 10 Nov 2025 23:44:02 +0100 Subject: [PATCH 5/6] Improve test to not sleep --- test/plugins/test_embedart.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index ae66fbc6f2..2b6f59e265 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -17,7 +17,6 @@ import os.path import shutil import tempfile -import time import unittest from unittest.mock import MagicMock, patch @@ -229,21 +228,19 @@ def test_clear_art_with_yes_input(self): item = album.items()[0] self.io.addinput("y") self.run_command("embedart", "-f", self.small_artpath) - embedded_time = item.current_mtime() - time.sleep(1) + embedded_time = os.path.getmtime(syspath(item.path)) self.io.addinput("y") self.run_command("clearart") mediafile = MediaFile(syspath(item.path)) assert not mediafile.images - clear_time = item.current_mtime() + clear_time = os.path.getmtime(syspath(item.path)) assert clear_time > embedded_time - time.sleep(1) # A run on a file without an image should not be modified self.io.addinput("y") self.run_command("clearart") - no_clear_time = item.current_mtime() + no_clear_time = os.path.getmtime(syspath(item.path)) assert no_clear_time == clear_time def test_clear_art_with_no_input(self): From 16c4f6e4331099ffa05d5fc9bbf4f15456bba611 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Thu, 20 Nov 2025 18:48:37 +0100 Subject: [PATCH 6/6] Fix lint --- test/plugins/test_embedart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugins/test_embedart.py b/test/plugins/test_embedart.py index 2b6f59e265..a7038b152b 100644 --- a/test/plugins/test_embedart.py +++ b/test/plugins/test_embedart.py @@ -26,9 +26,9 @@ from beets import config, logging, ui from beets.test import _common from beets.test.helper import ( - ImportHelper, BeetsTestCase, FetchImageHelper, + ImportHelper, IOMixin, PluginMixin, )