dsf
This commit is contained in:
150
MPV/lyric.py
150
MPV/lyric.py
@@ -467,6 +467,66 @@ def _extract_lrc_from_notes(notes: Dict[str, str]) -> Optional[str]:
|
||||
return text if text.strip() else None
|
||||
|
||||
|
||||
def _extract_sub_from_notes(notes: Dict[str, str]) -> Optional[str]:
|
||||
"""Return raw subtitle text from the note named 'sub'."""
|
||||
if not isinstance(notes, dict) or not notes:
|
||||
return None
|
||||
|
||||
raw = None
|
||||
for k, v in notes.items():
|
||||
if not isinstance(k, str):
|
||||
continue
|
||||
if k.strip() == "sub":
|
||||
raw = v
|
||||
break
|
||||
|
||||
if not isinstance(raw, str):
|
||||
return None
|
||||
|
||||
text = raw.strip("\ufeff\r\n")
|
||||
return text if text.strip() else None
|
||||
|
||||
|
||||
def _infer_sub_extension(text: str) -> str:
|
||||
# Best-effort: mpv generally understands SRT/VTT; choose based on content.
|
||||
t = (text or "").lstrip("\ufeff\r\n").lstrip()
|
||||
if t.upper().startswith("WEBVTT"):
|
||||
return ".vtt"
|
||||
if "-->" in t:
|
||||
# SRT typically uses commas for milliseconds, VTT uses dots.
|
||||
if re.search(r"\d\d:\d\d:\d\d,\d\d\d\s*-->\s*\d\d:\d\d:\d\d,\d\d\d", t):
|
||||
return ".srt"
|
||||
return ".vtt"
|
||||
return ".vtt"
|
||||
|
||||
|
||||
def _write_temp_sub_file(*, key: str, text: str) -> Path:
|
||||
# Write to a content-addressed temp path so updates force mpv reload.
|
||||
tmp_dir = Path(tempfile.gettempdir()) / "medeia-mpv-notes"
|
||||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ext = _infer_sub_extension(text)
|
||||
digest = hashlib.sha1((key + "\n" + (text or "")).encode("utf-8", errors="ignore")).hexdigest()[:16]
|
||||
safe_key = hashlib.sha1((key or "").encode("utf-8", errors="ignore")).hexdigest()[:12]
|
||||
path = (tmp_dir / f"sub-{safe_key}-{digest}{ext}").resolve()
|
||||
path.write_text(text or "", encoding="utf-8", errors="replace")
|
||||
return path
|
||||
|
||||
|
||||
def _try_remove_selected_external_sub(client: MPVIPCClient) -> None:
|
||||
try:
|
||||
client.send_command({"command": ["sub-remove"]})
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def _try_add_external_sub(client: MPVIPCClient, path: Path) -> None:
|
||||
try:
|
||||
client.send_command({"command": ["sub-add", str(path), "select", "medeia-sub"]})
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def _is_stream_target(target: str) -> bool:
|
||||
"""Return True when mpv's 'path' is not a local filesystem file.
|
||||
|
||||
@@ -726,7 +786,7 @@ def _infer_hash_for_target(target: str) -> Optional[str]:
|
||||
|
||||
|
||||
def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] = None) -> int:
|
||||
"""Auto mode: track mpv's current file and render lyrics from store notes (note name: 'lyric')."""
|
||||
"""Auto mode: track mpv's current file and render lyrics (note: 'lyric') or load subtitles (note: 'sub')."""
|
||||
cfg = config or {}
|
||||
|
||||
client = mpv.client()
|
||||
@@ -742,6 +802,8 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
current_key: Optional[str] = None
|
||||
current_backend: Optional[Any] = None
|
||||
last_loaded_key: Optional[str] = None
|
||||
last_loaded_mode: Optional[str] = None # 'lyric' | 'sub'
|
||||
last_loaded_sub_path: Optional[Path] = None
|
||||
last_fetch_attempt_key: Optional[str] = None
|
||||
last_fetch_attempt_at: float = 0.0
|
||||
|
||||
@@ -808,6 +870,9 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
_osd_overlay_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
if last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
last_loaded_sub_path = None
|
||||
last_target = target
|
||||
current_store_name = None
|
||||
current_file_hash = None
|
||||
@@ -816,6 +881,7 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
entries = []
|
||||
times = []
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -833,6 +899,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
last_loaded_sub_path = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -850,6 +920,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
last_loaded_sub_path = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -869,6 +943,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
last_loaded_sub_path = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -887,6 +965,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
last_loaded_sub_path = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -913,6 +995,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
last_loaded_sub_path = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -930,9 +1016,41 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
except Exception:
|
||||
_log("Loaded notes keys: <error>")
|
||||
|
||||
lrc_text = _extract_lrc_from_notes(notes)
|
||||
if not lrc_text:
|
||||
_log("No lyric note found (note name: 'lyric')")
|
||||
sub_text = _extract_sub_from_notes(notes)
|
||||
if sub_text:
|
||||
# Treat subtitles as an alternative to lyrics; do not show the lyric overlay.
|
||||
try:
|
||||
_osd_overlay_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
sub_path = _write_temp_sub_file(key=current_key, text=sub_text)
|
||||
except Exception as exc:
|
||||
_log(f"Failed to write sub note temp file: {exc}")
|
||||
sub_path = None
|
||||
|
||||
if sub_path is not None:
|
||||
# If we previously loaded a sub, remove it first to avoid stacking.
|
||||
if last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
_try_add_external_sub(client, sub_path)
|
||||
last_loaded_sub_path = sub_path
|
||||
|
||||
entries = []
|
||||
times = []
|
||||
last_loaded_key = current_key
|
||||
last_loaded_mode = "sub"
|
||||
|
||||
else:
|
||||
# Switching away from sub-note mode: best-effort unload the selected external subtitle.
|
||||
if last_loaded_mode == "sub" and last_loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
last_loaded_sub_path = None
|
||||
|
||||
lrc_text = _extract_lrc_from_notes(notes)
|
||||
if not lrc_text:
|
||||
_log("No lyric note found (note name: 'lyric')")
|
||||
|
||||
# Auto-fetch path: fetch and persist lyrics into the note named 'lyric'.
|
||||
# Throttle attempts per key to avoid hammering APIs.
|
||||
@@ -981,18 +1099,20 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
else:
|
||||
_log("Autofetch: no lyrics found")
|
||||
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
last_loaded_key = None
|
||||
else:
|
||||
_log(f"Loaded lyric note ({len(lrc_text)} chars)")
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
else:
|
||||
_log(f"Loaded lyric note ({len(lrc_text)} chars)")
|
||||
|
||||
parsed = parse_lrc(lrc_text)
|
||||
entries = parsed
|
||||
times = [e.time_s for e in entries]
|
||||
last_loaded_key = current_key
|
||||
parsed = parse_lrc(lrc_text)
|
||||
entries = parsed
|
||||
times = [e.time_s for e in entries]
|
||||
last_loaded_key = current_key
|
||||
last_loaded_mode = "lyric"
|
||||
|
||||
try:
|
||||
# mpv returns None when idle/no file.
|
||||
|
||||
Reference in New Issue
Block a user