mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-24 13:51:39 +00:00
* v3 library: context-menu unification — Fix match, Refresh metadata, Get info, multi-version remove (R2)
The ⋮ overflow and the native right-click menu already render from one
builder (openCardMenu), so every entry here lands in BOTH surfaces on
grid cards and tree rows alike — parity is structural, not maintained.
New entries (local library):
- Fix match… — opens the match modal in a single-song mode: no queue,
no Skip, the search panel open and pre-filled; a pick pins the match
exactly like the review flow (window.__fbFixMatch).
- Refresh metadata — POST /api/enrichment/refresh/{fn}: resets the
song's match to unscanned (canonical values + candidates cleared,
backoff zeroed) and kicks a pass. An EXPLICIT user action, so it may
discard a manual pin — the automation never does, but the user asking
for a re-match is the one party who owns that pin. Silent on success.
- Get info… — GET /api/chart/{fn}/fileinfo: file location + folder
(selectable/copyable under the v3 no-select default), format, size,
modified; for feedpaks the manifest summary (arrangements, stems,
cover/lyrics presence, authors, and whichever identity keys are
actually authored — mbid/isrc/genres/track/disc); plus the match
verdict ("Matched (text, 96%)" / "Pinned by you" / "Not scanned").
Under /api/chart because the GET /api/song/{path} catch-all would
swallow the suffix.
- Remove from library — with the multi-version interstitial: on a
multi-chart work, "remove the song" is ambiguous (a grouped card
stands for several files), so a modal lists EVERY version with
checkboxes (the card's own chart pre-ticked) and deletes exactly
what was picked — one file or the batch. Single-chart songs keep the
plain confirm.
Refresh + Get info are demo-mode blocked (cache mutation / path
exposure). apply_enrichment_match now zeroes `attempts` on an explicit
reset to unscanned, matching the stub upsert's identity-change rule.
5 new tests (refresh resets even a manual pin then re-matches via the
fake transport; 404s; fileinfo manifest/identity/match shapes;
traversal guard) + the demo-mode route list. Full-suite failure set
identical to the same-main baseline. tailwind.min.css regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nm7tHs1Yvjjtnnu4nzJgdN
* v3 context menu: review fixes — target displayed chart, harden Get-info
Three fixes from the PR review round:
- songs.js: Fix match / Refresh metadata / Get info now act on the DISPLAYED
chart (playTarget) rather than the group representative, matching Play. On a
grouped card where an intrinsic (tuning/arrangement) filter attached a
display_chart, these three previously fixed/refreshed/showed-info for the
wrong file. (__remove stays on `song`: it needs the group's work_key/
chart_count and already pre-ticks the shown chart.)
- server.py fileinfo: 404 ("not a chart") unless the path is a sloppak or a
loose song. The route previously stat'd ANY file under DLC_DIR, leaking its
path/size/mtime for e.g. a notes.txt the user keeps there. `format` can no
longer be "other".
- server.py fileinfo: the directory size sum skips symlinked entries so a link
inside a song folder can't pull in (or leak the size of) a file outside it.
Verified on the runtime that rglob does not descend symlinked subdirs.
+1 regression test (non-chart file -> 404).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
142 lines
6.0 KiB
Python
142 lines
6.0 KiB
Python
"""Tests for the R2 context-menu backend: per-song "Refresh metadata"
|
|
(explicit re-match reset + kick) and "Get info" (file location + pack
|
|
contents). The refresh flow reuses the P8 fake-transport pattern — nothing
|
|
here opens a socket."""
|
|
|
|
import importlib
|
|
import sys
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture()
|
|
def server(tmp_path, monkeypatch, isolate_logging):
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config"))
|
|
dlc = tmp_path / "dlc"
|
|
dlc.mkdir()
|
|
monkeypatch.setenv("DLC_DIR", str(dlc))
|
|
monkeypatch.setenv("FEEDBACK_SKIP_STARTUP_TASKS", "1")
|
|
sys.modules.pop("server", None)
|
|
srv = importlib.import_module("server")
|
|
try:
|
|
yield srv
|
|
finally:
|
|
conn = getattr(getattr(srv, "meta_db", None), "conn", None)
|
|
if conn is not None:
|
|
conn.close()
|
|
sys.modules.pop("server", None)
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(server):
|
|
return TestClient(server.app)
|
|
|
|
|
|
def _put(server, fn, title="Thunderstruck", artist="AC/DC", album="", year="1990",
|
|
duration=292):
|
|
server.meta_db.put(fn, 0, 0, {
|
|
"title": title, "artist": artist, "album": album, "year": year,
|
|
"duration": duration, "arrangements": [{"name": "Lead", "index": 0}],
|
|
})
|
|
|
|
|
|
def make_sloppak(server, name, extra_yaml="", title="Thunderstruck", artist="AC/DC"):
|
|
d = server.DLC_DIR / name
|
|
d.mkdir(parents=True)
|
|
(d / "manifest.yaml").write_text(
|
|
f"title: {title}\nartist: {artist}\nduration: 292\n"
|
|
"arrangements:\n - name: Lead\n id: lead\n"
|
|
"stems:\n - id: full\n file: stems/full.ogg\n" + extra_yaml,
|
|
encoding="utf-8")
|
|
_put(server, name, title=title, artist=artist)
|
|
return d
|
|
|
|
|
|
# ── Refresh metadata ──────────────────────────────────────────────────────────
|
|
|
|
def test_refresh_resets_even_a_manual_pin_and_rematches(server, client, monkeypatch):
|
|
_put(server, "a.sloppak")
|
|
server.meta_db.set_enrichment_manual(
|
|
"a.sloppak", {"recording_id": "old-pin", "title": "Thunderstruck",
|
|
"artist": "AC/DC"}, source="search")
|
|
# The reset itself is synchronous; the kicked pass runs on a daemon
|
|
# thread, so assert the reset here and drive the re-match inline below.
|
|
r = client.post("/api/enrichment/refresh/a.sloppak")
|
|
assert r.status_code == 200
|
|
for _ in range(200):
|
|
if not client.get("/api/enrichment/status").json()["running"]:
|
|
break
|
|
import time as _t
|
|
_t.sleep(0.02)
|
|
row = server.meta_db.get_enrichment("a.sloppak")
|
|
assert row["match_state"] == "unscanned" # the pin was discarded
|
|
assert row["mb_recording_id"] is None
|
|
assert row["attempts"] == 0
|
|
# …and the normal pass re-matches it (fake transport, network flag on).
|
|
def fake(path, params):
|
|
return {"recordings": [{
|
|
"id": "rec-new", "score": 100, "title": "Thunderstruck",
|
|
"length": 292000,
|
|
"artist-credit": [{"name": "AC/DC", "artist": {
|
|
"id": "art-1", "name": "AC/DC", "sort-name": "AC/DC"}}],
|
|
"releases": [{"id": "rel-1", "title": "The Razors Edge",
|
|
"status": "Official", "date": "1990-09-24",
|
|
"release-group": {"primary-type": "Album"}}],
|
|
}]}
|
|
monkeypatch.setattr(server, "_mb_http_get", fake)
|
|
monkeypatch.setattr(server, "_enrich_network_enabled", lambda: True)
|
|
server._background_enrich()
|
|
assert server.meta_db.get_enrichment("a.sloppak")["mb_recording_id"] == "rec-new"
|
|
|
|
|
|
def test_refresh_unknown_song_404(server, client):
|
|
assert client.post("/api/enrichment/refresh/ghost.sloppak").status_code == 404
|
|
|
|
|
|
# ── Get info ──────────────────────────────────────────────────────────────────
|
|
|
|
def test_fileinfo_sloppak_contents(server, client):
|
|
make_sloppak(server, "a.sloppak",
|
|
extra_yaml="mbid: 12345678-abcd-4ef0-9876-0123456789ab\n"
|
|
"genres: [rock, hard rock]\ntrack: 3\n")
|
|
body = client.get("/api/chart/a.sloppak/fileinfo").json()
|
|
assert body["format"] == "sloppak"
|
|
assert body["filename"] == "a.sloppak"
|
|
assert body["path"].endswith("a.sloppak")
|
|
assert body["size"] > 0
|
|
m = body["manifest"]
|
|
assert m["title"] == "Thunderstruck"
|
|
assert m["arrangements"] == ["Lead"]
|
|
assert m["stems"] == ["full"]
|
|
assert m["has_cover"] is False
|
|
assert m["identity"]["mbid"] == "12345678-abcd-4ef0-9876-0123456789ab"
|
|
assert m["identity"]["genres"] == ["rock", "hard rock"]
|
|
assert m["identity"]["track"] == 3
|
|
assert "isrc" not in m["identity"] # only keys actually present
|
|
# No enrichment row yet → no match block (the panel shows "Not scanned").
|
|
assert "match" not in body
|
|
|
|
|
|
def test_fileinfo_includes_match_verdict(server, client):
|
|
make_sloppak(server, "a.sloppak")
|
|
server.meta_db.set_enrichment_manual(
|
|
"a.sloppak", {"recording_id": "rec-1", "title": "Thunderstruck",
|
|
"artist": "AC/DC", "album": "The Razors Edge"},
|
|
source="search")
|
|
body = client.get("/api/chart/a.sloppak/fileinfo").json()
|
|
assert body["match"]["match_state"] == "manual"
|
|
assert body["match"]["canon_album"] == "The Razors Edge"
|
|
|
|
|
|
def test_fileinfo_missing_and_traversal(server, client):
|
|
assert client.get("/api/chart/ghost.sloppak/fileinfo").status_code == 404
|
|
assert client.get("/api/chart/..%2f..%2fetc%2fpasswd/fileinfo").status_code in (403, 404)
|
|
|
|
|
|
def test_fileinfo_non_chart_file_is_404(server, client):
|
|
"""A stray non-song file the user keeps under DLC_DIR must not have its
|
|
path/size/mtime exposed — the route is charts only, not a filesystem stat."""
|
|
(server.DLC_DIR / "private-notes.txt").write_text("secret", encoding="utf-8")
|
|
assert client.get("/api/chart/private-notes.txt/fileinfo").status_code == 404
|