From 2d70231502b0e28442b613d5c9ab230ec12c7788 Mon Sep 17 00:00:00 2001 From: Uyanide Date: Mon, 6 Apr 2026 07:44:05 +0200 Subject: [PATCH] refactor: move some impl-specific constants out from config.py --- lrx_cli/authenticators/musixmatch.py | 13 ++++------- lrx_cli/authenticators/spotify.py | 27 +++++++++++----------- lrx_cli/config.py | 34 +++------------------------- lrx_cli/enrichers/musixmatch.py | 7 ++++-- lrx_cli/fetchers/lrclib.py | 5 ++-- lrx_cli/fetchers/lrclib_search.py | 5 ++-- lrx_cli/fetchers/musixmatch.py | 14 ++++++------ lrx_cli/fetchers/netease.py | 15 ++++++------ lrx_cli/fetchers/qqmusic.py | 9 ++++---- lrx_cli/fetchers/spotify.py | 22 ++++-------------- pyproject.toml | 2 +- uv.lock | 2 +- 12 files changed, 59 insertions(+), 96 deletions(-) diff --git a/lrx_cli/authenticators/musixmatch.py b/lrx_cli/authenticators/musixmatch.py index 932ba38..331d37a 100644 --- a/lrx_cli/authenticators/musixmatch.py +++ b/lrx_cli/authenticators/musixmatch.py @@ -12,12 +12,9 @@ from loguru import logger from .base import BaseAuthenticator from ..cache import CacheEngine -from ..config import ( - HTTP_TIMEOUT, - MUSIXMATCH_TOKEN_URL, - MUSIXMATCH_COOLDOWN_MS, - credentials, -) +from ..config import HTTP_TIMEOUT, MUSIXMATCH_COOLDOWN_MS, credentials + +_MUSIXMATCH_TOKEN_URL = "https://apic-desktop.musixmatch.com/ws/1.1/token.get" _MXM_HEADERS = {"Cookie": "x-mxm-token-guid="} _MXM_BASE_PARAMS = { @@ -61,7 +58,7 @@ class MusixmatchAuthenticator(BaseAuthenticator): {"until_ms": until_ms}, expires_at_ms=until_ms, ) - logger.warning("Musixmatch: token unavailable, entering cooldown for 1 hour") + logger.warning("Musixmatch: token unavailable, entering cooldown") def _invalidate_token(self) -> None: """Discard the current token from memory and DB.""" @@ -76,7 +73,7 @@ class MusixmatchAuthenticator(BaseAuthenticator): "user_language": "en", "t": str(int(time.time() * 1000)), } - url = f"{MUSIXMATCH_TOKEN_URL}?{urlencode(params)}" + url = f"{_MUSIXMATCH_TOKEN_URL}?{urlencode(params)}" logger.debug("Musixmatch: fetching anonymous token") try: diff --git a/lrx_cli/authenticators/spotify.py b/lrx_cli/authenticators/spotify.py index fe3e758..2d8796c 100644 --- a/lrx_cli/authenticators/spotify.py +++ b/lrx_cli/authenticators/spotify.py @@ -14,16 +14,16 @@ from loguru import logger from .base import BaseAuthenticator from ..cache import CacheEngine -from ..config import ( - HTTP_TIMEOUT, - SPOTIFY_SERVER_TIME_URL, - SPOTIFY_SECRET_URL, - SPOTIFY_TOKEN_URL, - UA_BROWSER, - credentials, -) +from ..config import HTTP_TIMEOUT, UA_BROWSER, credentials -_SPOTIFY_BASE_HEADERS = { +_SPOTIFY_TOKEN_URL = "https://open.spotify.com/api/token" +_SPOTIFY_SERVER_TIME_URL = "https://open.spotify.com/api/server-time" +_SPOTIFY_SECRET_URL = ( + "https://raw.githubusercontent.com/xyloflake/spot-secrets-go" + "/refs/heads/main/secrets/secrets.json" +) +SPOTIFY_BASE_HEADERS = { + "User-Agent": UA_BROWSER, "Referer": "https://open.spotify.com/", "Origin": "https://open.spotify.com", "App-Platform": "WebPlayer", @@ -82,7 +82,7 @@ class SpotifyAuthenticator(BaseAuthenticator): async def _get_server_time(self, client: httpx.AsyncClient) -> Optional[int]: try: - res = await client.get(SPOTIFY_SERVER_TIME_URL, timeout=HTTP_TIMEOUT) + res = await client.get(_SPOTIFY_SERVER_TIME_URL, timeout=HTTP_TIMEOUT) res.raise_for_status() data = res.json() if not isinstance(data, dict) or "serverTime" not in data: @@ -100,7 +100,7 @@ class SpotifyAuthenticator(BaseAuthenticator): logger.debug("Spotify: using cached TOTP secret") return self._cached_secret try: - res = await client.get(SPOTIFY_SECRET_URL, timeout=HTTP_TIMEOUT) + res = await client.get(_SPOTIFY_SECRET_URL, timeout=HTTP_TIMEOUT) res.raise_for_status() data = res.json() if not isinstance(data, list) or len(data) == 0: @@ -140,10 +140,9 @@ class SpotifyAuthenticator(BaseAuthenticator): return None headers = { - "User-Agent": UA_BROWSER, "Accept": "*/*", "Cookie": f"sp_dc={credentials.SPOTIFY_SP_DC}", - **_SPOTIFY_BASE_HEADERS, + **SPOTIFY_BASE_HEADERS, } async with httpx.AsyncClient(headers=headers) as client: @@ -169,7 +168,7 @@ class SpotifyAuthenticator(BaseAuthenticator): try: res = await client.get( - SPOTIFY_TOKEN_URL, params=params, timeout=HTTP_TIMEOUT + _SPOTIFY_TOKEN_URL, params=params, timeout=HTTP_TIMEOUT ) if res.status_code != 200: logger.error(f"Spotify: token request returned {res.status_code}") diff --git a/lrx_cli/config.py b/lrx_cli/config.py index c0ef5b5..355024e 100644 --- a/lrx_cli/config.py +++ b/lrx_cli/config.py @@ -57,34 +57,10 @@ MULTI_CANDIDATE_DELAY_S = 0.2 # delay between sequential lyric fetches LEGACY_CONFIDENCE_SYNCED = 50.0 LEGACY_CONFIDENCE_UNSYNCED = 40.0 -# Spotify related -SPOTIFY_TOKEN_URL = "https://open.spotify.com/api/token" -SPOTIFY_LYRICS_URL = "https://spclient.wg.spotify.com/color-lyrics/v2/track/" -SPOTIFY_SERVER_TIME_URL = "https://open.spotify.com/api/server-time" -SPOTIFY_SECRET_URL = ( - "https://raw.githubusercontent.com/xyloflake/spot-secrets-go" - "/refs/heads/main/secrets/secrets.json" -) +# User-Agents +UA_BROWSER = "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0" +UA_LRX = f"LRX-CLI {APP_VERSION} (https://github.com/Uyanide/lrx-cli)" -# Netease api -NETEASE_SEARCH_URL = "https://music.163.com/api/cloudsearch/pc" -NETEASE_LYRIC_URL = "https://interface3.music.163.com/api/song/lyric" - -# QQ api endpoints -QQ_MUSIC_API_SEARCH_ENDPOINT = "/api/search" -QQ_MUSIC_API_LYRIC_ENDPOINT = "/api/lyric" - -# LRCLIB api -LRCLIB_API_URL = "https://lrclib.net/api/get" -LRCLIB_SEARCH_URL = "https://lrclib.net/api/search" - -# Musixmatch desktop API -MUSIXMATCH_TOKEN_URL = "https://apic-desktop.musixmatch.com/ws/1.1/token.get" -MUSIXMATCH_SEARCH_URL = "https://apic-desktop.musixmatch.com/ws/1.1/track.search" -MUSIXMATCH_MACRO_URL = "https://apic-desktop.musixmatch.com/ws/1.1/macro.subtitles.get" -MUSIXMATCH_TRACK_MATCH_URL = ( - "https://apic-desktop.musixmatch.com/ws/1.1/matcher.track.get" -) MUSIXMATCH_COOLDOWN_MS = 600_000 # 10 minutes # Player preference (used when multiple MPRIS players are active) @@ -114,10 +90,6 @@ class _Credentials: credentials = _Credentials() -# User-Agents -UA_BROWSER = "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0" -UA_LRX = f"LRX-CLI {APP_VERSION} (https://github.com/Uyanide/lrx-cli)" - os.makedirs(CACHE_DIR, exist_ok=True) # Logger diff --git a/lrx_cli/enrichers/musixmatch.py b/lrx_cli/enrichers/musixmatch.py index c09c14a..6425eaf 100644 --- a/lrx_cli/enrichers/musixmatch.py +++ b/lrx_cli/enrichers/musixmatch.py @@ -11,7 +11,10 @@ from loguru import logger from .base import BaseEnricher from ..authenticators.musixmatch import MusixmatchAuthenticator from ..models import TrackMeta -from ..config import MUSIXMATCH_TRACK_MATCH_URL + +_MUSIXMATCH_TRACK_MATCH_URL = ( + "https://apic-desktop.musixmatch.com/ws/1.1/matcher.track.get" +) class MusixmatchSpotifyEnricher(BaseEnricher): @@ -36,7 +39,7 @@ class MusixmatchSpotifyEnricher(BaseEnricher): try: data = await self.auth.get_json( - MUSIXMATCH_TRACK_MATCH_URL, + _MUSIXMATCH_TRACK_MATCH_URL, {"track_spotify_id": track.trackid}, ) except Exception as e: diff --git a/lrx_cli/fetchers/lrclib.py b/lrx_cli/fetchers/lrclib.py index 011c52c..1292e86 100644 --- a/lrx_cli/fetchers/lrclib.py +++ b/lrx_cli/fetchers/lrclib.py @@ -21,10 +21,11 @@ from ..config import ( TTL_UNSYNCED, TTL_NOT_FOUND, TTL_NETWORK_ERROR, - LRCLIB_API_URL, UA_LRX, ) +_LRCLIB_API_URL = "https://lrclib.net/api/get" + class LrclibFetcher(BaseFetcher): @property @@ -48,7 +49,7 @@ class LrclibFetcher(BaseFetcher): "album_name": track.album, "duration": track.length / 1000.0 if track.length else 0, } - url = f"{LRCLIB_API_URL}?{urlencode(params)}" + url = f"{_LRCLIB_API_URL}?{urlencode(params)}" logger.info(f"LRCLIB: fetching lyrics for {track.display_name()}") try: diff --git a/lrx_cli/fetchers/lrclib_search.py b/lrx_cli/fetchers/lrclib_search.py index f65c52e..d25a9ed 100644 --- a/lrx_cli/fetchers/lrclib_search.py +++ b/lrx_cli/fetchers/lrclib_search.py @@ -24,10 +24,11 @@ from ..config import ( TTL_UNSYNCED, TTL_NOT_FOUND, TTL_NETWORK_ERROR, - LRCLIB_SEARCH_URL, UA_LRX, ) +_LRCLIB_SEARCH_URL = "https://lrclib.net/api/search" + class LrclibSearchFetcher(BaseFetcher): @property @@ -83,7 +84,7 @@ class LrclibSearchFetcher(BaseFetcher): async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: async def _query(params: dict[str, str]) -> tuple[list[dict], bool]: - url = f"{LRCLIB_SEARCH_URL}?{urlencode(params)}" + url = f"{_LRCLIB_SEARCH_URL}?{urlencode(params)}" logger.debug(f"LRCLIB-search: query {params}") try: resp = await client.get(url, headers={"User-Agent": UA_LRX}) diff --git a/lrx_cli/fetchers/musixmatch.py b/lrx_cli/fetchers/musixmatch.py index f30b78e..33476f1 100644 --- a/lrx_cli/fetchers/musixmatch.py +++ b/lrx_cli/fetchers/musixmatch.py @@ -22,12 +22,12 @@ from .selection import SearchCandidate, select_best from ..authenticators.musixmatch import MusixmatchAuthenticator from ..lrc import LRCData from ..models import CacheStatus, LyricResult, TrackMeta -from ..config import ( - MUSIXMATCH_MACRO_URL, - MUSIXMATCH_SEARCH_URL, - TTL_NETWORK_ERROR, - TTL_NOT_FOUND, +from ..config import TTL_NETWORK_ERROR, TTL_NOT_FOUND + +_MUSIXMATCH_MACRO_URL = ( + "https://apic-desktop.musixmatch.com/ws/1.1/macro.subtitles.get" ) +_MUSIXMATCH_SEARCH_URL = "https://apic-desktop.musixmatch.com/ws/1.1/track.search" # Macro-specific params (format/app_id injected by authenticator) _MXM_MACRO_PARAMS = { @@ -97,7 +97,7 @@ async def _fetch_macro( lyrics are found. Raises on HTTP/network errors. """ logger.debug(f"Musixmatch: macro call with {list(params.keys())}") - data = await auth.get_json(MUSIXMATCH_MACRO_URL, {**_MXM_MACRO_PARAMS, **params}) + data = await auth.get_json(_MUSIXMATCH_MACRO_URL, {**_MXM_MACRO_PARAMS, **params}) if data is None: return None @@ -220,7 +220,7 @@ class MusixmatchFetcher(BaseFetcher): params["q_album"] = track.album logger.debug(f"Musixmatch: searching for '{track.display_name()}'") - data = await self.auth.get_json(MUSIXMATCH_SEARCH_URL, params) + data = await self.auth.get_json(_MUSIXMATCH_SEARCH_URL, params) if data is None: return None, 0.0 diff --git a/lrx_cli/fetchers/netease.py b/lrx_cli/fetchers/netease.py index 575b3e1..e0601f8 100644 --- a/lrx_cli/fetchers/netease.py +++ b/lrx_cli/fetchers/netease.py @@ -26,14 +26,15 @@ from ..config import ( TTL_NOT_FOUND, TTL_NETWORK_ERROR, MULTI_CANDIDATE_DELAY_S, - NETEASE_SEARCH_URL, - NETEASE_LYRIC_URL, UA_BROWSER, ) -_HEADERS = { +_NETEASE_SEARCH_URL = "https://music.163.com/api/cloudsearch/pc" +_NETEASE_LYRIC_URL = "https://interface3.music.163.com/api/song/lyric" +_NETEASE_BASE_HEADERS = { "User-Agent": UA_BROWSER, "Referer": "https://music.163.com/", + "Origin": "https://music.163.com", } @@ -57,8 +58,8 @@ class NeteaseFetcher(BaseFetcher): try: async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: resp = await client.post( - NETEASE_SEARCH_URL, - headers=_HEADERS, + _NETEASE_SEARCH_URL, + headers=_NETEASE_BASE_HEADERS, data={"s": query, "type": "1", "limit": str(limit), "offset": "0"}, ) resp.raise_for_status() @@ -124,8 +125,8 @@ class NeteaseFetcher(BaseFetcher): try: async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: resp = await client.post( - NETEASE_LYRIC_URL, - headers=_HEADERS, + _NETEASE_LYRIC_URL, + headers=_NETEASE_BASE_HEADERS, data={ "id": str(song_id), "cp": "false", diff --git a/lrx_cli/fetchers/qqmusic.py b/lrx_cli/fetchers/qqmusic.py index d6d0a17..ad10922 100644 --- a/lrx_cli/fetchers/qqmusic.py +++ b/lrx_cli/fetchers/qqmusic.py @@ -25,9 +25,10 @@ from ..config import ( TTL_NOT_FOUND, TTL_NETWORK_ERROR, MULTI_CANDIDATE_DELAY_S, - QQ_MUSIC_API_LYRIC_ENDPOINT, - QQ_MUSIC_API_SEARCH_ENDPOINT, ) + +_QQ_MUSIC_API_SEARCH_ENDPOINT = "/api/search" +_QQ_MUSIC_API_LYRIC_ENDPOINT = "/api/lyric" from ..authenticators import QQMusicAuthenticator @@ -54,7 +55,7 @@ class QQMusicFetcher(BaseFetcher): try: async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: resp = await client.get( - f"{await self.auth.authenticate()}{QQ_MUSIC_API_SEARCH_ENDPOINT}", + f"{await self.auth.authenticate()}{_QQ_MUSIC_API_SEARCH_ENDPOINT}", params={"keyword": query, "type": "song", "num": limit}, ) resp.raise_for_status() @@ -113,7 +114,7 @@ class QQMusicFetcher(BaseFetcher): try: async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client: resp = await client.get( - f"{await self.auth.authenticate()}{QQ_MUSIC_API_LYRIC_ENDPOINT}", + f"{await self.auth.authenticate()}{_QQ_MUSIC_API_LYRIC_ENDPOINT}", params={"mid": mid}, ) resp.raise_for_status() diff --git a/lrx_cli/fetchers/spotify.py b/lrx_cli/fetchers/spotify.py index b126a5a..26d2194 100644 --- a/lrx_cli/fetchers/spotify.py +++ b/lrx_cli/fetchers/spotify.py @@ -9,23 +9,12 @@ from typing import Optional from loguru import logger from .base import BaseFetcher -from ..authenticators.spotify import SpotifyAuthenticator +from ..authenticators.spotify import SpotifyAuthenticator, SPOTIFY_BASE_HEADERS from ..models import TrackMeta, LyricResult, CacheStatus from ..lrc import LRCData -from ..config import ( - HTTP_TIMEOUT, - TTL_NOT_FOUND, - TTL_NETWORK_ERROR, - SPOTIFY_LYRICS_URL, - UA_BROWSER, -) +from ..config import HTTP_TIMEOUT, TTL_NOT_FOUND, TTL_NETWORK_ERROR -_SPOTIFY_BASE_HEADERS = { - "Referer": "https://open.spotify.com/", - "Origin": "https://open.spotify.com", - "App-Platform": "WebPlayer", - "Spotify-App-Version": "1.2.88.21.g8e037c8f", -} +_SPOTIFY_LYRICS_URL = "https://spclient.wg.spotify.com/color-lyrics/v2/track/" class SpotifyFetcher(BaseFetcher): @@ -71,12 +60,11 @@ class SpotifyFetcher(BaseFetcher): logger.error("Spotify: cannot fetch lyrics without a token") return LyricResult(status=CacheStatus.NETWORK_ERROR, ttl=TTL_NETWORK_ERROR) - url = f"{SPOTIFY_LYRICS_URL}{track.trackid}?format=json&vocalRemoval=false&market=from_token" + url = f"{_SPOTIFY_LYRICS_URL}{track.trackid}?format=json&vocalRemoval=false&market=from_token" headers = { - "User-Agent": UA_BROWSER, "Accept": "application/json", "Authorization": f"Bearer {token}", - **_SPOTIFY_BASE_HEADERS, + **SPOTIFY_BASE_HEADERS, } try: diff --git a/pyproject.toml b/pyproject.toml index 266b774..c33a2b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lrx-cli" -version = "0.5.4" +version = "0.5.5" description = "Fetch line-synced lyrics for your music player." readme = "README.md" requires-python = ">=3.13" diff --git a/uv.lock b/uv.lock index 46bd90d..a13600b 100644 --- a/uv.lock +++ b/uv.lock @@ -153,7 +153,7 @@ wheels = [ [[package]] name = "lrx-cli" -version = "0.5.4" +version = "0.5.5" source = { editable = "." } dependencies = [ { name = "cyclopts" },