Compare commits

...

2 Commits

7 changed files with 40 additions and 42 deletions
+20 -18
View File
@@ -71,8 +71,7 @@ class CacheEngine:
expires_at INTEGER, expires_at INTEGER,
artist TEXT, artist TEXT,
title TEXT, title TEXT,
album TEXT, album TEXT
length INTEGER
) )
""") """)
# Migrations # Migrations
@@ -128,15 +127,13 @@ class CacheEngine:
f"[{status_str}, ttl={remaining}s]" f"[{status_str}, ttl={remaining}s]"
) )
status = CacheStatus(status_str) status = CacheStatus(status_str)
if confidence is None and status in ( if confidence is None:
CacheStatus.SUCCESS_SYNCED, if status == CacheStatus.SUCCESS_SYNCED:
CacheStatus.SUCCESS_UNSYNCED, confidence = LEGACY_CONFIDENCE_SYNCED
): elif status == CacheStatus.SUCCESS_UNSYNCED:
confidence = ( confidence = LEGACY_CONFIDENCE_UNSYNCED
LEGACY_CONFIDENCE_SYNCED else:
if status == CacheStatus.SUCCESS_SYNCED confidence = 100.0 # negative statuses: value irrelevant
else LEGACY_CONFIDENCE_UNSYNCED
)
return LyricResult( return LyricResult(
status=status, status=status,
@@ -164,12 +161,13 @@ class CacheEngine:
continue continue
if best is None: if best is None:
best = cached best = cached
else: elif cached.confidence > best.confidence:
cached_conf = ( best = cached
cached.confidence if cached.confidence is not None else 100.0 elif (
) cached.confidence == best.confidence
best_conf = best.confidence if best.confidence is not None else 100.0 and cached.status == CacheStatus.SUCCESS_SYNCED
if cached_conf > best_conf: and best.status != CacheStatus.SUCCESS_SYNCED
):
best = cached best = cached
return best return best
@@ -299,12 +297,15 @@ class CacheEngine:
f"SELECT status, lyrics, source, confidence FROM cache WHERE {where} " f"SELECT status, lyrics, source, confidence FROM cache WHERE {where} "
"ORDER BY COALESCE(confidence, " "ORDER BY COALESCE(confidence, "
" CASE status WHEN ? THEN ? ELSE ? END" " CASE status WHEN ? THEN ? ELSE ? END"
") DESC, created_at DESC LIMIT 1", ") DESC, "
"CASE status WHEN ? THEN 0 ELSE 1 END, "
"created_at DESC LIMIT 1",
params params
+ [ + [
CacheStatus.SUCCESS_SYNCED.value, CacheStatus.SUCCESS_SYNCED.value,
LEGACY_CONFIDENCE_SYNCED, LEGACY_CONFIDENCE_SYNCED,
LEGACY_CONFIDENCE_UNSYNCED, LEGACY_CONFIDENCE_UNSYNCED,
CacheStatus.SUCCESS_SYNCED.value,
], ],
).fetchall() ).fetchall()
@@ -390,6 +391,7 @@ class CacheEngine:
key=lambda x: ( key=lambda x: (
x[0], x[0],
-(x[1].get("confidence") or 0), -(x[1].get("confidence") or 0),
x[1].get("status") != CacheStatus.SUCCESS_SYNCED.value,
-(x[1].get("created_at") or 0), -(x[1].get("created_at") or 0),
) )
) )
+2
View File
@@ -453,6 +453,8 @@ def _print_cache_row(row: dict, indent: str = "") -> None:
print(f"{indent} Lyrics : {line_count} lines") print(f"{indent} Lyrics : {line_count} lines")
if confidence is not None: if confidence is not None:
print(f"{indent} Confidence: {confidence:.0f}") print(f"{indent} Confidence: {confidence:.0f}")
else:
print(f"{indent} Confidence: (legacy)")
def run(): def run():
+12 -18
View File
@@ -40,15 +40,14 @@ _STATUS_TTL: dict[CacheStatus, Optional[int]] = {
def _is_better(new: LyricResult, old: LyricResult) -> bool: def _is_better(new: LyricResult, old: LyricResult) -> bool:
"""Compare two results by confidence only. """Compare two results: higher confidence wins; synced breaks ties."""
if new.confidence != old.confidence:
Synced/unsynced preference is already baked into the confidence score return new.confidence > old.confidence
(synced bonus in scoring weights), so we don't need a separate tier. # Equal confidence — prefer synced as tiebreaker
None confidence = trusted = 100. return (
""" new.status == CacheStatus.SUCCESS_SYNCED
new_conf = new.confidence if new.confidence is not None else 100.0 and old.status != CacheStatus.SUCCESS_SYNCED
old_conf = old.confidence if old.confidence is not None else 100.0 )
return new_conf > old_conf
class LrcManager: class LrcManager:
@@ -121,13 +120,10 @@ class LrcManager:
# Positive cache hit — apply the same confidence evaluation # Positive cache hit — apply the same confidence evaluation
# as fresh fetches so that low-confidence cached results # as fresh fetches so that low-confidence cached results
# don't block better results from later fetchers. # don't block better results from later fetchers.
is_trusted = ( is_trusted = cached.confidence >= HIGH_CONFIDENCE
cached.confidence is None
or cached.confidence >= HIGH_CONFIDENCE
)
logger.info( logger.info(
f"[{source}] cache hit: {cached.status.value}" f"[{source}] cache hit: {cached.status.value}"
f" (confidence={'trusted' if cached.confidence is None else f'{cached.confidence:.0f}'})" f" (confidence={cached.confidence:.0f})"
) )
if cached.status == CacheStatus.SUCCESS_SYNCED and is_trusted: if cached.status == CacheStatus.SUCCESS_SYNCED and is_trusted:
return cached return cached
@@ -155,12 +151,10 @@ class LrcManager:
CacheStatus.SUCCESS_SYNCED, CacheStatus.SUCCESS_SYNCED,
CacheStatus.SUCCESS_UNSYNCED, CacheStatus.SUCCESS_UNSYNCED,
): ):
is_trusted = ( is_trusted = result.confidence >= HIGH_CONFIDENCE
result.confidence is None or result.confidence >= HIGH_CONFIDENCE
)
logger.info( logger.info(
f"[{source}] got {result.status.value} lyrics" f"[{source}] got {result.status.value} lyrics"
f" (confidence={'trusted' if result.confidence is None else f'{result.confidence:.0f}'})" f" (confidence={result.confidence:.0f})"
) )
# Trusted synced → return immediately # Trusted synced → return immediately
if result.status == CacheStatus.SUCCESS_SYNCED and is_trusted: if result.status == CacheStatus.SUCCESS_SYNCED and is_trusted:
+1 -3
View File
@@ -62,6 +62,4 @@ class LyricResult:
lyrics: Optional[LRCData] = None lyrics: Optional[LRCData] = None
source: Optional[str] = None # Which fetcher produced this result source: Optional[str] = None # Which fetcher produced this result
ttl: Optional[int] = None # Hint for cache TTL (seconds) ttl: Optional[int] = None # Hint for cache TTL (seconds)
confidence: Optional[float] = ( confidence: float = 100.0 # 0-100 selection confidence (100 = trusted/exact)
None # 0-100 selection confidence (None = exact/trusted)
)
+2
View File
@@ -141,6 +141,8 @@ async def _fetch_metadata_dbus(
trackid = trackid.removeprefix("spotify:track:") trackid = trackid.removeprefix("spotify:track:")
elif trackid.startswith("/com/spotify/track/"): elif trackid.startswith("/com/spotify/track/"):
trackid = trackid.removeprefix("/com/spotify/track/") trackid = trackid.removeprefix("/com/spotify/track/")
else:
trackid = None
# Extract length (usually microseconds) # Extract length (usually microseconds)
length = metadata.get("mpris:length", None) length = metadata.get("mpris:length", None)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lrx-cli" name = "lrx-cli"
version = "0.2.1" version = "0.3.0"
description = "Fetch line-synced lyrics for your music player." description = "Fetch line-synced lyrics for your music player."
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
Generated
+1 -1
View File
@@ -153,7 +153,7 @@ wheels = [
[[package]] [[package]]
name = "lrx-cli" name = "lrx-cli"
version = "0.2.1" version = "0.3.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "cyclopts" }, { name = "cyclopts" },