Add YAPF style + ignore, and format tracked Python files
This commit is contained in:
139
MPV/lyric.py
139
MPV/lyric.py
@@ -41,7 +41,6 @@ from urllib.parse import urlparse
|
||||
|
||||
from MPV.mpv_ipc import MPV, MPVIPCClient
|
||||
|
||||
|
||||
_TIMESTAMP_RE = re.compile(r"\[(?P<m>\d+):(?P<s>\d{2})(?:\.(?P<frac>\d{1,3}))?\]")
|
||||
_OFFSET_RE = re.compile(r"^\[offset:(?P<ms>[+-]?\d+)\]$", re.IGNORECASE)
|
||||
_HASH_RE = re.compile(r"[0-9a-f]{64}", re.IGNORECASE)
|
||||
@@ -50,11 +49,9 @@ _HYDRUS_HASH_QS_RE = re.compile(r"hash=([0-9a-f]{64})", re.IGNORECASE)
|
||||
_WIN_DRIVE_RE = re.compile(r"^[a-zA-Z]:[\\/]")
|
||||
_WIN_UNC_RE = re.compile(r"^\\\\")
|
||||
|
||||
|
||||
_LOG_FH: Optional[TextIO] = None
|
||||
_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.
|
||||
@@ -151,7 +148,13 @@ def _osd_overlay_set_ass(client: MPVIPCClient, ass_text: str) -> Optional[dict]:
|
||||
|
||||
def _osd_overlay_clear(client: MPVIPCClient) -> None:
|
||||
client.send_command(
|
||||
{"command": {"name": "osd-overlay", "id": _LYRIC_OSD_OVERLAY_ID, "format": "none"}}
|
||||
{
|
||||
"command": {
|
||||
"name": "osd-overlay",
|
||||
"id": _LYRIC_OSD_OVERLAY_ID,
|
||||
"format": "none"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -175,7 +178,10 @@ def _ipc_get_property(
|
||||
*,
|
||||
raise_on_disconnect: bool = False,
|
||||
) -> object:
|
||||
resp = client.send_command({"command": ["get_property", name]})
|
||||
resp = client.send_command({
|
||||
"command": ["get_property",
|
||||
name]
|
||||
})
|
||||
if resp is None:
|
||||
if raise_on_disconnect:
|
||||
raise ConnectionError("Lost mpv IPC connection")
|
||||
@@ -234,7 +240,10 @@ def _sanitize_query(s: Optional[str]) -> Optional[str]:
|
||||
return t if t else None
|
||||
|
||||
|
||||
def _infer_artist_title_from_tags(tags: List[str]) -> tuple[Optional[str], Optional[str]]:
|
||||
def _infer_artist_title_from_tags(
|
||||
tags: List[str]
|
||||
) -> tuple[Optional[str],
|
||||
Optional[str]]:
|
||||
artist = None
|
||||
title = None
|
||||
for t in tags or []:
|
||||
@@ -267,7 +276,10 @@ def _wrap_plain_lyrics_as_lrc(text: str) -> str:
|
||||
|
||||
|
||||
def _fetch_lrclib(
|
||||
*, artist: Optional[str], title: Optional[str], duration_s: Optional[float] = None
|
||||
*,
|
||||
artist: Optional[str],
|
||||
title: Optional[str],
|
||||
duration_s: Optional[float] = None
|
||||
) -> Optional[str]:
|
||||
base = "https://lrclib.net/api"
|
||||
|
||||
@@ -276,10 +288,11 @@ def _fetch_lrclib(
|
||||
return None
|
||||
|
||||
# Try direct get.
|
||||
q: Dict[str, str] = {
|
||||
"artist_name": artist,
|
||||
"track_name": title,
|
||||
}
|
||||
q: Dict[str,
|
||||
str] = {
|
||||
"artist_name": artist,
|
||||
"track_name": title,
|
||||
}
|
||||
if isinstance(duration_s, (int, float)) and duration_s and duration_s > 0:
|
||||
q["duration"] = str(int(duration_s))
|
||||
url = f"{base}/get?{urlencode(q)}"
|
||||
@@ -386,7 +399,7 @@ def parse_lrc(text: str) -> List[LrcLine]:
|
||||
# Ignore non-timestamp metadata lines like [ar:], [ti:], etc.
|
||||
continue
|
||||
|
||||
lyric_text = line[matches[-1].end() :].strip()
|
||||
lyric_text = line[matches[-1].end():].strip()
|
||||
for m in matches:
|
||||
mm = int(m.group("m"))
|
||||
ss = int(m.group("s"))
|
||||
@@ -445,10 +458,11 @@ def _extract_hash_from_target(target: str) -> Optional[str]:
|
||||
|
||||
def _load_config_best_effort() -> dict:
|
||||
try:
|
||||
from config import load_config
|
||||
from SYS.config import load_config
|
||||
|
||||
cfg = load_config()
|
||||
return cfg if isinstance(cfg, dict) else {}
|
||||
return cfg if isinstance(cfg,
|
||||
dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
@@ -512,10 +526,11 @@ def _write_temp_sub_file(*, key: str, text: str) -> Path:
|
||||
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]
|
||||
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
|
||||
@@ -523,14 +538,23 @@ def _write_temp_sub_file(*, key: str, text: str) -> Path:
|
||||
|
||||
def _try_remove_selected_external_sub(client: MPVIPCClient) -> None:
|
||||
try:
|
||||
client.send_command({"command": ["sub-remove"]})
|
||||
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"]})
|
||||
client.send_command(
|
||||
{
|
||||
"command": ["sub-add",
|
||||
str(path),
|
||||
"select",
|
||||
"medeia-sub"]
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
@@ -658,7 +682,8 @@ def _resolve_store_backend_for_target(
|
||||
target: str,
|
||||
file_hash: str,
|
||||
config: dict,
|
||||
) -> tuple[Optional[str], Any]:
|
||||
) -> tuple[Optional[str],
|
||||
Any]:
|
||||
"""Resolve a store backend for a local mpv target using the store DB.
|
||||
|
||||
A target is considered valid only when:
|
||||
@@ -756,7 +781,10 @@ def _infer_store_for_target(*, target: str, config: dict) -> Optional[str]:
|
||||
root = None
|
||||
try:
|
||||
root = (
|
||||
getattr(backend, "_location", None) or getattr(backend, "location", lambda: None)()
|
||||
getattr(backend,
|
||||
"_location",
|
||||
None) or getattr(backend,
|
||||
"location", lambda: None)()
|
||||
)
|
||||
except Exception:
|
||||
root = None
|
||||
@@ -795,7 +823,12 @@ def _infer_hash_for_target(target: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] = None) -> int:
|
||||
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 (note: 'lyric') or load subtitles (note: 'sub')."""
|
||||
cfg = config or {}
|
||||
|
||||
@@ -827,7 +860,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
try:
|
||||
# Toggle support (mpv Lua script sets this property; default to visible).
|
||||
visible_raw = _ipc_get_property(
|
||||
client, _LYRIC_VISIBLE_PROP, True, raise_on_disconnect=True
|
||||
client,
|
||||
_LYRIC_VISIBLE_PROP,
|
||||
True,
|
||||
raise_on_disconnect=True
|
||||
)
|
||||
raw_path = _ipc_get_property(client, "path", None, raise_on_disconnect=True)
|
||||
except ConnectionError:
|
||||
@@ -872,7 +908,9 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
else:
|
||||
last_visible = visible
|
||||
|
||||
target = _unwrap_memory_m3u(str(raw_path)) if isinstance(raw_path, str) else None
|
||||
target = _unwrap_memory_m3u(str(raw_path)
|
||||
) if isinstance(raw_path,
|
||||
str) else None
|
||||
if isinstance(target, str):
|
||||
target = _normalize_file_uri_target(target)
|
||||
|
||||
@@ -928,7 +966,8 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
# HTTP/HTTPS targets are only valid if they map to a store backend.
|
||||
store_from_url = _extract_store_from_url_target(target)
|
||||
store_name = store_from_url or _infer_hydrus_store_from_url_target(
|
||||
target=target, config=cfg
|
||||
target=target,
|
||||
config=cfg
|
||||
)
|
||||
if not store_name:
|
||||
_log("HTTP target has no store mapping; lyrics disabled")
|
||||
@@ -954,7 +993,9 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
current_backend = reg[store_name]
|
||||
current_store_name = store_name
|
||||
except Exception:
|
||||
_log(f"HTTP target store {store_name!r} not available; lyrics disabled")
|
||||
_log(
|
||||
f"HTTP target store {store_name!r} not available; lyrics disabled"
|
||||
)
|
||||
current_store_name = None
|
||||
current_backend = None
|
||||
current_key = None
|
||||
@@ -995,7 +1036,9 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
continue
|
||||
|
||||
current_key = f"{current_store_name}:{current_file_hash}"
|
||||
_log(f"Resolved store={current_store_name!r} hash={current_file_hash!r} valid=True")
|
||||
_log(
|
||||
f"Resolved store={current_store_name!r} hash={current_file_hash!r} valid=True"
|
||||
)
|
||||
|
||||
else:
|
||||
# Local files: resolve store item via store DB. If not resolvable, lyrics are disabled.
|
||||
@@ -1006,8 +1049,7 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
)
|
||||
current_key = (
|
||||
f"{current_store_name}:{current_file_hash}"
|
||||
if current_store_name and current_file_hash
|
||||
else None
|
||||
if current_store_name and current_file_hash else None
|
||||
)
|
||||
|
||||
_log(
|
||||
@@ -1032,16 +1074,15 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
|
||||
# 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
|
||||
and current_file_hash
|
||||
and current_backend
|
||||
):
|
||||
notes: Dict[str, str] = {}
|
||||
if (current_key and current_key != last_loaded_key and current_store_name
|
||||
and current_file_hash and current_backend):
|
||||
notes: Dict[str,
|
||||
str] = {}
|
||||
try:
|
||||
notes = current_backend.get_note(current_file_hash, config=cfg) or {}
|
||||
notes = current_backend.get_note(
|
||||
current_file_hash,
|
||||
config=cfg
|
||||
) or {}
|
||||
except Exception:
|
||||
notes = {}
|
||||
|
||||
@@ -1092,11 +1133,8 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
# Throttle attempts per key to avoid hammering APIs.
|
||||
autofetch_enabled = bool(cfg.get("lyric_autofetch", True))
|
||||
now = time.time()
|
||||
if (
|
||||
autofetch_enabled
|
||||
and current_key != last_fetch_attempt_key
|
||||
and (now - last_fetch_attempt_at) > 2.0
|
||||
):
|
||||
if (autofetch_enabled and current_key != last_fetch_attempt_key
|
||||
and (now - last_fetch_attempt_at) > 2.0):
|
||||
last_fetch_attempt_key = current_key
|
||||
last_fetch_attempt_at = now
|
||||
|
||||
@@ -1128,7 +1166,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
artist=artist,
|
||||
title=title,
|
||||
duration_s=(
|
||||
float(duration_s) if isinstance(duration_s, (int, float)) else None
|
||||
float(duration_s)
|
||||
if isinstance(duration_s,
|
||||
(int,
|
||||
float)) else None
|
||||
),
|
||||
)
|
||||
if not fetched or not fetched.strip():
|
||||
@@ -1137,7 +1178,10 @@ def run_auto_overlay(*, mpv: MPV, poll_s: float = 0.15, config: Optional[dict] =
|
||||
try:
|
||||
ok = bool(
|
||||
current_backend.set_note(
|
||||
current_file_hash, "lyric", fetched, config=cfg
|
||||
current_file_hash,
|
||||
"lyric",
|
||||
fetched,
|
||||
config=cfg
|
||||
)
|
||||
)
|
||||
_log(f"Autofetch stored lyric note ok={ok}")
|
||||
@@ -1230,7 +1274,8 @@ def run_overlay(*, mpv: MPV, entries: List[LrcLine], poll_s: float = 0.15) -> in
|
||||
client = mpv.client()
|
||||
if not client.connect():
|
||||
print(
|
||||
"mpv IPC is not reachable (is mpv running with --input-ipc-server?).", file=sys.stderr
|
||||
"mpv IPC is not reachable (is mpv running with --input-ipc-server?).",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user