From 1a301deb40d646cd3f35df79d3317fe35eb15b23 Mon Sep 17 00:00:00 2001 From: Uyanide Date: Tue, 31 Mar 2026 02:29:15 +0200 Subject: [PATCH] feat: export lyrics default to sidecar path --- lrcfetch/__init__.py | 2 +- lrcfetch/cli.py | 11 ++++++++++- lrcfetch/fetchers/local.py | 30 ++++++++++++++++-------------- lrcfetch/lrc.py | 31 +++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/lrcfetch/__init__.py b/lrcfetch/__init__.py index ae73625..bbab024 100644 --- a/lrcfetch/__init__.py +++ b/lrcfetch/__init__.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" diff --git a/lrcfetch/cli.py b/lrcfetch/cli.py index 4f18f00..7900f19 100644 --- a/lrcfetch/cli.py +++ b/lrcfetch/cli.py @@ -15,6 +15,7 @@ from .config import enable_debug from .models import TrackMeta, CacheStatus from .mpris import get_current_track from .core import LrcManager, FetcherMethodType +from .lrc import get_sidecar_path app = cyclopts.App( @@ -174,7 +175,7 @@ def export( str | None, cyclopts.Parameter( name=["--output", "-o"], - help="Output file path (default: - .lrc).", + help="Output file path (default: same directory as audio file with .lrc extension, or current directory if not available).", ), ] = None, method: Annotated[ @@ -202,6 +203,14 @@ def export( sys.exit(1) # Build default output path + if not output: + if track.url: + lrc_path = get_sidecar_path(track.url, ensure_exists=False) + if lrc_path: + output = str(lrc_path) + logger.info(f"Exporting to sidecar path: {output}") + + # Fallback to current directory with sanitized filename if not output: filename = ( f"{track.artist} - {track.title}.lrc" diff --git a/lrcfetch/fetchers/local.py b/lrcfetch/fetchers/local.py index ea25c4e..f3569b8 100644 --- a/lrcfetch/fetchers/local.py +++ b/lrcfetch/fetchers/local.py @@ -10,16 +10,14 @@ Priority: 2. Embedded lyrics in audio metadata (FLAC, MP3 USLT/SYLT tags) """ -import os from typing import Optional -from urllib.parse import unquote from loguru import logger from mutagen._file import File from mutagen.flac import FLAC from .base import BaseFetcher from ..models import TrackMeta, LyricResult -from ..lrc import detect_sync_status +from ..lrc import detect_sync_status, get_audio_path, get_sidecar_path class LocalFetcher(BaseFetcher): @@ -32,16 +30,15 @@ class LocalFetcher(BaseFetcher): if not track.is_local or not track.url: return None - file_path = unquote(track.url.replace("file://", "", 1)) - if not os.path.exists(file_path): - logger.debug(f"Local: file does not exist: {file_path}") + audio_path = get_audio_path(track.url, ensure_exists=False) + if not audio_path: + logger.debug(f"Local: audio URL is not a valid file path: {track.url}") return None - logger.info(f"Local: checking for lyrics near {file_path}") - - # Sidecar .lrc file - lrc_path = os.path.splitext(file_path)[0] + ".lrc" - if os.path.exists(lrc_path): + lrc_path = get_sidecar_path( + track.url, ensure_audio_exists=False, ensure_exists=True + ) + if lrc_path: try: with open(lrc_path, "r", encoding="utf-8") as f: content = f.read().strip() @@ -53,10 +50,15 @@ class LocalFetcher(BaseFetcher): ) except Exception as e: logger.error(f"Local: error reading {lrc_path}: {e}") + else: + logger.debug(f"Local: no .lrc sidecar found for {audio_path}") # Embedded metadata + if not audio_path.exists(): + logger.debug(f"Local: audio file does not exist: {audio_path}") + return None try: - audio = File(file_path) + audio = File(audio_path) if audio is not None: lyrics = None @@ -83,7 +85,7 @@ class LocalFetcher(BaseFetcher): else: logger.debug("Local: no embedded lyrics found") except Exception as e: - logger.error(f"Local: error reading metadata for {file_path}: {e}") + logger.error(f"Local: error reading metadata for {audio_path}: {e}") - logger.debug(f"Local: no lyrics found for {file_path}") + logger.debug(f"Local: no lyrics found for {audio_path}") return None diff --git a/lrcfetch/lrc.py b/lrcfetch/lrc.py index 11ece6f..65deeb6 100644 --- a/lrcfetch/lrc.py +++ b/lrcfetch/lrc.py @@ -5,6 +5,9 @@ Description: Shared LRC time-tag utilities """ import re +from pathlib import Path +from typing import Optional +from urllib.parse import unquote from .models import CacheStatus @@ -93,3 +96,31 @@ def detect_sync_status(text: str) -> CacheStatus: return ( CacheStatus.SUCCESS_SYNCED if is_synced(text) else CacheStatus.SUCCESS_UNSYNCED ) + + +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 +) -> 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(".lrc") + if ensure_exists and not lrc_path.exists(): + return None + return lrc_path diff --git a/pyproject.toml b/pyproject.toml index d75ea1b..a4fde28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lrcfetch" -version = "0.1.3" +version = "0.1.4" description = "Fetch line-synced lyrics for your music player." readme = "README.md" requires-python = ">=3.13"