""" Author: Uyanide pywang0608@foxmail.com Date: 2026-03-26 02:08:41 Description: Local fetcher — reads lyrics from .lrc sidecar files or embedded audio metadata """ """ Priority: 1. Same-directory .lrc file (e.g. /path/to/track.lrc) 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 class LocalFetcher(BaseFetcher): @property def source_name(self) -> str: return "local" def fetch(self, track: TrackMeta) -> Optional[LyricResult]: """Attempt to read lyrics from local filesystem.""" 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}") 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): try: with open(lrc_path, "r", encoding="utf-8") as f: content = f.read().strip() if content: status = detect_sync_status(content) logger.info(f"Local: found .lrc sidecar ({status.value})") return LyricResult( status=status, lyrics=content, source=self.source_name ) except Exception as e: logger.error(f"Local: error reading {lrc_path}: {e}") # Embedded metadata try: audio = File(file_path) if audio is not None: lyrics = None if isinstance(audio, FLAC): # FLAC stores lyrics in vorbis comment tags lyrics = ( audio.get("lyrics") or audio.get("unsynclyrics") or [None] )[0] elif hasattr(audio, "tags") and audio.tags: # MP3 / other: look for USLT or SYLT ID3 frames for key in audio.tags.keys(): if key.startswith("USLT") or key.startswith("SYLT"): lyrics = str(audio.tags[key]) break if lyrics: status = detect_sync_status(lyrics) logger.info(f"Local: found embedded lyrics ({status.value})") return LyricResult( status=status, lyrics=lyrics.strip(), source=f"{self.source_name} (embedded)", ) else: logger.debug("Local: no embedded lyrics found") except Exception as e: logger.error(f"Local: error reading metadata for {file_path}: {e}") logger.debug(f"Local: no lyrics found for {file_path}") return None