Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
67bc4e7d7a
|
|||
|
d3e0e289a6
|
|||
|
e8d9299939
|
@@ -330,6 +330,37 @@ def stats():
|
||||
print(f" {source}: {count}")
|
||||
|
||||
|
||||
@cache_app.command
|
||||
def insert(
|
||||
*,
|
||||
path: Annotated[
|
||||
str | None,
|
||||
cyclopts.Parameter(
|
||||
name=["--path"],
|
||||
help="Path to a local .lrc file to insert instead of reading from stdin.",
|
||||
),
|
||||
] = None,
|
||||
):
|
||||
"""Manually insert lyrics into the cache for the current track."""
|
||||
track = get_current_track(_player)
|
||||
if not track:
|
||||
logger.error("No active playing track found.")
|
||||
sys.exit(1)
|
||||
|
||||
if path:
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
lyrics = f.read()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read file: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.info("Reading lyrics from stdin (Ctrl+D to finish)...")
|
||||
lyrics = sys.stdin.read()
|
||||
|
||||
manager.manual_insert(track, lyrics)
|
||||
|
||||
|
||||
# helpers
|
||||
|
||||
|
||||
|
||||
+22
-13
@@ -18,7 +18,7 @@ from loguru import logger
|
||||
from .fetchers import FetcherMethodType, create_fetchers
|
||||
from .fetchers.base import BaseFetcher
|
||||
from .cache import CacheEngine
|
||||
from .lrc import normalize_unsynced
|
||||
from .lrc import normalize_tags, normalize_unsynced, detect_sync_status
|
||||
from .config import TTL_SYNCED, TTL_UNSYNCED, TTL_NOT_FOUND, TTL_NETWORK_ERROR
|
||||
from .models import TrackMeta, LyricResult, CacheStatus
|
||||
from .enrichers import enrich_track
|
||||
@@ -51,18 +51,9 @@ class LrcManager:
|
||||
return [self.fetchers[force_method]]
|
||||
|
||||
sequence: list[BaseFetcher] = []
|
||||
if track.is_local:
|
||||
sequence.append(self.fetchers["local"])
|
||||
if track.title:
|
||||
sequence.append(self.fetchers["cache-search"])
|
||||
if track.trackid:
|
||||
sequence.append(self.fetchers["spotify"])
|
||||
if track.is_complete:
|
||||
sequence.append(self.fetchers["lrclib"])
|
||||
if track.title:
|
||||
sequence.append(self.fetchers["lrclib-search"])
|
||||
sequence.append(self.fetchers["netease"])
|
||||
sequence.append(self.fetchers["qqmusic"])
|
||||
for method in self.fetchers.keys():
|
||||
if self.fetchers[method].is_available(track):
|
||||
sequence.append(self.fetchers[method])
|
||||
|
||||
logger.debug(f"Fallback sequence: {[f.source_name for f in sequence]}")
|
||||
return sequence
|
||||
@@ -167,3 +158,21 @@ class LrcManager:
|
||||
logger.info(f"No lyrics found for {track.display_name()}")
|
||||
|
||||
return best_result
|
||||
|
||||
def manual_insert(
|
||||
self,
|
||||
track: TrackMeta,
|
||||
lyrics: str,
|
||||
) -> None:
|
||||
"""Manually insert lyrics into the cache for a track."""
|
||||
track = enrich_track(track)
|
||||
logger.info(f"Manually inserting lyrics for: {track.display_name()}")
|
||||
lyrics = normalize_tags(lyrics)
|
||||
result = LyricResult(
|
||||
status=detect_sync_status(lyrics),
|
||||
lyrics=normalize_tags(lyrics),
|
||||
source="manual",
|
||||
ttl=None,
|
||||
)
|
||||
self.cache.set(track, "manual", result, ttl_seconds=None)
|
||||
logger.info("Lyrics inserted into cache.")
|
||||
|
||||
@@ -22,6 +22,11 @@ class BaseFetcher(ABC):
|
||||
"""True if this fetcher manages its own cache (skip per-source cache check)."""
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
"""Check if the fetcher is available for the given track (e.g. has required metadata)."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
|
||||
@@ -30,6 +30,9 @@ class CacheSearchFetcher(BaseFetcher):
|
||||
def self_cached(self) -> bool:
|
||||
return True
|
||||
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
return bool(track.title)
|
||||
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
|
||||
@@ -25,6 +25,9 @@ class LocalFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "local"
|
||||
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
return track.is_local
|
||||
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
|
||||
@@ -31,6 +31,9 @@ class LrclibFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "lrclib"
|
||||
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
return track.is_complete
|
||||
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
|
||||
@@ -33,6 +33,9 @@ class LrclibSearchFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "lrclib-search"
|
||||
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
return bool(track.title)
|
||||
|
||||
def fetch(
|
||||
self, track: TrackMeta, bypass_cache: bool = False
|
||||
) -> Optional[LyricResult]:
|
||||
|
||||
@@ -40,6 +40,9 @@ class NeteaseFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "netease"
|
||||
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
return bool(track.title)
|
||||
|
||||
def _search(self, track: TrackMeta, limit: int = 10) -> Optional[int]:
|
||||
"""Search Netease and return the best-matching song ID.
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ class QQMusicFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "qqmusic"
|
||||
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
return bool(track.title) and bool(QQ_MUSIC_API_URL)
|
||||
|
||||
def _search(self, track: TrackMeta, limit: int = 10) -> Optional[str]:
|
||||
"""Search QQ Music and return the best-matching song MID."""
|
||||
query = f"{track.artist or ''} {track.title or ''}".strip()
|
||||
|
||||
@@ -55,6 +55,9 @@ class SpotifyFetcher(BaseFetcher):
|
||||
def source_name(self) -> str:
|
||||
return "spotify"
|
||||
|
||||
def is_available(self, track: TrackMeta) -> bool:
|
||||
return bool(track.trackid) and bool(SPOTIFY_SP_DC)
|
||||
|
||||
# ─── Auth helpers ────────────────────────────────────────────────
|
||||
|
||||
def _get_server_time(self, client: httpx.Client) -> Optional[int]:
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lrcfetch"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
description = "Fetch line-synced lyrics for your music player."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
Reference in New Issue
Block a user