diff --git a/lrcfetch/cli.py b/lrcfetch/cli.py index 9f9ab8a..8d8a3e3 100644 --- a/lrcfetch/cli.py +++ b/lrcfetch/cli.py @@ -13,7 +13,7 @@ from lrcfetch.core import LrcManager app = typer.Typer( help="LRCFetch — Fetch line-synced lyrics for your music player.", - add_completion=False, + add_completion=True, ) manager = LrcManager() diff --git a/lrcfetch/lrc.py b/lrcfetch/lrc.py index 578d4b4..5a9eff2 100644 --- a/lrcfetch/lrc.py +++ b/lrcfetch/lrc.py @@ -18,10 +18,55 @@ LRC_LINE_RE = re.compile(r"^\[(\d{2}:\d{2}[.:]\d{2,3})\]", re.MULTILINE) # All-zero tags _ZERO_TAG_RE = re.compile(r"^\[00:00[.:]0{2,3}\]$") +# [offset:+/-xxx] tag — value in milliseconds +_OFFSET_RE = re.compile(r"^\[offset:\s*([+-]?\d+)\]\s*$", re.MULTILINE | re.IGNORECASE) + +# Time tag for offset application: captures mm, ss, cc/ccc +_TIME_TAG_RE = re.compile(r"\[(\d{2}):(\d{2})\.(\d{2,3})\]") + + +def _apply_offset(text: str) -> str: + """Parse [offset:±ms] tag and shift all time tags accordingly. + + Per LRC spec, a positive offset means lyrics appear sooner (subtract + from timestamps), negative means later (add to timestamps). + """ + m = _OFFSET_RE.search(text) + if not m: + return text + offset_ms = int(m.group(1)) + if offset_ms == 0: + return _OFFSET_RE.sub("", text).strip("\n") + + # Remove the offset tag line + text = _OFFSET_RE.sub("", text) + + def _shift(match: re.Match) -> str: + mm, ss, cs = int(match.group(1)), int(match.group(2)), match.group(3) + # Normalize centiseconds to milliseconds + if len(cs) == 2: + ms = int(cs) * 10 + fmt_cs = 2 + else: + ms = int(cs) + fmt_cs = 3 + total_ms = (mm * 60 + ss) * 1000 + ms - offset_ms + total_ms = max(0, total_ms) + new_mm = total_ms // 60000 + new_ss = (total_ms % 60000) // 1000 + new_cs = total_ms % 1000 + if fmt_cs == 2: + new_cs = new_cs // 10 + return f"[{new_mm:02d}:{new_ss:02d}.{new_cs:02d}]" + return f"[{new_mm:02d}:{new_ss:02d}.{new_cs:03d}]" + + return _TIME_TAG_RE.sub(_shift, text) + def normalize_tags(text: str) -> str: - """Convert non-standard time tags [mm:ss:cc] to standard [mm:ss.cc].""" - return _COLON_TAG_RE.sub(r"[\1.\2]", text) + """Normalize LRC time tags: colon format → dot format, then apply offset.""" + text = _COLON_TAG_RE.sub(r"[\1.\2]", text) + return _apply_offset(text) def is_synced(text: str) -> bool: