Compare commits

..

3 Commits

Author SHA1 Message Date
Uyanide 7fd98ffcb9 feat: revert c8ccf31 2026-04-10 18:51:42 +02:00
Uyanide cfac95ca03 refactor: add __all__ and more decouple 2026-04-10 17:32:49 +02:00
Uyanide 10bfb6090c chore: preferred_player default to none 2026-04-10 15:49:22 +02:00
15 changed files with 92 additions and 56 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ optional; all values have defaults. Unknown keys are rejected with an error.
```toml ```toml
[general] [general]
preferred_player = "spotify" # preferred MPRIS player when multiple are active preferred_player = "" # preferred MPRIS player when multiple are active
player_blacklist = ["firefox", "zen", "chrome", "chromium", "vivaldi", "edge", "opera", "mpv"] # bypassed by --player/-p player_blacklist = ["firefox", "zen", "chrome", "chromium", "vivaldi", "edge", "opera", "mpv"] # bypassed by --player/-p
http_timeout = 10.0 # seconds http_timeout = 10.0 # seconds
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lrx-cli" name = "lrx-cli"
version = "0.7.8" version = "0.7.9"
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"
+21
View File
@@ -0,0 +1,21 @@
from .config import AppConfig, GeneralConfig, CredentialConfig, load_config
from .core import LrcManager
from .models import CacheStatus, TrackMeta, LyricResult
from .lrc import LRCData, LyricLine
from .fetchers import FetcherMethodType
from .utils import get_sidecar_path
__all__ = [
"AppConfig",
"GeneralConfig",
"CredentialConfig",
"load_config",
"LrcManager",
"CacheStatus",
"TrackMeta",
"LRCData",
"LyricLine",
"LyricResult",
"FetcherMethodType",
"get_sidecar_path",
]
+1 -1
View File
@@ -24,7 +24,7 @@ from .config import (
SLOT_UNSYNCED, SLOT_UNSYNCED,
) )
from .models import TrackMeta, LyricResult, CacheStatus from .models import TrackMeta, LyricResult, CacheStatus
from .ranking import is_positive_status, select_best_positive from .utils import is_positive_status, select_best_positive
_ALL_SLOTS = (SLOT_SYNCED, SLOT_UNSYNCED) _ALL_SLOTS = (SLOT_SYNCED, SLOT_UNSYNCED)
+1 -1
View File
@@ -23,11 +23,11 @@ from .config import (
load_config, load_config,
enable_debug, enable_debug,
) )
from .utils import get_sidecar_path
from .models import TrackMeta from .models import TrackMeta
from .mpris import get_current_track from .mpris import get_current_track
from .core import LrcManager from .core import LrcManager
from .fetchers import FetcherMethodType from .fetchers import FetcherMethodType
from .lrc import get_sidecar_path
from .watch import WatchCoordinator from .watch import WatchCoordinator
from .watch.control import ControlClient, parse_delta from .watch.control import ControlClient, parse_delta
from .watch.view.pipe import PipeOutput from .watch.view.pipe import PipeOutput
+1 -1
View File
@@ -69,7 +69,7 @@ MUSIXMATCH_COOLDOWN_MS = 600_000 # 10 minutes
os.makedirs(CACHE_DIR, exist_ok=True) os.makedirs(CACHE_DIR, exist_ok=True)
DEFAULT_PREFERRED_PLAYER = "spotify" DEFAULT_PREFERRED_PLAYER = ""
DEFAULT_PLAYER_BLACKLIST: tuple[str, ...] = ( DEFAULT_PLAYER_BLACKLIST: tuple[str, ...] = (
"firefox", "firefox",
"zen", "zen",
+1 -1
View File
@@ -28,7 +28,7 @@ from .config import (
) )
from .models import TrackMeta, LyricResult, CacheStatus from .models import TrackMeta, LyricResult, CacheStatus
from .enrichers import create_enrichers, enrich_track from .enrichers import create_enrichers, enrich_track
from .ranking import is_better_result, select_best_positive from .utils import is_better_result, select_best_positive
# Maps CacheStatus to the default TTL used when storing results # Maps CacheStatus to the default TTL used when storing results
+1 -1
View File
@@ -12,7 +12,7 @@ from mutagen._file import File, FileType
from .base import BaseEnricher from .base import BaseEnricher
from ..models import TrackMeta from ..models import TrackMeta
from ..lrc import get_audio_path from ..utils import get_audio_path
class AudioTagEnricher(BaseEnricher): class AudioTagEnricher(BaseEnricher):
+1 -1
View File
@@ -12,7 +12,7 @@ from loguru import logger
from .base import BaseEnricher from .base import BaseEnricher
from ..models import TrackMeta from ..models import TrackMeta
from ..lrc import get_audio_path from ..utils import get_audio_path
# Common track-number prefixes: "01 - ", "01. ", "1 - ", etc. # Common track-number prefixes: "01 - ", "01. ", "1 - ", etc.
+2 -1
View File
@@ -16,7 +16,8 @@ from mutagen.flac import FLAC
from .base import BaseFetcher, FetchResult from .base import BaseFetcher, FetchResult
from ..models import CacheStatus, TrackMeta, LyricResult from ..models import CacheStatus, TrackMeta, LyricResult
from ..lrc import get_audio_path, get_sidecar_path, LRCData from ..lrc import LRCData
from ..utils import get_audio_path, get_sidecar_path
class LocalFetcher(BaseFetcher): class LocalFetcher(BaseFetcher):
-33
View File
@@ -9,9 +9,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
import re import re
from pathlib import Path
from typing import Optional from typing import Optional
from urllib.parse import unquote
from .models import CacheStatus from .models import CacheStatus
@@ -465,34 +463,3 @@ class LRCData:
""" """
normalized = self.normalize() normalized = self.normalize()
return self._serialize_lines(normalized._lines, include_word_sync=False) return self._serialize_lines(normalized._lines, include_word_sync=False)
def get_audio_path(audio_url: str, ensure_exists: bool = False) -> Optional[Path]:
"""Convert file:// URL to Path, return None if invalid or (if ensure_exists) file doesn't exist."""
if not audio_url.startswith("file://"):
return None
file_path = unquote(audio_url.replace("file://", "", 1))
path = Path(file_path)
if ensure_exists and not path.exists():
return None
return path
def get_sidecar_path(
audio_url: str,
ensure_audio_exists: bool = False,
ensure_exists: bool = False,
extension: str = ".lrc",
) -> Optional[Path]:
"""Given a file:// URL, return the corresponding .lrc sidecar path.
If ensure_audio_exists is True, return None if the audio file does not exist.
If ensure_exists is True, return None if the .lrc file does not exist.
"""
audio_path = get_audio_path(audio_url, ensure_exists=ensure_audio_exists)
if not audio_path:
return None
lrc_path = audio_path.with_suffix(extension)
if ensure_exists and not lrc_path.exists():
return None
return lrc_path
+11 -6
View File
@@ -103,12 +103,17 @@ async def _select_player(
return matched[0] if matched else None return matched[0] if matched else None
# auto-selection: apply blacklist before choosing # auto-selection: apply blacklist before choosing
candidates = [] # candidates = []
for p in all_names: # for p in all_names:
if any(x.lower() in p.lower() for x in player_blacklist): # if any(x.lower() in p.lower() for x in player_blacklist):
logger.info(f"Excluding blacklisted player: {p}") # logger.info(f"Excluding blacklisted player: {p}")
else: # else:
candidates.append(p) # candidates.append(p)
candidates = [
p
for p in all_names
if not any(x.lower() in p.lower() for x in player_blacklist)
]
playing: list[str] = [] playing: list[str] = []
for p in candidates: for p in candidates:
status = await _get_playback_status(bus, p) status = await _get_playback_status(bus, p)
@@ -1,14 +1,56 @@
"""Shared ranking rules for LyricResult selection. """
Author: Uyanide pywang0608@foxmail.com
This module centralizes how positive lyric results are compared so cache/core Date: 2026-04-10 17:06:37
and other callers use the same precedence and edge-case handling. Description: Utility functions
""" """
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import TYPE_CHECKING, Optional
from urllib.parse import unquote
from pathlib import Path
from .models import CacheStatus, LyricResult from .models import CacheStatus
if TYPE_CHECKING:
from .models import LyricResult
# Paths
def get_audio_path(audio_url: str, ensure_exists: bool = False) -> Optional[Path]:
"""Convert file:// URL to Path, return None if invalid or (if ensure_exists) file doesn't exist."""
if not audio_url.startswith("file://"):
return None
file_path = unquote(audio_url.replace("file://", "", 1))
path = Path(file_path)
if ensure_exists and not path.exists():
return None
return path
def get_sidecar_path(
audio_url: str,
ensure_audio_exists: bool = False,
ensure_exists: bool = False,
extension: str = ".lrc",
) -> Optional[Path]:
"""Given a file:// URL, return the corresponding .lrc sidecar path.
If ensure_audio_exists is True, return None if the audio file does not exist.
If ensure_exists is True, return None if the .lrc file does not exist.
"""
audio_path = get_audio_path(audio_url, ensure_exists=ensure_audio_exists)
if not audio_path:
return None
lrc_path = audio_path.with_suffix(extension)
if ensure_exists and not lrc_path.exists():
return None
return lrc_path
# Ranking
def is_positive_status(status: CacheStatus) -> bool: def is_positive_status(status: CacheStatus) -> bool:
+1 -1
View File
@@ -178,7 +178,7 @@ class PlayerMonitor:
if not hint_active and any( if not hint_active and any(
x.lower() in name.lower() for x in self._player_blacklist x.lower() in name.lower() for x in self._player_blacklist
): ):
logger.info(f"Excluding blacklisted player: {name}") # logger.info(f"Excluding blacklisted player: {name}")
continue continue
if not self._target.allows(name): if not self._target.allows(name):
continue continue
Generated
+1 -1
View File
@@ -153,7 +153,7 @@ wheels = [
[[package]] [[package]]
name = "lrx-cli" name = "lrx-cli"
version = "0.7.8" version = "0.7.9"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "cyclopts" }, { name = "cyclopts" },