diff --git a/docs/features/plugins.md b/docs/features/plugins.md
index 77cf1154..d6f2d99f 100644
--- a/docs/features/plugins.md
+++ b/docs/features/plugins.md
@@ -206,7 +206,7 @@ A discovery view of recent releases from the artists in your library — the fir
**Blurred backdrops take a thumbnail, never the animated source.** "Change cover → Local file" accepts `webp` and refuses video, so what people reach for is an animated WebP set as the ordinary cover — an ` `, decoded on the CPU. The immersive view drew that at `scale-150 blur-3xl` across the whole screen, re-blurring every frame (#615). It now renders the pre-resized variant, which the artwork pipeline generates single-frame; the animation still plays once, on the foreground cover. The result renders as a muted-loop `` overlay ([`MotionCoverOverlay`](../../src/components/player/MotionCoverOverlay.tsx) + [`useAlbumMotionArtwork`](../../src/hooks/useAlbumMotionArtwork.ts)) over the **static** cover in ImmersiveNowPlaying + NowPlayingPanel — it is purely additive: no motion (or a dead URL that 404s) simply falls back to the static album cover, which the motion path never touches.
-- **Manual motion cover** (issue #408): a user can set a local mp4 per album via the "Set motion cover" button (Film icon) on the album page → [`MotionCoverPickerModal`](../../src/components/common/MotionCoverPickerModal.tsx). It stores into a never-evicted `motion/` dir and takes precedence over any plugin resolution. Each file is capped at 256 MiB — raised from 64 MiB together with the plugin-download cap, because a 4K cover does not fit under a ceiling sized for Apple's 1080 renditions. The directory itself has no aggregate ceiling; nothing evicts it. **The override is keyed by album id, so the playing track must carry one.** Until #766 it did not: `QueueTrack` (the engine's playing-track shape) selected `al.title` but not `t.album_id`, and [`queuePayloadToTrack`](../../src/lib/queueTrack.ts) set `album_id: null`. Every now-playing surface therefore asked by title alone, the backend skipped the manual row and went straight to the plugins, and a hand-set cover never showed on any platform — "removing" it seemed to do nothing (the plugin's was the one on screen), and disabling the plugin seemed to disable manual covers too. The three queue queries now select `t.album_id` and the payload carries it, which also gives the immersive view and the queue panel their go-to-album entry. A cover that fails to load still falls back to the static one, but now says so in the console. **Settings → Immersive view → Show animated covers** ([`useMotionCovers`](../../src/hooks/useMotionCovers.ts), `profile_setting` `ui.motion_covers`, default on) hides every motion cover, hand-set ones included, and stops the plugin fan-out: `useAlbumMotionArtwork` reads it, so every surface obeys it at once. Before it, disabling the plugin was the only off switch, and it could not hide a hand-set cover. It is a setting and not a player button on purpose — the immersive top bar is already full on a small screen.
+- **Manual motion cover** (issue #408): a user can set a local mp4 per album via the "Set motion cover" button (Film icon) on the album page → [`MotionCoverPickerModal`](../../src/components/common/MotionCoverPickerModal.tsx). It stores into a never-evicted `motion/` dir and takes precedence over any plugin resolution. When it wins, a debug line says so ("manual motion cover wins; plugins skipped"); the log used to stay silent there, so a hand-set cover passed for a plugin's in a diagnostic log, and a row whose file is gone logs "manual motion cover file missing". Each file is capped at 256 MiB — raised from 64 MiB together with the plugin-download cap, because a 4K cover does not fit under a ceiling sized for Apple's 1080 renditions. The directory itself has no aggregate ceiling; nothing evicts it. **The override is keyed by album id, so the playing track must carry one.** Until #766 it did not: `QueueTrack` (the engine's playing-track shape) selected `al.title` but not `t.album_id`, and [`queuePayloadToTrack`](../../src/lib/queueTrack.ts) set `album_id: null`. Every now-playing surface therefore asked by title alone, the backend skipped the manual row and went straight to the plugins, and a hand-set cover never showed on any platform — "removing" it seemed to do nothing (the plugin's was the one on screen), and disabling the plugin seemed to disable manual covers too. The three queue queries now select `t.album_id` and the payload carries it, which also gives the immersive view and the queue panel their go-to-album entry. A cover that fails to load still falls back to the static one, but now says so in the console. **Settings → Immersive view → Show animated covers** ([`useMotionCovers`](../../src/hooks/useMotionCovers.ts), `profile_setting` `ui.motion_covers`, default on) hides every motion cover, hand-set ones included, and stops the plugin fan-out: `useAlbumMotionArtwork` reads it, so every surface obeys it at once. Before it, disabling the plugin was the only off switch, and it could not hide a hand-set cover. It is a setting and not a player button on purpose — the immersive top bar is already full on a small screen.
- **Opt-in local cache** (default **OFF**, `app_setting['motion_artwork.cache_enabled']`): when on, the resolved plugin mp4 is downloaded into an app-wide LRU cache (`motion_cache/` under the cache root — `/waveflow/` by default, or wherever _Settings → Storage and backups → Folders and caches_ moved it; hash-addressed by source URL, 1 GB cap, mtime-based eviction) and served from disk. OFF relies on the webview's transient HTTP cache. Toggle + size + clear live in **Settings → Storage and backups → Folders and caches** ([`MediaCachesCard`](../../src/components/views/settings/MediaCachesCard.tsx)); i18n under `settings.motionArtwork.*`.
**Not in the plugin's ⚙️ panel, deliberately.** Both video caches used to render there, keyed on the plugin's world, and the key was wrong twice: the caches are app-wide, so every plugin of the world showed the same switch and the same footprint, and a world does not say what a plugin produces — the first `waveflow:metadata` plugin shipping lyrics rather than motion covers showed a motion-cache switch that acted on another plugin's files. The ⚙️ panel now carries only a plugin's own `[[options]]`, and the gear appears only when the manifest declares some.
diff --git a/src-tauri/crates/app/src/commands/motion_artwork.rs b/src-tauri/crates/app/src/commands/motion_artwork.rs
index 46f7dff0..62054d25 100644
--- a/src-tauri/crates/app/src/commands/motion_artwork.rs
+++ b/src-tauri/crates/app/src/commands/motion_artwork.rs
@@ -104,6 +104,15 @@ pub async fn fetch_album_motion_artwork(
) -> AppResult> {
if let Some(id) = album_id {
if let Some(manual) = manual_motion_artwork(&app, &state, id).await? {
+ // Without this line a manual cover is invisible in the logs:
+ // the "resolving motion artwork" line below never runs, so the
+ // plugins look like the source of a cover they never saw.
+ tracing::debug!(
+ %artist,
+ %album,
+ album_id = id,
+ "manual motion cover wins; plugins skipped"
+ );
return Ok(Some(manual));
}
}
@@ -346,6 +355,7 @@ async fn manual_motion_artwork(
let Some(path) = library_media::find_media_file(candidates, hash, format.clone()).await else {
// The row outlived its file; the album falls back to its static
// cover, as it would for one that never had a motion cover.
+ tracing::debug!(album_id, "manual motion cover file missing; ignoring it");
return Ok(None);
};
if let Some(parent) = path.parent() {