feat: export lyrics default to sidecar path

This commit is contained in:
2026-03-31 02:29:15 +02:00
parent 4f6244bc9e
commit e482fb1a82
5 changed files with 59 additions and 17 deletions
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.3" __version__ = "0.1.4"
+10 -1
View File
@@ -15,6 +15,7 @@ from .config import enable_debug
from .models import TrackMeta, CacheStatus from .models import TrackMeta, CacheStatus
from .mpris import get_current_track from .mpris import get_current_track
from .core import LrcManager, FetcherMethodType from .core import LrcManager, FetcherMethodType
from .lrc import get_sidecar_path
app = cyclopts.App( app = cyclopts.App(
@@ -174,7 +175,7 @@ def export(
str | None, str | None,
cyclopts.Parameter( cyclopts.Parameter(
name=["--output", "-o"], name=["--output", "-o"],
help="Output file path (default: <Artist> - <Title>.lrc).", help="Output file path (default: same directory as audio file with .lrc extension, or current directory if not available).",
), ),
] = None, ] = None,
method: Annotated[ method: Annotated[
@@ -202,6 +203,14 @@ def export(
sys.exit(1) sys.exit(1)
# Build default output path # 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: if not output:
filename = ( filename = (
f"{track.artist} - {track.title}.lrc" f"{track.artist} - {track.title}.lrc"
+16 -14
View File
@@ -10,16 +10,14 @@ Priority:
2. Embedded lyrics in audio metadata (FLAC, MP3 USLT/SYLT tags) 2. Embedded lyrics in audio metadata (FLAC, MP3 USLT/SYLT tags)
""" """
import os
from typing import Optional from typing import Optional
from urllib.parse import unquote
from loguru import logger from loguru import logger
from mutagen._file import File from mutagen._file import File
from mutagen.flac import FLAC from mutagen.flac import FLAC
from .base import BaseFetcher from .base import BaseFetcher
from ..models import TrackMeta, LyricResult 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): class LocalFetcher(BaseFetcher):
@@ -32,16 +30,15 @@ class LocalFetcher(BaseFetcher):
if not track.is_local or not track.url: if not track.is_local or not track.url:
return None return None
file_path = unquote(track.url.replace("file://", "", 1)) audio_path = get_audio_path(track.url, ensure_exists=False)
if not os.path.exists(file_path): if not audio_path:
logger.debug(f"Local: file does not exist: {file_path}") logger.debug(f"Local: audio URL is not a valid file path: {track.url}")
return None return None
logger.info(f"Local: checking for lyrics near {file_path}") lrc_path = get_sidecar_path(
track.url, ensure_audio_exists=False, ensure_exists=True
# Sidecar .lrc file )
lrc_path = os.path.splitext(file_path)[0] + ".lrc" if lrc_path:
if os.path.exists(lrc_path):
try: try:
with open(lrc_path, "r", encoding="utf-8") as f: with open(lrc_path, "r", encoding="utf-8") as f:
content = f.read().strip() content = f.read().strip()
@@ -53,10 +50,15 @@ class LocalFetcher(BaseFetcher):
) )
except Exception as e: except Exception as e:
logger.error(f"Local: error reading {lrc_path}: {e}") logger.error(f"Local: error reading {lrc_path}: {e}")
else:
logger.debug(f"Local: no .lrc sidecar found for {audio_path}")
# Embedded metadata # Embedded metadata
if not audio_path.exists():
logger.debug(f"Local: audio file does not exist: {audio_path}")
return None
try: try:
audio = File(file_path) audio = File(audio_path)
if audio is not None: if audio is not None:
lyrics = None lyrics = None
@@ -83,7 +85,7 @@ class LocalFetcher(BaseFetcher):
else: else:
logger.debug("Local: no embedded lyrics found") logger.debug("Local: no embedded lyrics found")
except Exception as e: 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 return None
+31
View File
@@ -5,6 +5,9 @@ Description: Shared LRC time-tag utilities
""" """
import re import re
from pathlib import Path
from typing import Optional
from urllib.parse import unquote
from .models import CacheStatus from .models import CacheStatus
@@ -93,3 +96,31 @@ def detect_sync_status(text: str) -> CacheStatus:
return ( return (
CacheStatus.SUCCESS_SYNCED if is_synced(text) else CacheStatus.SUCCESS_UNSYNCED 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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lrcfetch" name = "lrcfetch"
version = "0.1.3" version = "0.1.4"
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"