fix(metadata): auto-download missing source images on profile change or indexing

Add two mechanisms to ensure source images are downloaded:

1. Trigger SourceMetadataStorageWorker when media_profile_id changes,
   so if a user switches to a profile with download_source_images enabled,
   the images will be fetched.

2. After successful indexing, check if source images are missing but
   should exist (profile has download_source_images: true). If so,
   kick off the metadata storage worker to download them.

This fixes issues where sources created before download_source_images
was added, or sources that had failed metadata jobs, never got their
artwork downloaded.
This commit is contained in:
Amadeus Mader 2026-03-07 18:56:26 +01:00
parent 80ae7ea1ff
commit 50a1ff61f4
5 changed files with 68 additions and 2 deletions

View file

@ -2,7 +2,7 @@
Issues discovered while debugging Jellyfin artwork problems. Issues discovered while debugging Jellyfin artwork problems.
## Issue 1: Source images not downloaded for early sources ## Issue 1: Source images not downloaded for early sources [COMPLETED]
**Problem:** Sources created before `download_source_images` was added to the media profile have empty `poster_filepath`, `fanart_filepath`, and `banner_filepath` in the `sources` table. The `source_metadata` table may have stale references to files that were never actually downloaded. **Problem:** Sources created before `download_source_images` was added to the media profile have empty `poster_filepath`, `fanart_filepath`, and `banner_filepath` in the `sources` table. The `source_metadata` table may have stale references to files that were never actually downloaded.
@ -19,7 +19,7 @@ Issues discovered while debugging Jellyfin artwork problems.
2. Downloads missing source images for sources that don't have them 2. Downloads missing source images for sources that don't have them
3. Updates both `sources` and `source_metadata` tables 3. Updates both `sources` and `source_metadata` tables
## Issue 2: Source images not downloaded for some new sources ## Issue 2: Source images not downloaded for some new sources [COMPLETED]
**Problem:** Some recently added sources have no `source_metadata` entry at all, even though they use a profile with `download_source_images: true`. **Problem:** Some recently added sources have no `source_metadata` entry at all, even though they use a profile with `download_source_images: true`.

View file

@ -21,6 +21,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
alias Pinchflat.SlowIndexing.FileFollowerServer alias Pinchflat.SlowIndexing.FileFollowerServer
alias Pinchflat.Downloading.DownloadOptionBuilder alias Pinchflat.Downloading.DownloadOptionBuilder
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
alias Pinchflat.Metadata.SourceMetadataStorageWorker
alias Pinchflat.YtDlp.Media, as: YtDlpMedia alias Pinchflat.YtDlp.Media, as: YtDlpMedia
@ -245,6 +246,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
# Updates the source after a successful indexing run. This includes: # Updates the source after a successful indexing run. This includes:
# - Setting `last_indexed_at` to the current time # - Setting `last_indexed_at` to the current time
# - Advancing `download_cutoff_date` to 7 days ago if it's older (or nil) # - Advancing `download_cutoff_date` to 7 days ago if it's older (or nil)
# - Kicking off metadata storage if source images are missing but should be downloaded
# #
# Advancing the cutoff date prevents yt-dlp from scanning through months of old videos # Advancing the cutoff date prevents yt-dlp from scanning through months of old videos
# on every index. We use 7 days as a buffer to ensure we don't miss any videos that # on every index. We use 7 days as a buffer to ensure we don't miss any videos that
@ -260,6 +262,8 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
|> maybe_advance_cutoff_date(source.download_cutoff_date, new_cutoff_date) |> maybe_advance_cutoff_date(source.download_cutoff_date, new_cutoff_date)
Sources.update_source(source, update_attrs, run_post_commit_tasks: false) Sources.update_source(source, update_attrs, run_post_commit_tasks: false)
maybe_kickoff_metadata_storage_for_missing_images(source)
end end
defp maybe_advance_cutoff_date(attrs, nil, new_cutoff_date) do defp maybe_advance_cutoff_date(attrs, nil, new_cutoff_date) do
@ -273,4 +277,24 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpers do
attrs attrs
end end
end end
# Kicks off metadata storage if source images should be downloaded but are missing.
# This handles the case where:
# 1. A source was created before download_source_images was enabled on the profile
# 2. The source metadata worker failed or was interrupted
# 3. The profile's download_source_images setting was later enabled
defp maybe_kickoff_metadata_storage_for_missing_images(source) do
source = Repo.preload(source, :media_profile)
if source.media_profile.download_source_images && source_images_missing?(source) do
Logger.info("Source #{source.id} is missing images, kicking off metadata storage")
SourceMetadataStorageWorker.kickoff_with_task(source)
end
:ok
end
defp source_images_missing?(source) do
is_nil(source.poster_filepath) || is_nil(source.fanart_filepath)
end
end end

