Compare commits
3 Commits
c8ccf31583
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
7fd98ffcb9
|
|||
|
cfac95ca03
|
|||
|
10bfb6090c
|
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user