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
[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
http_timeout = 10.0 # seconds
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "lrx-cli"
version = "0.7.8"
version = "0.7.9"
description = "Fetch line-synced lyrics for your music player."
readme = "README.md"
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,
)
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)
+1 -1
View File
@@ -23,11 +23,11 @@ from .config import (
load_config,
enable_debug,
)
from .utils import get_sidecar_path
from .models import TrackMeta
from .mpris import get_current_track
from .core import LrcManager
from .fetchers import FetcherMethodType
from .lrc import get_sidecar_path
from .watch import WatchCoordinator
from .watch.control import ControlClient, parse_delta
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)
DEFAULT_PREFERRED_PLAYER = "spotify"
DEFAULT_PREFERRED_PLAYER = ""
DEFAULT_PLAYER_BLACKLIST: tuple[str, ...] = (
"firefox",
"zen",
+1 -1
View File
@@ -28,7 +28,7 @@ from .config import (
)
from .models import TrackMeta, LyricResult, CacheStatus
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
+1 -1
View File
@@ -12,7 +12,7 @@ from mutagen._file import File, FileType
from .base import BaseEnricher
from ..models import TrackMeta
from ..lrc import get_audio_path
from ..utils import get_audio_path
class AudioTagEnricher(BaseEnricher):
+1 -1
View File
@@ -12,7 +12,7 @@ from loguru import logger
from .base import BaseEnricher
from ..models import TrackMeta
from ..lrc import get_audio_path
from ..utils import get_audio_path
# 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 ..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):
-33
View File
@@ -9,9 +9,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
import re
from pathlib import Path
from typing import Optional
from urllib.parse import unquote
from .models import CacheStatus
@@ -465,34 +463,3 @@ class LRCData:
"""
normalized = self.normalize()
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
# auto-selection: apply blacklist before choosing
candidates = []
for p in all_names:
if any(x.lower() in p.lower() for x in player_blacklist):
logger.info(f"Excluding blacklisted player: {p}")
else:
candidates.append(p)
# candidates = []
# for p in all_names:
# if any(x.lower() in p.lower() for x in player_blacklist):
# logger.info(f"Excluding blacklisted player: {p}")
# else:
# 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] = []
for p in candidates:
status = await _get_playback_status(bus, p)
@@ -1,14 +1,56 @@
"""Shared ranking rules for LyricResult selection.
This module centralizes how positive lyric results are compared so cache/core
and other callers use the same precedence and edge-case handling.
"""
Author: Uyanide pywang0608@foxmail.com
Date: 2026-04-10 17:06:37
Description: Utility functions
"""
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:
+1 -1
View File
@@ -178,7 +178,7 @@ class PlayerMonitor:
if not hint_active and any(
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
if not self._target.allows(name):
continue
Generated
+1 -1
View File
@@ -153,7 +153,7 @@ wheels = [
[[package]]
name = "lrx-cli"
version = "0.7.8"
version = "0.7.9"
source = { editable = "." }
dependencies = [
{ name = "cyclopts" },