Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/features/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<img>`, 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 `<video>` 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 — `<app-data>/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.
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/crates/app/src/commands/motion_artwork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ pub async fn fetch_album_motion_artwork(
) -> AppResult<Option<MotionArtwork>> {
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));
}
}
Expand Down Expand Up @@ -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() {
Expand Down
Loading