mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 04:54:29 +00:00
Use manifest preview flag to avoid hover 404s
Expose a has_preview boolean from pack manifests and use it to gate hover previews. backend: plugins/folder_library/routes.py now (optionally) reads sloppak.load_manifest to set m['has_preview'] (guarded so plugin still loads without sloppak). frontend: plugins/folder_library/screen.js skips preview requests when song.has_preview is false and removes the HEAD-probe + previewMissing cache. docs: plugins/folder_library/CLAUDE.md updated to document has_preview and the preview behavior. This prevents unnecessary HEAD/audio 404s and console noise. Signed-off-by: Kyle <kyle.j.t@live.co.uk>
This commit is contained in:
@@ -153,14 +153,15 @@ Each song object (built by `_meta()`):
|
||||
"added": 1748132400.0,
|
||||
"arrangements": ["Lead", "Rhythm", "Bass"],
|
||||
"stems": ["Drums", "Bass", "Vocals"],
|
||||
"lyrics": true
|
||||
"lyrics": true,
|
||||
"has_preview": true
|
||||
}
|
||||
```
|
||||
|
||||
- `filename` is the full relative path from the DLC root — pass it directly to `window.playSong()`.
|
||||
- `added` is a Unix timestamp (float, seconds) from `stat().st_mtime` — convert with `new Date(added * 1000)`. Always recomputed fresh (it changes when a file moves), even on a metadata-cache hit.
|
||||
- `arrangements` / `stems` are flat lists of **strings**, even though `extract_meta()` returns them as objects.
|
||||
- Hover-preview audio is **not** resolved here — it delegates to the `song_preview` plugin (see Preview on Hover), so the tree carries no audio member.
|
||||
- `has_preview` is set from the pack's manifest `preview:` key (via core `load_manifest`) — `true` iff `song_preview` will actually serve a preview. The frontend previews **only** `has_preview` songs, so hover never requests (and 404-logs) a preview-less pack. Hover-preview audio itself is **not** resolved here — it comes from the `song_preview` endpoint (see Preview on Hover).
|
||||
|
||||
### extract_meta returns arrangements/stems as objects, not strings
|
||||
|
||||
@@ -334,7 +335,7 @@ Drag-and-drop uses **pointer events** (mousedown/mousemove/mouseup), not the HTM
|
||||
|
||||
Hovering a song for `_HOVER_PREVIEW_DELAY_MS` (800 ms) plays a short audio preview in place — no navigation to the player. Toggled by a play-icon toolbar button (`_injectToolbar`); **on by default** (`_previewHover`, persisted per surface under `<cfg.storePrefix>previewHover` — only an explicit stored `'false'` disables it).
|
||||
|
||||
- **Audio** — a dedicated `_previewAudio` `<audio>` element (never the main player's), played **from 0**. The URL (`_previewUrl`) **delegates to the `song_preview` plugin** — `/api/plugins/song_preview/audio?file=<filename>` — the canonical preview subsystem. It resolves the clip **strictly** from the pack's manifest `preview:` key (a short baked clip) with Range support; there is **no stem fallback by design** (a full-length stem isn't a preview), so a pack without a baked preview — hand-authored packs, most tutorials — returns **404** and simply doesn't preview until `song_preview`'s backfill generates a clip. Don't resolve pack members here — reading the manifest is the spec-faithful way, guessing filenames (and playing a full-length stem as a "preview") is not. If `song_preview` isn't installed the endpoint 404s too, and either way `onerror` clears the indicator so preview no-ops. Don't seek by `song.duration` — the clip is short, so a full-song offset lands past its end and nothing plays.
|
||||
- **Audio** — a dedicated `_previewAudio` `<audio>` element (never the main player's), played **from 0**. `_startPreview` runs **only when `song.has_preview`** (a backend flag from the manifest `preview:` key), so a preview-less pack is never requested — that's what keeps hover from logging 404s (a HEAD/`<audio>` request to `song_preview`'s 404 shows in the console even via `fetch`, because a plugin wraps `window.fetch`). The URL (`_previewUrl`) points at the **`song_preview` plugin** — `/api/plugins/song_preview/audio?file=<filename>` — the same endpoint the grid/list previews use. `song_preview` serves the clip **strictly** from the manifest `preview:` key (short baked clip, Range); there is **no stem fallback by design** (a full-length stem isn't a preview), so packs without a baked preview — hand-authored, most tutorials — don't preview until `song_preview`'s backfill generates one. Don't seek by `song.duration` — the clip is short, so a full-song offset lands past its end and nothing plays.
|
||||
- **Sequence guard** — `_previewSeq` is bumped on every start/stop, so a slow load that resolves after the pointer has moved on is ignored.
|
||||
- **Drag / click safety** — `mouseenter` skips arming while a drag is in progress (`_dragState` non-null); `mousedown` clears the pending dwell timer **and** `_stopPreview()`s any already-playing one, so grabbing a song to drag (or clicking to play) never leaves a preview running.
|
||||
- **Indicator** — a waveform overlay (`.fl-wf`, 9 bars) over the art, drawn via a one-time injected `<style>` (`_ensurePreviewStyle`). It **fades in** (`fl-in`) to avoid an abrupt pop, and the bars animate only once audio actually fires (the `.playing` class, set on the `playing` event). Perf: bars animate with `transform: scaleY` + `will-change: transform` (GPU-composited, no per-frame JS), only while playing. `_showIndicator` / `_markIndicatorPlaying` / `_clearIndicator` manage it against `_previewIndHost`.
|
||||
|
||||
@@ -12,6 +12,14 @@ from fastapi.responses import JSONResponse
|
||||
import shutil
|
||||
import re
|
||||
|
||||
# Core manifest reader — used only to tell the frontend which packs actually
|
||||
# carry a baked preview (manifest `preview:` key), so hover never requests a
|
||||
# preview-less pack and logs a 404. Guarded so the plugin still loads without it.
|
||||
try:
|
||||
from sloppak import load_manifest as _load_manifest
|
||||
except Exception:
|
||||
_load_manifest = None
|
||||
|
||||
|
||||
# ── Pure, testable helpers ─────────────────────────────────────────────────
|
||||
|
||||
@@ -136,7 +144,13 @@ def setup(app, context):
|
||||
|
||||
# Cache miss — run the expensive extract.
|
||||
m = {"title": None, "artist": None, "album": None, "duration": None,
|
||||
"year": None, "tuning": None, "arrangements": [], "stems": [], "lyrics": False}
|
||||
"year": None, "tuning": None, "arrangements": [], "stems": [], "lyrics": False,
|
||||
"has_preview": False}
|
||||
if _load_manifest is not None:
|
||||
try:
|
||||
m["has_preview"] = bool((_load_manifest(p) or {}).get("preview"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
raw = context["extract_meta"](p)
|
||||
if raw:
|
||||
|
||||
@@ -742,7 +742,6 @@ function createFolderSurface(cfg) {
|
||||
// calls playSong or touches the main player's <audio> element.
|
||||
var _HOVER_PREVIEW_DELAY_MS = 800; // dwell before preview — long enough that a click/drag doesn't trigger it
|
||||
var _previewIndHost = null; // art element currently showing the indicator
|
||||
var _previewMissing = {}; // filenames song_preview has no preview for (don't re-request)
|
||||
|
||||
// Delegate to the song_preview plugin (the canonical preview subsystem): it
|
||||
// resolves the clip from the pack's manifest `preview:` key (falling back to
|
||||
@@ -809,24 +808,20 @@ function createFolderSurface(cfg) {
|
||||
}
|
||||
}
|
||||
function _startPreview(song, host) {
|
||||
// Only packs the backend flagged with a baked preview are requested at
|
||||
// all — so hover never hits song_preview's 404 for a preview-less pack
|
||||
// (which would log a console error). Play from 0; the clip is short.
|
||||
if (!song || !song.has_preview) return;
|
||||
var url = _previewUrl(song);
|
||||
if (!url || _previewMissing[song.filename]) return; // no URL, or known no-preview → silent skip
|
||||
if (!url) return;
|
||||
var a = _previewEl();
|
||||
var seq = ++_previewSeq;
|
||||
// HEAD-probe first: song_preview 404s for packs with no baked preview, and
|
||||
// pointing <audio> at a 404 logs a console error (a fetch 404 is silent).
|
||||
// Remember misses so re-hovering the same song doesn't re-request. Play
|
||||
// from 0 — the clip is already short (don't seek by song.duration).
|
||||
fetch(url, { method: 'HEAD' }).then(function (res) {
|
||||
if (seq !== _previewSeq) return; // hover moved on before it resolved
|
||||
if (!res.ok) { _previewMissing[song.filename] = true; return; }
|
||||
_showIndicator(host);
|
||||
a.onerror = function () { if (seq === _previewSeq) _clearIndicator(); };
|
||||
a.onloadedmetadata = function () { if (seq === _previewSeq) a.play().catch(function () {}); };
|
||||
a.onplaying = function () { if (seq === _previewSeq) _markIndicatorPlaying(); };
|
||||
a.src = url;
|
||||
a.load();
|
||||
}).catch(function () {}); // network error → no-op, no noise
|
||||
_showIndicator(host);
|
||||
a.onerror = function () { if (seq === _previewSeq) _clearIndicator(); };
|
||||
a.onloadedmetadata = function () { if (seq === _previewSeq) a.play().catch(function () {}); };
|
||||
a.onplaying = function () { if (seq === _previewSeq) _markIndicatorPlaying(); };
|
||||
a.src = url;
|
||||
a.load();
|
||||
}
|
||||
function _armHoverPreview(el, song, host) {
|
||||
el.addEventListener('mouseenter', function () {
|
||||
|
||||
Reference in New Issue
Block a user