Add per-episode podcast cover art and fix nested album grouping

- podcast_feeds.py: fetch per-episode cover art from a feed's itunes:image
  (when it's genuinely distinct from the channel image) or, failing that,
  from the og:image on the episode's own linked page; extract audio from
  video-only enclosures via ffmpeg; match podcast-dl's filename convention
  (illegal characters become "_" instead of being dropped) so enabling
  feed.txt on an already-downloaded show doesn't re-download its back catalog
- scanner.py: _cover_for_episode now checks for a same-stem sidecar cover
  image before falling back to embedded ID3 art and the shared folder cover
- scanner.py/__init__.py: fixed a bug where an album folder nested one level
  deeper than usual (an age-range grouping folder, say) was mistaken for an
  empty album and skipped; added periodic progress logging for long scans
  and analysis passes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 09:54:23 +02:00
parent 747c390303
commit 69119bb72a
5 changed files with 635 additions and 46 deletions

View File

@@ -237,6 +237,22 @@ async def test_a_cover_file_is_found_and_used(config_dir: Path) -> None:
assert album.colors[0] != colors_from_id(album.id)[0]
async def test_an_episodes_sidecar_cover_wins_over_the_shows_shared_one(config_dir: Path) -> None:
from PIL import Image
# Downloaded by `podcast_feeds.py` when a feed has real per-episode art (see its
# `episode_cover_filename`) - same stem as the episode's audio file.
podcast = config_dir / "music" / "Kinderpodcasts" / "Wissen macht Ah"
Image.new("RGB", (32, 32), (10, 20, 30)).save(podcast / "cover.jpg")
Image.new("RGB", (32, 32), (200, 40, 30)).save(podcast / "20260101 - Neu.jpg")
library = await build(config_dir)
assert album_named(library, "Neu").cover == podcast / "20260101 - Neu.jpg"
# No sidecar for this one, so it still falls back to the show's shared cover.
assert album_named(library, "Alt").cover == podcast / "cover.jpg"
# --------------------------------------------------------------------------- cache
@@ -359,6 +375,28 @@ def test_album_ids_are_stable_and_path_derived(tmp_path: Path) -> None:
assert first != other
async def test_an_album_nested_under_an_extra_grouping_folder_is_still_found(
config_dir: Path,
) -> None:
"""The bug this fixed: an artist folder that groups its books one level deeper than
usual (an age-range folder, say) has no audio directly in it, so the scanner used to
treat it as an empty album and skip the whole artist rather than looking further
down for the actual album folders."""
for index in range(2):
write_track(
config_dir / "music" / "Hörbücher" / "Petzi" / "ab 5" / "Petzi und der Wal"
/ f"{index:02d} - teil.mp3",
title=f"Teil {index}",
album="Petzi und der Wal",
albumartist="Petzi",
)
library = await build(config_dir)
album = album_named(library, "Petzi und der Wal")
assert album.kind == "book"
assert len(album.tracks) == 2
# -------------------------------------------------------------------- figure kinds