feat: export lyrics default to sidecar path
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "0.1.3"
|
||||
__version__ = "0.1.4"
|
||||
|
||||
+10
-1
@@ -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: <Artist> - <Title>.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"
|
||||
|
||||
+16
-14
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user