j
This commit is contained in:
449
MPV/lyric.py
449
MPV/lyric.py
@@ -54,9 +54,18 @@ _SINGLE_INSTANCE_LOCK_FH: Optional[TextIO] = None
|
||||
|
||||
_LYRIC_VISIBLE_PROP = "user-data/medeia-lyric-visible"
|
||||
|
||||
# mpv osd-overlay IDs are scoped to the IPC client connection.
|
||||
# MPV.lyric keeps a persistent connection, so we can safely reuse a constant ID.
|
||||
_LYRIC_OSD_OVERLAY_ID = 4242
|
||||
# Optional overrides set by the playlist controller (.pipe/.mpv) so the lyric
|
||||
# helper can resolve notes even when the local file path cannot be mapped back
|
||||
# to a store via the store DB (common for Folder stores).
|
||||
_ITEM_STORE_PROP = "user-data/medeia-item-store"
|
||||
_ITEM_HASH_PROP = "user-data/medeia-item-hash"
|
||||
|
||||
# Note: We previously used `osd-overlay`, but some mpv builds return
|
||||
# error='invalid parameter' for that command. We now use `show-text`, which is
|
||||
# widely supported across mpv versions.
|
||||
|
||||
_OSD_STYLE_SAVED: Optional[Dict[str, Any]] = None
|
||||
_OSD_STYLE_APPLIED: bool = False
|
||||
|
||||
|
||||
def _single_instance_lock_path(ipc_path: str) -> Path:
|
||||
@@ -70,7 +79,7 @@ def _single_instance_lock_path(ipc_path: str) -> Path:
|
||||
def _acquire_single_instance_lock(ipc_path: str) -> bool:
|
||||
"""Ensure only one MPV.lyric process runs per IPC server.
|
||||
|
||||
This prevents duplicate overlays (e.g. one old show-text overlay + one new osd-overlay).
|
||||
This prevents duplicate overlays (e.g. multiple lyric helpers racing to update OSD).
|
||||
"""
|
||||
global _SINGLE_INSTANCE_LOCK_FH
|
||||
|
||||
@@ -123,39 +132,29 @@ def _ass_escape(text: str) -> str:
|
||||
return t
|
||||
|
||||
|
||||
def _format_lyric_as_subtitle(text: str) -> str:
|
||||
# Bottom-center like a subtitle (ASS alignment 2).
|
||||
# NOTE: show-text escapes ASS by default; we use osd-overlay so this is honored.
|
||||
return "{\\an2}" + _ass_escape(text)
|
||||
def _osd_set_text(client: MPVIPCClient, text: str, *, duration_ms: int = 1000) -> Optional[dict]:
|
||||
# Signature: show-text <string> [<duration-ms>] [<level>]
|
||||
# Duration 0 clears immediately; we generally set it to cover until next update.
|
||||
try:
|
||||
d = int(duration_ms)
|
||||
except Exception:
|
||||
d = 1000
|
||||
if d < 0:
|
||||
d = 0
|
||||
return client.send_command({
|
||||
"command": [
|
||||
"show-text",
|
||||
str(text or ""),
|
||||
d,
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def _osd_overlay_set_ass(client: MPVIPCClient, ass_text: str) -> Optional[dict]:
|
||||
# Use osd-overlay with ass-events so ASS override tags (e.g. {\an2}) are applied.
|
||||
# Keep z low so UI scripts (like uosc) can draw above it if they use higher z.
|
||||
return client.send_command(
|
||||
{
|
||||
"command": {
|
||||
"name": "osd-overlay",
|
||||
"id": _LYRIC_OSD_OVERLAY_ID,
|
||||
"format": "ass-events",
|
||||
"data": ass_text,
|
||||
"res_y": 720,
|
||||
"z": -50,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _osd_overlay_clear(client: MPVIPCClient) -> None:
|
||||
client.send_command(
|
||||
{
|
||||
"command": {
|
||||
"name": "osd-overlay",
|
||||
"id": _LYRIC_OSD_OVERLAY_ID,
|
||||
"format": "none"
|
||||
}
|
||||
}
|
||||
)
|
||||
def _osd_clear(client: MPVIPCClient) -> None:
|
||||
try:
|
||||
_osd_set_text(client, "", duration_ms=0)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
@@ -191,6 +190,104 @@ def _ipc_get_property(
|
||||
return default
|
||||
|
||||
|
||||
def _ipc_set_property(client: MPVIPCClient, name: str, value: Any) -> bool:
|
||||
resp = client.send_command({
|
||||
"command": ["set_property",
|
||||
name,
|
||||
value]
|
||||
})
|
||||
return bool(resp and resp.get("error") == "success")
|
||||
|
||||
|
||||
def _osd_capture_style(client: MPVIPCClient) -> Dict[str, Any]:
|
||||
keys = [
|
||||
"osd-align-x",
|
||||
"osd-align-y",
|
||||
"osd-font-size",
|
||||
"osd-margin-y",
|
||||
]
|
||||
out: Dict[str, Any] = {}
|
||||
for k in keys:
|
||||
try:
|
||||
out[k] = _ipc_get_property(client, k, None)
|
||||
except Exception:
|
||||
out[k] = None
|
||||
return out
|
||||
|
||||
|
||||
def _osd_apply_lyric_style(client: MPVIPCClient, *, config: Dict[str, Any]) -> None:
|
||||
"""Apply bottom-center + larger font for lyric show-text messages.
|
||||
|
||||
This modifies mpv's global OSD settings, so we save and restore them.
|
||||
"""
|
||||
global _OSD_STYLE_SAVED, _OSD_STYLE_APPLIED
|
||||
|
||||
if not _OSD_STYLE_APPLIED:
|
||||
if _OSD_STYLE_SAVED is None:
|
||||
_OSD_STYLE_SAVED = _osd_capture_style(client)
|
||||
|
||||
try:
|
||||
_ipc_set_property(client, "osd-align-x", "center")
|
||||
_ipc_set_property(client, "osd-align-y", "bottom")
|
||||
|
||||
scale = config.get("lyric_osd_font_scale", 1.15)
|
||||
try:
|
||||
scale_f = float(scale)
|
||||
except Exception:
|
||||
scale_f = 1.15
|
||||
if scale_f < 1.0:
|
||||
scale_f = 1.0
|
||||
|
||||
old_size = None
|
||||
try:
|
||||
if _OSD_STYLE_SAVED is not None:
|
||||
old_size = _OSD_STYLE_SAVED.get("osd-font-size")
|
||||
except Exception:
|
||||
old_size = None
|
||||
if isinstance(old_size, (int, float)):
|
||||
new_size = int(max(10, round(float(old_size) * scale_f)))
|
||||
else:
|
||||
# mpv default is typically ~55; choose a conservative readable size.
|
||||
new_size = int(config.get("lyric_osd_font_size", 64))
|
||||
|
||||
_ipc_set_property(client, "osd-font-size", new_size)
|
||||
|
||||
min_margin_y = int(config.get("lyric_osd_min_margin_y", 60))
|
||||
old_margin_y = None
|
||||
try:
|
||||
if _OSD_STYLE_SAVED is not None:
|
||||
old_margin_y = _OSD_STYLE_SAVED.get("osd-margin-y")
|
||||
except Exception:
|
||||
old_margin_y = None
|
||||
if isinstance(old_margin_y, (int, float)):
|
||||
_ipc_set_property(client, "osd-margin-y", int(max(old_margin_y, min_margin_y)))
|
||||
else:
|
||||
_ipc_set_property(client, "osd-margin-y", min_margin_y)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
_OSD_STYLE_APPLIED = True
|
||||
|
||||
|
||||
def _osd_restore_style(client: MPVIPCClient) -> None:
|
||||
global _OSD_STYLE_SAVED, _OSD_STYLE_APPLIED
|
||||
|
||||
if not _OSD_STYLE_APPLIED:
|
||||
return
|
||||
|
||||
try:
|
||||
saved = _OSD_STYLE_SAVED or {}
|
||||
for k, v in saved.items():
|
||||
if v is None:
|
||||
continue
|
||||
try:
|
||||
_ipc_set_property(client, k, v)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_OSD_STYLE_APPLIED = False
|
||||
|
||||
|
||||
def _http_get_json(url: str, *, timeout_s: float = 10.0) -> Optional[dict]:
|
||||
try:
|
||||
req = Request(
|
||||
@@ -460,9 +557,26 @@ def _load_config_best_effort() -> dict:
|
||||
try:
|
||||
from SYS.config import load_config
|
||||
|
||||
cfg = load_config()
|
||||
return cfg if isinstance(cfg,
|
||||
dict) else {}
|
||||
# `SYS.config.load_config()` defaults to loading `config.conf` from the
|
||||
# SYS/ directory, but this repo keeps `config.conf` at the repo root.
|
||||
# MPV.lyric is often spawned from mpv (not the CLI), so we must locate
|
||||
# the repo root ourselves.
|
||||
try:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
except Exception:
|
||||
repo_root = None
|
||||
|
||||
cfg = None
|
||||
if repo_root is not None:
|
||||
try:
|
||||
cfg = load_config(config_dir=repo_root)
|
||||
except Exception:
|
||||
cfg = None
|
||||
|
||||
if cfg is None:
|
||||
cfg = load_config()
|
||||
|
||||
return cfg if isinstance(cfg, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@@ -745,6 +859,22 @@ def _resolve_store_backend_for_target(
|
||||
|
||||
return name, backend
|
||||
|
||||
# Fallback for Folder stores:
|
||||
# If the mpv target is inside a configured Folder store root and the filename
|
||||
# is hash-named, accept the inferred store even if the store DB doesn't map
|
||||
# hash->path (e.g. DB missing entry, external copy, etc.).
|
||||
try:
|
||||
inferred = _infer_store_for_target(target=target, config=config)
|
||||
if inferred and inferred in backend_names:
|
||||
backend = reg[inferred]
|
||||
if type(backend).__name__ == "Folder":
|
||||
p = Path(target)
|
||||
stem = str(p.stem or "").strip().lower()
|
||||
if stem and stem == str(file_hash or "").strip().lower():
|
||||
return inferred, backend
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -856,6 +986,8 @@ def run_auto_overlay(
|
||||
last_text: Optional[str] = None
|
||||
last_visible: Optional[bool] = None
|
||||
|
||||
global _OSD_STYLE_SAVED, _OSD_STYLE_APPLIED
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Toggle support (mpv Lua script sets this property; default to visible).
|
||||
@@ -868,13 +1000,20 @@ def run_auto_overlay(
|
||||
raw_path = _ipc_get_property(client, "path", None, raise_on_disconnect=True)
|
||||
except ConnectionError:
|
||||
try:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
# If mpv restarted, recapture baseline OSD settings on reconnect.
|
||||
_OSD_STYLE_SAVED = None
|
||||
_OSD_STYLE_APPLIED = False
|
||||
if not client.connect():
|
||||
_log("mpv IPC disconnected; exiting MPV.lyric")
|
||||
return 4
|
||||
@@ -888,7 +1027,11 @@ def run_auto_overlay(
|
||||
elif last_visible is True and visible is False:
|
||||
# Clear immediately when switching off.
|
||||
try:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
# Also remove any external subtitle that may be showing lyrics so
|
||||
@@ -923,7 +1066,11 @@ def run_auto_overlay(
|
||||
# Non-http streams (ytdl://, edl://, rtmp://, etc.) are never valid for lyrics.
|
||||
if last_loaded_key is not None:
|
||||
try:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
if last_loaded_sub_path is not None:
|
||||
@@ -941,6 +1088,28 @@ def run_auto_overlay(
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
# Optional override from the playlist controller: `.mpv` can publish the
|
||||
# intended store/hash in mpv user-data. We use this both on target change
|
||||
# and as a late-arriving fallback (the helper may start before `.mpv`
|
||||
# sets the properties).
|
||||
store_override = None
|
||||
hash_override = None
|
||||
try:
|
||||
store_override = _ipc_get_property(client, _ITEM_STORE_PROP, None)
|
||||
hash_override = _ipc_get_property(client, _ITEM_HASH_PROP, None)
|
||||
except Exception:
|
||||
store_override = None
|
||||
hash_override = None
|
||||
|
||||
try:
|
||||
store_override = str(store_override).strip() if store_override else None
|
||||
except Exception:
|
||||
store_override = None
|
||||
try:
|
||||
hash_override = str(hash_override).strip().lower() if hash_override else None
|
||||
except Exception:
|
||||
hash_override = None
|
||||
|
||||
if target != last_target:
|
||||
last_target = target
|
||||
last_idx = None
|
||||
@@ -953,7 +1122,11 @@ def run_auto_overlay(
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
@@ -962,6 +1135,22 @@ def run_auto_overlay(
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
if store_override and (not hash_override or hash_override == current_file_hash):
|
||||
try:
|
||||
from Store import Store as StoreRegistry
|
||||
|
||||
reg = StoreRegistry(cfg, suppress_debug=True)
|
||||
current_backend = reg[store_override]
|
||||
current_store_name = store_override
|
||||
current_key = f"{current_store_name}:{current_file_hash}"
|
||||
_log(
|
||||
f"Resolved via mpv override store={current_store_name!r} hash={current_file_hash!r} valid=True"
|
||||
)
|
||||
except Exception:
|
||||
current_backend = None
|
||||
current_store_name = None
|
||||
current_key = None
|
||||
|
||||
if is_http:
|
||||
# HTTP/HTTPS targets are only valid if they map to a store backend.
|
||||
store_from_url = _extract_store_from_url_target(target)
|
||||
@@ -977,7 +1166,11 @@ def run_auto_overlay(
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
@@ -1002,7 +1195,11 @@ def run_auto_overlay(
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
@@ -1026,7 +1223,11 @@ def run_auto_overlay(
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
@@ -1042,15 +1243,16 @@ def run_auto_overlay(
|
||||
|
||||
else:
|
||||
# Local files: resolve store item via store DB. If not resolvable, lyrics are disabled.
|
||||
current_store_name, current_backend = _resolve_store_backend_for_target(
|
||||
target=target,
|
||||
file_hash=current_file_hash,
|
||||
config=cfg,
|
||||
)
|
||||
current_key = (
|
||||
f"{current_store_name}:{current_file_hash}"
|
||||
if current_store_name and current_file_hash else None
|
||||
)
|
||||
if not current_key or not current_backend:
|
||||
current_store_name, current_backend = _resolve_store_backend_for_target(
|
||||
target=target,
|
||||
file_hash=current_file_hash,
|
||||
config=cfg,
|
||||
)
|
||||
current_key = (
|
||||
f"{current_store_name}:{current_file_hash}"
|
||||
if current_store_name and current_file_hash else None
|
||||
)
|
||||
|
||||
_log(
|
||||
f"Resolved store={current_store_name!r} hash={current_file_hash!r} valid={bool(current_key)}"
|
||||
@@ -1063,7 +1265,11 @@ def run_auto_overlay(
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
if last_loaded_sub_path is not None:
|
||||
@@ -1072,6 +1278,29 @@ def run_auto_overlay(
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
# Late-arriving context fallback: if we still don't have a store/backend for a
|
||||
# local file, but `.mpv` has since populated user-data overrides, apply them
|
||||
# without requiring a track change.
|
||||
if (not is_http) and target and (not current_key or not current_backend):
|
||||
try:
|
||||
current_file_hash = _infer_hash_for_target(target) or current_file_hash
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if (store_override and current_file_hash and (not hash_override or hash_override == current_file_hash)):
|
||||
try:
|
||||
from Store import Store as StoreRegistry
|
||||
|
||||
reg = StoreRegistry(cfg, suppress_debug=True)
|
||||
current_backend = reg[store_override]
|
||||
current_store_name = store_override
|
||||
current_key = f"{current_store_name}:{current_file_hash}"
|
||||
_log(
|
||||
f"Resolved via mpv override store={current_store_name!r} hash={current_file_hash!r} valid=True"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Load/reload lyrics when we have a resolvable key and it differs from what we loaded.
|
||||
# This is important for the autofetch path: the note can appear without the mpv target changing.
|
||||
if (current_key and current_key != last_loaded_key and current_store_name
|
||||
@@ -1097,7 +1326,11 @@ def run_auto_overlay(
|
||||
if sub_text:
|
||||
# Treat subtitles as an alternative to lyrics; do not show the lyric overlay.
|
||||
try:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1194,30 +1427,56 @@ def run_auto_overlay(
|
||||
entries = []
|
||||
times = []
|
||||
if last_loaded_key is not None:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_loaded_key = None
|
||||
last_loaded_mode = None
|
||||
else:
|
||||
_log(f"Loaded lyric note ({len(lrc_text)} chars)")
|
||||
if not lrc_text:
|
||||
# No lyric note, and we didn't run autofetch this tick.
|
||||
# Clear any previous overlay and avoid crashing on None.
|
||||
try:
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
entries = []
|
||||
times = []
|
||||
last_loaded_key = current_key
|
||||
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
|
||||
last_loaded_mode = "lyric"
|
||||
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.
|
||||
t = _ipc_get_property(client, "time-pos", None, raise_on_disconnect=True)
|
||||
except ConnectionError:
|
||||
try:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
_OSD_STYLE_SAVED = None
|
||||
_OSD_STYLE_APPLIED = False
|
||||
if not client.connect():
|
||||
_log("mpv IPC disconnected; exiting MPV.lyric")
|
||||
return 4
|
||||
@@ -1229,10 +1488,34 @@ def run_auto_overlay(
|
||||
continue
|
||||
|
||||
if not entries:
|
||||
# Nothing to show; ensure any previous text is cleared.
|
||||
if last_text is not None:
|
||||
try:
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_text = None
|
||||
last_idx = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
if not visible:
|
||||
# User toggled lyrics off.
|
||||
if last_text is not None:
|
||||
try:
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_osd_restore_style(client)
|
||||
except Exception:
|
||||
pass
|
||||
last_text = None
|
||||
last_idx = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -1244,8 +1527,23 @@ def run_auto_overlay(
|
||||
|
||||
line = entries[idx]
|
||||
if idx != last_idx or line.text != last_text:
|
||||
# osd-overlay has no duration; refresh periodically.
|
||||
resp = _osd_overlay_set_ass(client, _format_lyric_as_subtitle(line.text))
|
||||
try:
|
||||
if last_loaded_mode == "lyric":
|
||||
_osd_apply_lyric_style(client, config=cfg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Show until the next lyric timestamp (or a sane max) to avoid flicker.
|
||||
dur_ms = 1200
|
||||
try:
|
||||
if idx + 1 < len(times):
|
||||
nxt = float(times[idx + 1])
|
||||
cur = float(t)
|
||||
dur_ms = int(max(250, min(8000, (nxt - cur) * 1000)))
|
||||
except Exception:
|
||||
dur_ms = 1200
|
||||
|
||||
resp = _osd_set_text(client, line.text, duration_ms=dur_ms)
|
||||
if resp is None:
|
||||
client.disconnect()
|
||||
if not client.connect():
|
||||
@@ -1253,7 +1551,7 @@ def run_auto_overlay(
|
||||
return 4
|
||||
elif isinstance(resp, dict) and resp.get("error") not in (None, "success"):
|
||||
try:
|
||||
_log(f"mpv osd-overlay returned error={resp.get('error')!r}")
|
||||
_log(f"mpv show-text returned error={resp.get('error')!r}")
|
||||
except Exception:
|
||||
pass
|
||||
last_idx = idx
|
||||
@@ -1285,7 +1583,7 @@ def run_overlay(*, mpv: MPV, entries: List[LrcLine], poll_s: float = 0.15) -> in
|
||||
t = _ipc_get_property(client, "time-pos", None, raise_on_disconnect=True)
|
||||
except ConnectionError:
|
||||
try:
|
||||
_osd_overlay_clear(client)
|
||||
_osd_clear(client)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
@@ -1311,8 +1609,17 @@ def run_overlay(*, mpv: MPV, entries: List[LrcLine], poll_s: float = 0.15) -> in
|
||||
|
||||
line = entries[idx]
|
||||
if idx != last_idx or line.text != last_text:
|
||||
# osd-overlay has no duration; refresh periodically.
|
||||
resp = _osd_overlay_set_ass(client, _format_lyric_as_subtitle(line.text))
|
||||
# Show until the next lyric timestamp (or a sane max) to avoid flicker.
|
||||
dur_ms = 1200
|
||||
try:
|
||||
if idx + 1 < len(times):
|
||||
nxt = float(times[idx + 1])
|
||||
cur = float(t)
|
||||
dur_ms = int(max(250, min(8000, (nxt - cur) * 1000)))
|
||||
except Exception:
|
||||
dur_ms = 1200
|
||||
|
||||
resp = _osd_set_text(client, line.text, duration_ms=dur_ms)
|
||||
if resp is None:
|
||||
client.disconnect()
|
||||
if not client.connect():
|
||||
@@ -1320,7 +1627,7 @@ def run_overlay(*, mpv: MPV, entries: List[LrcLine], poll_s: float = 0.15) -> in
|
||||
return 4
|
||||
elif isinstance(resp, dict) and resp.get("error") not in (None, "success"):
|
||||
try:
|
||||
_log(f"mpv osd-overlay returned error={resp.get('error')!r}")
|
||||
_log(f"mpv show-text returned error={resp.get('error')!r}")
|
||||
except Exception:
|
||||
pass
|
||||
last_idx = idx
|
||||
|
||||
Reference in New Issue
Block a user