diff --git a/lrx_cli/cli.py b/lrx_cli/cli.py index d4ec25a..b63ef95 100644 --- a/lrx_cli/cli.py +++ b/lrx_cli/cli.py @@ -18,7 +18,7 @@ from .models import TrackMeta, CacheStatus from .mpris import get_current_track from .core import LrcManager from .fetchers import FetcherMethodType -from .lrc import get_sidecar_path +from .lrc import get_sidecar_path, print_lyrics, to_plain app = cyclopts.App( @@ -94,6 +94,12 @@ def fetch( name="--only-synced", negative="", help="Only accept synced (timed) lyrics." ), ] = False, + plain: Annotated[ + bool, + cyclopts.Parameter( + name="--plain", negative="", help="Output only the raw lyrics without tags." + ), + ] = False, ): """Fetch and print lyrics for the currently playing track.""" track = get_current_track(_player) @@ -114,7 +120,7 @@ def fetch( logger.error("Only unsynced lyrics available (--only-synced requested).") sys.exit(1) - print(result.lyrics) + print_lyrics(result.lyrics, plain=plain) # search @@ -165,6 +171,12 @@ def search( name="--only-synced", negative="", help="Only accept synced (timed) lyrics." ), ] = False, + plain: Annotated[ + bool, + cyclopts.Parameter( + name="--plain", negative="", help="Output only the raw lyrics without tags." + ), + ] = False, ): """Search for lyrics by metadata (bypasses MPRIS).""" if url and path: @@ -196,7 +208,7 @@ def search( logger.error("Only unsynced lyrics available (--only-synced requested).") sys.exit(1) - print(result.lyrics) + print_lyrics(result.lyrics, plain=plain) # export @@ -224,6 +236,12 @@ def export( name=["--overwrite", "-f"], negative="", help="Overwrite existing file." ), ] = False, + plain: Annotated[ + bool, + cyclopts.Parameter( + name="--plain", negative="", help="Export only the raw lyrics without tags." + ), + ] = False, ): """Export lyrics of the current track to a .lrc file.""" track = get_current_track(_player) @@ -263,7 +281,10 @@ def export( try: with open(output, "w", encoding="utf-8") as f: - f.write(result.lyrics) + if plain: + f.write(to_plain(result.lyrics)) + else: + f.write(result.lyrics) logger.info(f"Exported lyrics to {output}") except Exception as e: logger.error(f"Failed to write file: {e}") diff --git a/lrx_cli/lrc.py b/lrx_cli/lrc.py index 6913512..2965d52 100644 --- a/lrx_cli/lrc.py +++ b/lrx_cli/lrc.py @@ -27,6 +27,9 @@ _LRC_LINE_RE = re.compile(r"^\[\d{2,}:\d{2}\.\d{2}\]", re.MULTILINE) # [offset:+/-xxx] tag — value in milliseconds _OFFSET_RE = re.compile(r"^\[offset:\s*([+-]?\d+)\]\s*$", re.MULTILINE | re.IGNORECASE) +# Matches any number of tags at the start of a line +_LINE_START_TAGS_RE = re.compile(r"^(?:\[[^\]]*\])+") + def _raw_tag_to_cs(mm: str, ss: str, frac: Optional[str]) -> str: """Convert parsed time tag components to standard [mm:ss.cc] string.""" @@ -176,3 +179,41 @@ def get_sidecar_path( if ensure_exists and not lrc_path.exists(): return None return lrc_path + + +def to_plain( + text: str, +) -> str: + """Convert lyrics to plain text with all tags stripped. + + Assumes text has been normalized by normalize_tags. + """ + + lines = [] + first = True + for line in text.splitlines(): + cleaned = _LINE_START_TAGS_RE.sub("", line).strip() + # Ignore the leading empty lines that is likely caused by tag lines + if not cleaned and not first: + lines.append("") + elif cleaned: + lines.append(cleaned) + first = False + # Remove trailing empty lines that are meaningless + while lines and not lines[-1]: + lines.pop() + return "\n".join(lines) + + +def print_lyrics( + text: str, + plain: bool = False, +) -> None: + """Print lyrics, optionally stripping tags. + + Assumes text has been normalized by normalize_tags. + """ + if plain: + print(to_plain(text)) + else: + print(text)