From 649302551d2f69866fa470d0fb296f624b502e2b Mon Sep 17 00:00:00 2001 From: trav Date: Thu, 2 Jul 2026 12:44:58 -0400 Subject: [PATCH] tysm fable --- lintunes/gui/album_art_dialog.py | 23 ++++++++++++++- lintunes/gui/main_window.py | 19 +++++++++++-- tests/test_round17.py | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/lintunes/gui/album_art_dialog.py b/lintunes/gui/album_art_dialog.py index 4f70412..594a244 100644 --- a/lintunes/gui/album_art_dialog.py +++ b/lintunes/gui/album_art_dialog.py @@ -16,7 +16,8 @@ PREVIEW_PX = 300 class AlbumArtDialog(QDialog): - def __init__(self, candidates, track_count: int, parent=None): + def __init__(self, candidates, track_count: int, album_count: int = 0, + parent=None): super().__init__(parent) self.setWindowTitle("Download Album Art") self._candidates = candidates @@ -25,6 +26,9 @@ class AlbumArtDialog(QDialog): self._fetcher.image_finished.connect(self._on_image) self.selected_image: bytes | None = None self.selected_mime = "image/jpeg" + # True when the user chose the whole-album button instead of just the + # selected tracks. + self.apply_to_album = False self._image: bytes | None = None self._mime = "image/jpeg" @@ -49,6 +53,15 @@ class AlbumArtDialog(QDialog): self._use_btn = QPushButton("Use This Artwork") self._use_btn.setDefault(True) buttons.addButton(self._use_btn, QDialogButtonBox.ButtonRole.AcceptRole) + # Offered when the library's album holds more songs than the + # selection, so one download can cover the whole album. + self._album_btn = None + if album_count > track_count: + self._album_btn = QPushButton( + f"Use for All {album_count} Songs in Album") + buttons.addButton(self._album_btn, + QDialogButtonBox.ButtonRole.ActionRole) + self._album_btn.clicked.connect(self._accept_album) self._next_btn = QPushButton("Next Result") self._next_btn.setEnabled(len(candidates) > 1) buttons.addButton(self._next_btn, QDialogButtonBox.ButtonRole.ActionRole) @@ -70,6 +83,8 @@ class AlbumArtDialog(QDialog): f"{candidate.artist} — {candidate.album}{counter}") self._image = None self._use_btn.setEnabled(False) + if self._album_btn is not None: + self._album_btn.setEnabled(False) self._preview.setText("Loading…") self._preview.setPixmap(QPixmap()) self._fetcher.fetch(candidate) @@ -96,6 +111,8 @@ class AlbumArtDialog(QDialog): self._preview.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) self._use_btn.setEnabled(True) + if self._album_btn is not None: + self._album_btn.setEnabled(True) def _accept_current(self): if self._image is None: @@ -103,3 +120,7 @@ class AlbumArtDialog(QDialog): self.selected_image = self._image self.selected_mime = self._mime self.accept() + + def _accept_album(self): + self.apply_to_album = True + self._accept_current() diff --git a/lintunes/gui/main_window.py b/lintunes/gui/main_window.py index 2c03068..2953750 100644 --- a/lintunes/gui/main_window.py +++ b/lintunes/gui/main_window.py @@ -23,6 +23,16 @@ from lintunes.gui.preferences_dialog import PreferencesDialog from lintunes.gui.track_table import format_total_time +def album_tracks(library, artist: str, album: str) -> list: + """Every library track on the given album (case-insensitive match on + album name + album artist, falling back to track artist — the same + grouping the art download uses).""" + artist_cf, album_cf = artist.casefold(), album.casefold() + return [t for t in library.tracks.values() + if t.album.casefold() == album_cf + and (t.album_artist or t.artist).casefold() == artist_cf] + + class MainWindow(QMainWindow): def __init__(self, manager, prefs, lastfm=None, parent=None): super().__init__(parent) @@ -364,10 +374,15 @@ class MainWindow(QMainWindow): 6000) else: self.statusBar().clearMessage() + whole_album = album_tracks(self._manager.library, + group["artist"], group["album"]) dialog = AlbumArtDialog(result["candidates"], - len(group["tracks"]), self) + len(group["tracks"]), + album_count=len(whole_album), parent=self) if dialog.exec() and dialog.selected_image: - self._embed_album_art(group["tracks"], dialog.selected_image, + targets = whole_album if dialog.apply_to_album \ + else group["tracks"] + self._embed_album_art(targets, dialog.selected_image, dialog.selected_mime) self._art_next_group() diff --git a/tests/test_round17.py b/tests/test_round17.py index 7fb3d67..529553c 100644 --- a/tests/test_round17.py +++ b/tests/test_round17.py @@ -381,6 +381,54 @@ class TestArtSearch: assert not manager.undo_stack.can_undo() +class TestAlbumWideArt: + def test_album_tracks_matches_album_case_insensitively(self): + from lintunes.gui.main_window import album_tracks + library = Library() + library.tracks = { + 1: Track(track_id=1, artist="Artist", album="Great Album"), + 2: Track(track_id=2, artist="artist", album="great album"), + 3: Track(track_id=3, artist="Someone Else", album="Great Album", + album_artist="Artist"), # album_artist wins the grouping + 4: Track(track_id=4, artist="Artist", album="Other Album"), + 5: Track(track_id=5, artist="Other Artist", album="Great Album"), + } + ids = {t.track_id + for t in album_tracks(library, "Artist", "Great Album")} + assert ids == {1, 2, 3} + + def _dialog(self, qapp, monkeypatch, track_count, album_count): + from lintunes.art_search import AlbumArtFetcher, ArtCandidate + from lintunes.gui.album_art_dialog import AlbumArtDialog + # No network: capture the fetch and feed the image back by hand. + monkeypatch.setattr(AlbumArtFetcher, "fetch", lambda self, c: None) + candidate = ArtCandidate(artist="A", album="B", art_url="https://x") + dialog = AlbumArtDialog([candidate], track_count, + album_count=album_count) + dialog._on_image({"candidate": candidate, "image": b"img", + "mime": "image/png"}) + return dialog + + def test_album_button_applies_to_whole_album(self, qapp, monkeypatch): + dialog = self._dialog(qapp, monkeypatch, track_count=1, album_count=12) + assert dialog._album_btn is not None + assert dialog._album_btn.isEnabled() + assert "12" in dialog._album_btn.text() + assert not dialog.apply_to_album + dialog._accept_album() + assert dialog.apply_to_album + assert dialog.selected_image == b"img" + assert dialog.selected_mime == "image/png" + + def test_no_album_button_when_selection_covers_album(self, qapp, + monkeypatch): + dialog = self._dialog(qapp, monkeypatch, track_count=5, album_count=5) + assert dialog._album_btn is None + dialog._accept_current() + assert not dialog.apply_to_album + assert dialog.selected_image == b"img" + + # ---- context menu exposes the new action's signal ---- class TestDownloadArtSignal: