mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-10 23:24:30 +00:00
Resolve in-pack audio on backend
Move audio member selection logic from frontend to backend. The frontend was probing multiple candidate audio files with error/retry logic, often resulting in 404s. Now the backend resolves the best available audio file (preview.ogg, stems/full.ogg, or any stem) during metadata extraction and includes it in song metadata as `audio_member`. The frontend makes a single, guaranteed request instead of multiple probes. Simplifies the preview flow and improves reliability. Signed-off-by: Kyle <kyle.j.t@live.co.uk>
This commit is contained in:
@@ -11,6 +11,7 @@ from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
import shutil
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
|
||||
# ── Pure, testable helpers ─────────────────────────────────────────────────
|
||||
@@ -114,6 +115,37 @@ def setup(app, context):
|
||||
sloppak = dlc / "sloppak"
|
||||
return sloppak if sloppak.exists() else dlc
|
||||
|
||||
def _audio_member(p: Path):
|
||||
"""Best in-pack audio file for hover-preview, as a pack-relative path.
|
||||
|
||||
Prefers a dedicated preview clip, then a full mix, then any stem — so the
|
||||
frontend can preview with a single request instead of probing (and 404ing)
|
||||
a hardcoded path. Returns None when the pack carries no audio.
|
||||
"""
|
||||
prefer = ("preview.ogg", "stems/full.ogg", "stems/audio.mp3",
|
||||
"stems/audio.ogg", "audio.mp3", "audio.ogg", "full.ogg")
|
||||
audio_ext = (".ogg", ".mp3", ".opus", ".m4a", ".oga")
|
||||
try:
|
||||
if p.is_dir():
|
||||
names = ["/".join(f.relative_to(p).parts) for f in p.rglob("*") if f.is_file()]
|
||||
else:
|
||||
with zipfile.ZipFile(p) as z:
|
||||
names = z.namelist()
|
||||
except Exception:
|
||||
return None
|
||||
nameset = set(names)
|
||||
for m in prefer:
|
||||
if m in nameset:
|
||||
return m
|
||||
for n in names: # any stem audio
|
||||
low = n.lower()
|
||||
if low.startswith("stems/") and low.endswith(audio_ext):
|
||||
return n
|
||||
for n in names: # any audio at all
|
||||
if n.lower().endswith(audio_ext):
|
||||
return n
|
||||
return None
|
||||
|
||||
def _meta(p: Path, dlc: Path) -> dict:
|
||||
# filename and added are always computed fresh — they change when files move.
|
||||
try:
|
||||
@@ -136,7 +168,8 @@ 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,
|
||||
"audio_member": _audio_member(p)}
|
||||
try:
|
||||
raw = context["extract_meta"](p)
|
||||
if raw:
|
||||
|
||||
@@ -743,11 +743,13 @@ function createFolderSurface(cfg) {
|
||||
var _HOVER_PREVIEW_DELAY_MS = 500;
|
||||
var _previewIndHost = null; // art element currently showing the indicator
|
||||
|
||||
// Pack audio layout varies: a dedicated preview.ogg (most songs), a single
|
||||
// stems/full.ogg mix, or tutorials' stems/audio.mp3 — try in that order.
|
||||
function _previewCandidates(song) {
|
||||
var base = '/api/sloppak/' + song.filename.split('/').map(encodeURIComponent).join('/') + '/file/';
|
||||
return [base + 'preview.ogg', base + 'stems/full.ogg', base + 'stems/audio.mp3'];
|
||||
// The backend resolves the correct in-pack audio member (song.audio_member),
|
||||
// so we preview with a single request — no probing / 404s. Null = no audio.
|
||||
function _previewUrl(song) {
|
||||
if (!song || !song.audio_member) return null;
|
||||
var enc = song.filename.split('/').map(encodeURIComponent).join('/');
|
||||
var mem = song.audio_member.split('/').map(encodeURIComponent).join('/');
|
||||
return '/api/sloppak/' + enc + '/file/' + mem;
|
||||
}
|
||||
|
||||
function _ensurePreviewStyle() {
|
||||
@@ -808,24 +810,16 @@ function createFolderSurface(cfg) {
|
||||
// full-track fallbacks are fine from the start. (Seeking by a fraction
|
||||
// of song.duration broke playback whenever the offset landed past the
|
||||
// end of a short preview clip.)
|
||||
var urls = _previewCandidates(song);
|
||||
var a = _previewEl();
|
||||
var seq = ++_previewSeq;
|
||||
var idx = 0;
|
||||
var url = _previewUrl(song);
|
||||
if (!url) return; // pack carries no previewable audio
|
||||
var a = _previewEl();
|
||||
var seq = ++_previewSeq;
|
||||
_showIndicator(host);
|
||||
function _tryNext() {
|
||||
if (seq !== _previewSeq) return;
|
||||
if (idx >= urls.length) { _clearIndicator(); return; } // no playable member
|
||||
a.src = urls[idx++];
|
||||
a.load();
|
||||
}
|
||||
a.onerror = function () { if (seq === _previewSeq) _tryNext(); }; // candidate 404/decode → next
|
||||
a.onloadedmetadata = function () {
|
||||
if (seq !== _previewSeq) return;
|
||||
a.play().catch(function () {});
|
||||
};
|
||||
a.onplaying = function () { if (seq === _previewSeq) _markIndicatorPlaying(); };
|
||||
_tryNext();
|
||||
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