mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 04:44:31 +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
|
from fastapi.responses import JSONResponse
|
||||||
import shutil
|
import shutil
|
||||||
import re
|
import re
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
# ── Pure, testable helpers ─────────────────────────────────────────────────
|
# ── Pure, testable helpers ─────────────────────────────────────────────────
|
||||||
@@ -114,6 +115,37 @@ def setup(app, context):
|
|||||||
sloppak = dlc / "sloppak"
|
sloppak = dlc / "sloppak"
|
||||||
return sloppak if sloppak.exists() else dlc
|
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:
|
def _meta(p: Path, dlc: Path) -> dict:
|
||||||
# filename and added are always computed fresh — they change when files move.
|
# filename and added are always computed fresh — they change when files move.
|
||||||
try:
|
try:
|
||||||
@@ -136,7 +168,8 @@ def setup(app, context):
|
|||||||
|
|
||||||
# Cache miss — run the expensive extract.
|
# Cache miss — run the expensive extract.
|
||||||
m = {"title": None, "artist": None, "album": None, "duration": None,
|
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:
|
try:
|
||||||
raw = context["extract_meta"](p)
|
raw = context["extract_meta"](p)
|
||||||
if raw:
|
if raw:
|
||||||
|
|||||||
@@ -743,11 +743,13 @@ function createFolderSurface(cfg) {
|
|||||||
var _HOVER_PREVIEW_DELAY_MS = 500;
|
var _HOVER_PREVIEW_DELAY_MS = 500;
|
||||||
var _previewIndHost = null; // art element currently showing the indicator
|
var _previewIndHost = null; // art element currently showing the indicator
|
||||||
|
|
||||||
// Pack audio layout varies: a dedicated preview.ogg (most songs), a single
|
// The backend resolves the correct in-pack audio member (song.audio_member),
|
||||||
// stems/full.ogg mix, or tutorials' stems/audio.mp3 — try in that order.
|
// so we preview with a single request — no probing / 404s. Null = no audio.
|
||||||
function _previewCandidates(song) {
|
function _previewUrl(song) {
|
||||||
var base = '/api/sloppak/' + song.filename.split('/').map(encodeURIComponent).join('/') + '/file/';
|
if (!song || !song.audio_member) return null;
|
||||||
return [base + 'preview.ogg', base + 'stems/full.ogg', base + 'stems/audio.mp3'];
|
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() {
|
function _ensurePreviewStyle() {
|
||||||
@@ -808,24 +810,16 @@ function createFolderSurface(cfg) {
|
|||||||
// full-track fallbacks are fine from the start. (Seeking by a fraction
|
// full-track fallbacks are fine from the start. (Seeking by a fraction
|
||||||
// of song.duration broke playback whenever the offset landed past the
|
// of song.duration broke playback whenever the offset landed past the
|
||||||
// end of a short preview clip.)
|
// end of a short preview clip.)
|
||||||
var urls = _previewCandidates(song);
|
var url = _previewUrl(song);
|
||||||
var a = _previewEl();
|
if (!url) return; // pack carries no previewable audio
|
||||||
var seq = ++_previewSeq;
|
var a = _previewEl();
|
||||||
var idx = 0;
|
var seq = ++_previewSeq;
|
||||||
_showIndicator(host);
|
_showIndicator(host);
|
||||||
function _tryNext() {
|
a.onerror = function () { if (seq === _previewSeq) _clearIndicator(); };
|
||||||
if (seq !== _previewSeq) return;
|
a.onloadedmetadata = function () { if (seq === _previewSeq) a.play().catch(function () {}); };
|
||||||
if (idx >= urls.length) { _clearIndicator(); return; } // no playable member
|
a.onplaying = function () { if (seq === _previewSeq) _markIndicatorPlaying(); };
|
||||||
a.src = urls[idx++];
|
a.src = url;
|
||||||
a.load();
|
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();
|
|
||||||
}
|
}
|
||||||
function _armHoverPreview(el, song, host) {
|
function _armHoverPreview(el, song, host) {
|
||||||
el.addEventListener('mouseenter', function () {
|
el.addEventListener('mouseenter', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user