Compare commits
5 Commits
794f42b42b
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
7fd98ffcb9
|
|||
|
cfac95ca03
|
|||
|
10bfb6090c
|
|||
|
c8ccf31583
|
|||
|
6e971941f8
|
@@ -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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lrx-cli"
|
||||
version = "0.7.6"
|
||||
version = "0.7.9"
|
||||
description = "Fetch line-synced lyrics for your music player."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -103,6 +103,12 @@ 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 = [
|
||||
p
|
||||
for p in all_names
|
||||
|
||||
@@ -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:
|
||||
@@ -178,6 +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}")
|
||||
continue
|
||||
if not self._target.allows(name):
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user