View file

@ -336,6 +336,11 @@ defmodule Pinchflat.Sources do
{_, %{original_url: _}} -> {_, %{original_url: _}} ->
SourceMetadataStorageWorker.kickoff_with_task(source) SourceMetadataStorageWorker.kickoff_with_task(source)
# If the media_profile_id has changed, re-fetch metadata to potentially
# download source images if the new profile has download_source_images enabled
{_, %{media_profile_id: _}} ->
SourceMetadataStorageWorker.kickoff_with_task(source)
_ -> _ ->
:ok :ok
end end

View file

@ -13,6 +13,7 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
alias Pinchflat.Downloading.MediaDownloadWorker alias Pinchflat.Downloading.MediaDownloadWorker
alias Pinchflat.SlowIndexing.SlowIndexingHelpers alias Pinchflat.SlowIndexing.SlowIndexingHelpers
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
alias Pinchflat.Metadata.SourceMetadataStorageWorker
setup do setup do
{:ok, %{source: source_fixture()}} {:ok, %{source: source_fixture()}}
@ -259,6 +260,32 @@ defmodule Pinchflat.SlowIndexing.SlowIndexingHelpersTest do
assert source.download_cutoff_date == recent_cutoff assert source.download_cutoff_date == recent_cutoff
end end
test "kicks off metadata storage if source images are missing but should be downloaded" do
profile = media_profile_fixture(%{download_source_images: true})
source = source_fixture(%{media_profile_id: profile.id, poster_filepath: nil, fanart_filepath: nil})
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
assert_enqueued(worker: SourceMetadataStorageWorker, args: %{"id" => source.id})
end
test "does not kick off metadata storage if source images exist" do
source = source_with_metadata_attachments()
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
refute_enqueued(worker: SourceMetadataStorageWorker)
end
test "does not kick off metadata storage if profile does not download source images" do
profile = media_profile_fixture(%{download_source_images: false})
source = source_fixture(%{media_profile_id: profile.id, poster_filepath: nil})
SlowIndexingHelpers.index_and_enqueue_download_for_media_items(source)
refute_enqueued(worker: SourceMetadataStorageWorker)
end
test "enqueues a job for each pending media item" do test "enqueues a job for each pending media item" do
source = source_fixture() source = source_fixture()
media_item = media_item_fixture(source_id: source.id, media_filepath: nil) media_item = media_item_fixture(source_id: source.id, media_filepath: nil)

View file

@ -565,6 +565,16 @@ defmodule Pinchflat.SourcesTest do
refute_enqueued(worker: SourceMetadataStorageWorker) refute_enqueued(worker: SourceMetadataStorageWorker)
end end
test "updating will kickoff a metadata storage worker if the media_profile_id changes" do
source = source_fixture()
new_profile = media_profile_fixture()
update_attrs = %{media_profile_id: new_profile.id}
assert {:ok, %Source{} = source} = Sources.update_source(source, update_attrs)
assert_enqueued(worker: SourceMetadataStorageWorker, args: %{"id" => source.id})
end
end end
describe "update_source/3 when testing media download tasks" do describe "update_source/3 when testing media download tasks" do