mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 12:08:30 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
152d786417 | ||
|
|
6aaa2dcf47 |
@@ -1266,6 +1266,18 @@ class MetadataDB:
|
|||||||
out.setdefault(fn, {})[field] = {"value": value, "locked": bool(locked)}
|
out.setdefault(fn, {})[field] = {"value": value, "locked": bool(locked)}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def _romaji_display(self, filename: str, artist: str, title: str):
|
||||||
|
"""English-base display fallback. A blank-artist CDLC pack named
|
||||||
|
'Artist_Title_v1_p' has no readable name (artist blank; title = the raw
|
||||||
|
filename), and a match would fill it with the artist's NATIVE script
|
||||||
|
(kanji/kana). Surface the author's own romaji parsed from the filename
|
||||||
|
instead, so an English base reads 'Junko Yagami - BAY CITY'. Only kicks in
|
||||||
|
when the pack has no artist of its own — a real pack artist is untouched."""
|
||||||
|
if (artist or "").strip():
|
||||||
|
return artist, title
|
||||||
|
d = _artist_title_from_filename(filename)
|
||||||
|
return (d["artist"], d["title"]) if d else (artist, title)
|
||||||
|
|
||||||
def pack_fields(self, filename: str) -> dict:
|
def pack_fields(self, filename: str) -> dict:
|
||||||
"""The stored (pack) values for the overridable catalog fields — the
|
"""The stored (pack) values for the overridable catalog fields — the
|
||||||
Fix-metadata popup shows these behind each override as the 'revert to
|
Fix-metadata popup shows these behind each override as the 'revert to
|
||||||
@@ -1275,7 +1287,11 @@ class MetadataDB:
|
|||||||
row = self.conn.execute(
|
row = self.conn.execute(
|
||||||
"SELECT title, artist, album, year, genre FROM songs WHERE filename = ?",
|
"SELECT title, artist, album, year, genre FROM songs WHERE filename = ?",
|
||||||
(filename,)).fetchone()
|
(filename,)).fetchone()
|
||||||
return {k: ((row[i] or "") if row else "") for i, k in enumerate(keys)}
|
vals = {k: ((row[i] or "") if row else "") for i, k in enumerate(keys)}
|
||||||
|
# Baseline the author's romaji (from the filename) for a blank-artist pack,
|
||||||
|
# so the Details tab's Pack reference matches what the grid shows.
|
||||||
|
vals["artist"], vals["title"] = self._romaji_display(filename, vals["artist"], vals["title"])
|
||||||
|
return vals
|
||||||
|
|
||||||
# Effective genre = a per-song genre OVERRIDE (Fix-metadata popup) else the
|
# Effective genre = a per-song genre OVERRIDE (Fix-metadata popup) else the
|
||||||
# scanned pack genre. Applied at FILTER/FACET time (like the P4 artist alias)
|
# scanned pack genre. Applied at FILTER/FACET time (like the P4 artist alias)
|
||||||
@@ -4215,6 +4231,17 @@ class MetadataDB:
|
|||||||
s["unmatched"] = s["filename"] in um
|
s["unmatched"] = s["filename"] in um
|
||||||
if amap:
|
if amap:
|
||||||
s["artist"] = amap.get((s.get("artist") or "").lower(), s.get("artist"))
|
s["artist"] = amap.get((s.get("artist") or "").lower(), s.get("artist"))
|
||||||
|
# English-base romaji fallback: a blank-artist CDLC pack shows nothing
|
||||||
|
# useful (artist blank; title = the raw filename). Surface the author's
|
||||||
|
# romaji from the "Artist_Title_v1_p" filename so the card reads
|
||||||
|
# "Junko Yagami — BAY CITY", never blank or native script. Display-only;
|
||||||
|
# a user override (below) still wins. Keyset-safe: stash the raw title
|
||||||
|
# for the cursor before replacing it.
|
||||||
|
if not (s.get("artist") or "").strip():
|
||||||
|
r_artist, r_title = self._romaji_display(s["filename"], s.get("artist"), s.get("title"))
|
||||||
|
if r_title != s.get("title") and "_sort_title" not in s:
|
||||||
|
s["_sort_title"] = s["title"]
|
||||||
|
s["artist"], s["title"] = r_artist, r_title
|
||||||
# Override wins over the pack AND the alias re-label — it's the user's
|
# Override wins over the pack AND the alias re-label — it's the user's
|
||||||
# explicit per-song choice. Only a non-empty override VALUE replaces a
|
# explicit per-song choice. Only a non-empty override VALUE replaces a
|
||||||
# cell; a lock-only row (value None) leaves the displayed value alone.
|
# cell; a lock-only row (value None) leaves the displayed value alone.
|
||||||
@@ -4224,7 +4251,7 @@ class MetadataDB:
|
|||||||
cell = ov.get(field)
|
cell = ov.get(field)
|
||||||
val = cell.get("value") if cell else None
|
val = cell.get("value") if cell else None
|
||||||
if val:
|
if val:
|
||||||
if field == "title":
|
if field == "title" and "_sort_title" not in s:
|
||||||
s["_sort_title"] = s["title"] # raw title, for the keyset cursor
|
s["_sort_title"] = s["title"] # raw title, for the keyset cursor
|
||||||
s[field] = val
|
s[field] = val
|
||||||
# Grouped rows carry the ⚑ N (chart_count) + the work_key from the
|
# Grouped rows carry the ⚑ N (chart_count) + the work_key from the
|
||||||
@@ -6520,6 +6547,36 @@ def _mb_search_recordings(artist, title, limit: int = 12) -> list[dict]:
|
|||||||
return cands
|
return cands
|
||||||
|
|
||||||
|
|
||||||
|
def _mb_search_release_groups(query: str, limit: int = 8) -> list[dict]:
|
||||||
|
"""Text search /release-group for the Change-cover picker: albums matching a
|
||||||
|
free query, each mapped to its Cover Art Archive front thumb. One request;
|
||||||
|
tiles whose CAA art is missing self-hide client-side (front-250 404s). Lets a
|
||||||
|
cover be found even for a song with no metadata match (the city-pop pile)."""
|
||||||
|
q = (query or "").strip()
|
||||||
|
if not q:
|
||||||
|
return []
|
||||||
|
body = _mb_http_get("release-group", {"query": q, "limit": limit})
|
||||||
|
out: list[dict] = []
|
||||||
|
for rg in ((body or {}).get("release-groups") or []):
|
||||||
|
rid = rg.get("id")
|
||||||
|
if not rid:
|
||||||
|
continue
|
||||||
|
# artist-credit is a list of {name, joinphrase, artist} (joinphrase glues
|
||||||
|
# collaborations) — reconstruct the credited name.
|
||||||
|
artist = "".join(
|
||||||
|
(c.get("name", "") + c.get("joinphrase", "")) if isinstance(c, dict) else str(c)
|
||||||
|
for c in (rg.get("artist-credit") or [])
|
||||||
|
).strip()
|
||||||
|
title = rg.get("title") or ""
|
||||||
|
year = (rg.get("first-release-date") or "")[:4]
|
||||||
|
out.append({
|
||||||
|
"id": rid,
|
||||||
|
"label": " · ".join(x for x in (title, artist, year) if x) or title or "Cover",
|
||||||
|
"thumb_url": f"https://coverartarchive.org/release-group/{rid}/front-250",
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# ── AcoustID audio fingerprinting (content-based identification) ──────────────
|
# ── AcoustID audio fingerprinting (content-based identification) ──────────────
|
||||||
# Optional path: requires the Chromaprint `fpcalc` binary AND an AcoustID API
|
# Optional path: requires the Chromaprint `fpcalc` binary AND an AcoustID API
|
||||||
# key ($ACOUSTID_API_KEY). Both absent ⇒ graceful no-op; the text matcher runs.
|
# key ($ACOUSTID_API_KEY). Both absent ⇒ graceful no-op; the text matcher runs.
|
||||||
@@ -12222,6 +12279,26 @@ async def get_song_art(filename: str, request: Request = None, source: str = "")
|
|||||||
_ART_PICKER_MAX_CAA = 12
|
_ART_PICKER_MAX_CAA = 12
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/song/{filename:path}/art/cover-search")
|
||||||
|
def api_art_cover_search(filename: str, q: str = ""):
|
||||||
|
"""Search Cover Art Archive (via MusicBrainz release-groups) for album covers
|
||||||
|
— powers the Change-cover picker's search box, so a cover can be found even
|
||||||
|
for a song with no metadata match (the unmatched city-pop pile, where
|
||||||
|
/art/candidates is empty). `q` defaults to the song's own artist + album/
|
||||||
|
title (romaji fallback applied). Read-only; the picker renders the thumbs and
|
||||||
|
applies a pick through the existing /art/url route."""
|
||||||
|
query = (q or "").strip()
|
||||||
|
if not query:
|
||||||
|
pack = meta_db.pack_fields(meta_db._canonical_song_filename(filename))
|
||||||
|
query = " ".join(x for x in (pack.get("artist"), pack.get("album") or pack.get("title")) if x).strip()
|
||||||
|
if not query:
|
||||||
|
return {"query": "", "covers": []}
|
||||||
|
try:
|
||||||
|
return {"query": query, "covers": _mb_search_release_groups(query, limit=8)}
|
||||||
|
except EnrichTransportError:
|
||||||
|
return {"query": query, "covers": [], "error": "unavailable"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/song/{filename:path}/art/candidates")
|
@app.get("/api/song/{filename:path}/art/candidates")
|
||||||
def get_song_art_candidates(filename: str):
|
def get_song_art_candidates(filename: str):
|
||||||
"""Everything the cover picker can offer for one song, without fetching a
|
"""Everything the cover picker can offer for one song, without fetching a
|
||||||
|
|||||||
@@ -138,6 +138,17 @@
|
|||||||
'<div class="flex flex-wrap gap-3">' + SKELETON_TILE + SKELETON_TILE + SKELETON_TILE + '</div>' +
|
'<div class="flex flex-wrap gap-3">' + SKELETON_TILE + SKELETON_TILE + SKELETON_TILE + '</div>' +
|
||||||
'<div class="text-xs text-fb-textDim pt-2">Fetching covers… the source is rate-limited.</div>' +
|
'<div class="text-xs text-fb-textDim pt-2">Fetching covers… the source is rate-limited.</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
// Search Cover Art Archive — find an album cover even when the song has
|
||||||
|
// no match (the auto candidates above are empty then). Pre-filled from
|
||||||
|
// the song's artist + album/title; the source is rate-limited.
|
||||||
|
'<div class="space-y-2 pt-1">' +
|
||||||
|
'<div class="text-xs font-semibold uppercase tracking-wider text-fb-textDim">Search covers</div>' +
|
||||||
|
'<div class="flex gap-2">' +
|
||||||
|
'<input data-ip-search-input type="text" value="' + esc(_cur.query || '') + '" placeholder="artist album" class="flex-1 bg-gray-800/50 border border-gray-700 rounded-md px-2 py-1 text-sm text-fb-text outline-none focus:border-fb-primary">' +
|
||||||
|
'<button data-ip-search-go class="text-sm text-fb-primary hover:text-fb-primaryHi border border-fb-primary/40 rounded-md px-3">Search</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div data-ip-search-results class="flex flex-wrap gap-3"></div>' +
|
||||||
|
'</div>' +
|
||||||
'<div data-ip-status class="hidden text-xs text-fb-accent"></div>' +
|
'<div data-ip-status class="hidden text-xs text-fb-accent"></div>' +
|
||||||
'</div></div>' +
|
'</div></div>' +
|
||||||
'<input type="file" accept="image/*" data-ip-file class="hidden">';
|
'<input type="file" accept="image/*" data-ip-file class="hidden">';
|
||||||
@@ -185,9 +196,47 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const searchInput = panel.querySelector('[data-ip-search-input]');
|
||||||
|
const runSearch = () => coverSearch(panel, (searchInput && searchInput.value) || '');
|
||||||
|
panel.querySelector('[data-ip-search-go]')?.addEventListener('click', runSearch);
|
||||||
|
searchInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); runSearch(); } });
|
||||||
panel.querySelector('[data-ip-close]')?.focus();
|
panel.querySelector('[data-ip-close]')?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search Cover Art Archive (via the song-scoped cover-search endpoint) and
|
||||||
|
// render the album covers as pickable tiles — the same apply('url') path as
|
||||||
|
// the auto candidates. Covers with no CAA art self-hide (img onerror).
|
||||||
|
async function coverSearch(panel, query) {
|
||||||
|
const out = panel.querySelector('[data-ip-search-results]');
|
||||||
|
const fn = _cur && _cur.filename;
|
||||||
|
if (!out || !fn) return;
|
||||||
|
out.innerHTML = '<div class="flex flex-wrap gap-3">' + SKELETON_TILE + SKELETON_TILE + '</div>';
|
||||||
|
let body = null;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/song/' + enc(fn) + '/art/cover-search?q=' + enc(String(query).trim()));
|
||||||
|
if (r.ok) body = await r.json();
|
||||||
|
} catch (_) { /* falls through to the empty state */ }
|
||||||
|
if (!_cur || _cur.filename !== fn) return; // closed / changed song while searching
|
||||||
|
const covers = (body && body.covers) || [];
|
||||||
|
if (!covers.length) {
|
||||||
|
out.innerHTML = '<div class="text-xs text-fb-textDim">' +
|
||||||
|
((body && body.error) ? 'Cover search is unavailable right now.' : 'No covers found — try a different search.') +
|
||||||
|
'</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.innerHTML = covers.map((c, i) =>
|
||||||
|
tileHtml('data-ip-cover="' + i + '"', imgFace(c.thumb_url), c.label || 'Cover')).join('');
|
||||||
|
out.querySelectorAll('[data-ip-cover]').forEach((btn) => {
|
||||||
|
const img = btn.querySelector('img');
|
||||||
|
if (img) img.onerror = () => btn.classList.add('hidden'); // no CAA art for this album → hide
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
if (_busy) return;
|
||||||
|
const c = covers[Number(btn.getAttribute('data-ip-cover'))];
|
||||||
|
if (c) apply('url', c.thumb_url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// The one candidates fetch, cancelled if the modal closes first. Failure
|
// The one candidates fetch, cancelled if the modal closes first. Failure
|
||||||
// (offline, demo mode, aborted) is silent: the skeletons just clear and
|
// (offline, demo mode, aborted) is silent: the skeletons just clear and
|
||||||
// the instant tiles remain — never an error wall.
|
// the instant tiles remain — never an error wall.
|
||||||
@@ -281,7 +330,13 @@
|
|||||||
const filename = opts && opts.filename;
|
const filename = opts && opts.filename;
|
||||||
if (!filename) return;
|
if (!filename) return;
|
||||||
_lastFocus = document.activeElement;
|
_lastFocus = document.activeElement;
|
||||||
_cur = { filename: filename, title: (opts && opts.title) || filename };
|
const title = (opts && opts.title) || filename;
|
||||||
|
const artist = (opts && opts.artist) || '';
|
||||||
|
const album = (opts && opts.album) || '';
|
||||||
|
// Pre-fill the cover search: "artist album" when the album is known, else
|
||||||
|
// just the artist, else the title — the server default backs it up.
|
||||||
|
const query = [artist, album].filter(Boolean).join(' ').trim() || title;
|
||||||
|
_cur = { filename: filename, title: title, query: query };
|
||||||
_busy = false;
|
_busy = false;
|
||||||
const m = ensureModal();
|
const m = ensureModal();
|
||||||
const panel = document.getElementById('v3-imgpick-panel');
|
const panel = document.getElementById('v3-imgpick-panel');
|
||||||
|
|||||||
@@ -511,10 +511,17 @@
|
|||||||
// then land on Details pre-filled for review.
|
// then land on Details pre-filled for review.
|
||||||
async function useTheseValues(song, cand) {
|
async function useTheseValues(song, cand) {
|
||||||
if (!cand) return;
|
if (!cand) return;
|
||||||
|
// Smart adopt for an English base: KEEP the readable name + title the card
|
||||||
|
// already shows (the author's romaji, e.g. "Junko Yagami / BAY CITY") — the
|
||||||
|
// match is often native script (kanji/kana). Take only what the pack lacks
|
||||||
|
// — album / year / genre — from the match; the pin below still brings the
|
||||||
|
// correct art + identity. The user can still edit any field.
|
||||||
song._pendingDetails = {
|
song._pendingDetails = {
|
||||||
title: String(cand.title || ''), artist: String(cand.artist || ''),
|
artist: String(song.artist || cand.artist || ''),
|
||||||
album: String(cand.album || ''), year: String(cand.year || ''),
|
title: String(song.title || cand.title || ''),
|
||||||
genre: (Array.isArray(cand.genres) && cand.genres[0]) ? String(cand.genres[0]) : String(cand.genre || ''),
|
album: String(cand.album || song.album || ''),
|
||||||
|
year: String(cand.year || song.year || ''),
|
||||||
|
genre: String((Array.isArray(cand.genres) && cand.genres[0]) || cand.genre || ''),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await post('/api/enrichment/review/' + enc(song.filename) + '/pick', { candidate: cand });
|
await post('/api/enrichment/review/' + enc(song.filename) + '/pick', { candidate: cand });
|
||||||
@@ -689,7 +696,7 @@
|
|||||||
'</div>';
|
'</div>';
|
||||||
body.querySelector('[data-cover-open]')?.addEventListener('click', () => {
|
body.querySelector('[data-cover-open]')?.addEventListener('click', () => {
|
||||||
if (window.__fbOpenImagePicker) {
|
if (window.__fbOpenImagePicker) {
|
||||||
window.__fbOpenImagePicker({ filename: song.filename, title: song.title || song.filename });
|
window.__fbOpenImagePicker({ filename: song.filename, title: song.title || song.filename, artist: song.artist, album: song.album });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-8
@@ -515,15 +515,22 @@
|
|||||||
done: ['bg-fb-good/90 text-black', '✓ Updated', ''],
|
done: ['bg-fb-good/90 text-black', '✓ Updated', ''],
|
||||||
nochange: ['bg-black/60 text-fb-textDim', '— No match', ''],
|
nochange: ['bg-black/60 text-fb-textDim', '— No match', ''],
|
||||||
// Resting indicator: subtle, so a mostly-unmatched library isn't a
|
// Resting indicator: subtle, so a mostly-unmatched library isn't a
|
||||||
// wall of loud badges; points at the manual fix.
|
// wall of loud badges. Clickable — a one-click handoff into the
|
||||||
nomatch: ['bg-black/60 text-fb-textDim', 'No match', 'No metadata match found — right-click to fix it by hand'],
|
// Fix-metadata popup for this song (see the [data-meta-fix] wiring).
|
||||||
|
nomatch: ['bg-black/60 text-fb-textDim', 'No match', 'Click to fix the metadata by hand'],
|
||||||
};
|
};
|
||||||
const conf = M[st] || M.queued;
|
const conf = M[st] || M.queued;
|
||||||
|
const fixable = st === 'nomatch'; // resting badge → opens Fix-metadata
|
||||||
// top-10 clears the tuning chip (top-2) in both normal and select mode;
|
// top-10 clears the tuning chip (top-2) in both normal and select mode;
|
||||||
// z-20 sits it above the art. Non-interactive so it never eats a click.
|
// z-20 sits it above the art. Batch states are non-interactive; the
|
||||||
return '<span class="v3-meta-tile absolute top-10 left-2 z-20 ' + conf[0] +
|
// resting "no match" badge is the handoff into the popup.
|
||||||
' text-[0.5625rem] font-bold px-1.5 py-0.5 rounded-sm leading-tight pointer-events-none"' +
|
const cls = 'v3-meta-tile absolute top-10 left-2 z-20 ' + conf[0] +
|
||||||
(conf[2] ? ' title="' + conf[2] + '"' : '') + '>' + conf[1] + '</span>';
|
' text-[0.5625rem] font-bold px-1.5 py-0.5 rounded-sm leading-tight ' +
|
||||||
|
(fixable ? 'pointer-events-auto cursor-pointer hover:bg-fb-primary hover:text-white transition-colors' : 'pointer-events-none');
|
||||||
|
return '<span class="' + cls + '"' +
|
||||||
|
(fixable ? ' data-meta-fix="1"' : '') +
|
||||||
|
(conf[2] ? ' title="' + conf[2] + '"' : '') +
|
||||||
|
'>' + conf[1] + '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a song is scored, the badge for that card is stale until the next
|
// After a song is scored, the badge for that card is stale until the next
|
||||||
@@ -1000,7 +1007,7 @@
|
|||||||
// the group's work_key/chart_count and pre-ticks the shown chart.)
|
// the group's work_key/chart_count and pre-ticks the shown chart.)
|
||||||
if (id === '__fixmatch') { if (window.__fbFixMatch) window.__fbFixMatch(playTarget); return; }
|
if (id === '__fixmatch') { if (window.__fbFixMatch) window.__fbFixMatch(playTarget); return; }
|
||||||
if (id === '__cover') {
|
if (id === '__cover') {
|
||||||
if (window.__fbOpenImagePicker) window.__fbOpenImagePicker({ filename: playTarget.filename, title: playTarget.title || playTarget.filename });
|
if (window.__fbOpenImagePicker) window.__fbOpenImagePicker({ filename: playTarget.filename, title: playTarget.title || playTarget.filename, artist: playTarget.artist, album: playTarget.album });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (id === '__refreshmeta') {
|
if (id === '__refreshmeta') {
|
||||||
@@ -1467,6 +1474,13 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openChartsDrawer(e.currentTarget.getAttribute('data-charts'), song);
|
openChartsDrawer(e.currentTarget.getAttribute('data-charts'), song);
|
||||||
});
|
});
|
||||||
|
// "No match" badge → straight into the Fix-metadata popup for this
|
||||||
|
// song (the batch → fix handoff). stopPropagation so it doesn't also
|
||||||
|
// trigger the card's play. Follows the displayed chart, like the menu.
|
||||||
|
el.querySelector('[data-meta-fix]')?.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (window.__fbFixMatch) window.__fbFixMatch(playTarget);
|
||||||
|
});
|
||||||
// Artist line → the artist page (PR-B). In select mode the grid's
|
// Artist line → the artist page (PR-B). In select mode the grid's
|
||||||
// capture-phase toggle intercepts first, so selection still wins.
|
// capture-phase toggle intercepts first, so selection still wins.
|
||||||
el.querySelector('[data-v3-artist]')?.addEventListener('click', (e) => {
|
el.querySelector('[data-v3-artist]')?.addEventListener('click', (e) => {
|
||||||
@@ -3112,7 +3126,7 @@
|
|||||||
// when image-picker.js isn't loaded.
|
// when image-picker.js isn't loaded.
|
||||||
artWrap.addEventListener('click', () => {
|
artWrap.addEventListener('click', () => {
|
||||||
if (window.__fbOpenImagePicker) {
|
if (window.__fbOpenImagePicker) {
|
||||||
window.__fbOpenImagePicker({ filename: song.filename, title: song.title || song.filename });
|
window.__fbOpenImagePicker({ filename: song.filename, title: song.title || song.filename, artist: song.artist, album: song.album });
|
||||||
} else {
|
} else {
|
||||||
artFile.click();
|
artFile.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,6 +233,24 @@ def test_lock_only_genre_does_not_change_facet(server):
|
|||||||
assert server.meta_db._effective_genre_expr() == "genre"
|
assert server.meta_db._effective_genre_expr() == "genre"
|
||||||
|
|
||||||
|
|
||||||
|
def test_romaji_fallback_for_blank_artist_pack(server):
|
||||||
|
fn = "CDLC/0 - City Pop/Junko-Yagami_BAY-CITY_v1_p.feedpak"
|
||||||
|
_put(server, fn, title="Junko-Yagami_BAY-CITY_v1_p", artist="") # scanner fell back to the filename
|
||||||
|
s = {x["filename"]: x for x in server.meta_db.query_page()[0]}[fn]
|
||||||
|
# the grid shows the author's romaji, not blank / the raw filename / kanji
|
||||||
|
assert s["artist"] == "Junko Yagami"
|
||||||
|
assert s["title"] == "BAY CITY"
|
||||||
|
# the Details baseline (pack_fields) matches, so the popup agrees with the grid
|
||||||
|
pack = server.meta_db.pack_fields(fn)
|
||||||
|
assert pack["artist"] == "Junko Yagami" and pack["title"] == "BAY CITY"
|
||||||
|
|
||||||
|
|
||||||
|
def test_romaji_fallback_left_alone_when_pack_has_artist(server):
|
||||||
|
_put(server, "a.archive", title="Real Title", artist="Real Artist")
|
||||||
|
s = {x["filename"]: x for x in server.meta_db.query_page()[0]}["a.archive"]
|
||||||
|
assert s["artist"] == "Real Artist" and s["title"] == "Real Title"
|
||||||
|
|
||||||
|
|
||||||
def test_title_keyset_paging_is_complete_with_overrides(client, server):
|
def test_title_keyset_paging_is_complete_with_overrides(client, server):
|
||||||
# Raw titles A/B/C → title-sort order is A, B, C on the RAW column.
|
# Raw titles A/B/C → title-sort order is A, B, C on the RAW column.
|
||||||
_put(server, "b.archive", title="B")
|
_put(server, "b.archive", title="B")
|
||||||
|
|||||||
Reference in New Issue
Block a user