dfdfdf
This commit is contained in:
@@ -4,6 +4,31 @@ local msg = require 'mp.msg'
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Lyrics overlay toggle
|
||||
-- The Python helper (python -m MPV.lyric) will read this property via IPC.
|
||||
local LYRIC_VISIBLE_PROP = "user-data/medeia-lyric-visible"
|
||||
|
||||
local function lyric_get_visible()
|
||||
local ok, v = pcall(mp.get_property_native, LYRIC_VISIBLE_PROP)
|
||||
if not ok or v == nil then
|
||||
return true
|
||||
end
|
||||
return v and true or false
|
||||
end
|
||||
|
||||
local function lyric_set_visible(v)
|
||||
pcall(mp.set_property_native, LYRIC_VISIBLE_PROP, v and true or false)
|
||||
end
|
||||
|
||||
local function lyric_toggle()
|
||||
local now = not lyric_get_visible()
|
||||
lyric_set_visible(now)
|
||||
mp.osd_message("Lyrics: " .. (now and "on" or "off"), 1)
|
||||
end
|
||||
|
||||
-- Default to visible unless user overrides.
|
||||
lyric_set_visible(true)
|
||||
|
||||
-- Configuration
|
||||
local opts = {
|
||||
python_path = "python",
|
||||
@@ -138,4 +163,8 @@ mp.add_key_binding("mbtn_right", "medios-menu-right-click", M.show_menu)
|
||||
mp.add_key_binding("ctrl+i", "medios-info", M.get_file_info)
|
||||
mp.add_key_binding("ctrl+del", "medios-delete", M.delete_current_file)
|
||||
|
||||
-- Lyrics toggle (requested: 'L')
|
||||
mp.add_key_binding("l", "medeia-lyric-toggle", lyric_toggle)
|
||||
mp.add_key_binding("L", "medeia-lyric-toggle-shift", lyric_toggle)
|
||||
|
||||
return M
|
||||
|
||||
1195
MPV/lyric.py
Normal file
1195
MPV/lyric.py
Normal file
File diff suppressed because it is too large
Load Diff
210
MPV/mpv_ipc.py
210
MPV/mpv_ipc.py
@@ -12,6 +12,7 @@ import os
|
||||
import platform
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time as _time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, List, BinaryIO, cast
|
||||
@@ -24,6 +25,88 @@ FIXED_IPC_PIPE_NAME = "mpv-medeia-macina"
|
||||
MPV_LUA_SCRIPT_PATH = str(Path(__file__).resolve().parent / "LUA" / "main.lua")
|
||||
|
||||
|
||||
_LYRIC_PROCESS: Optional[subprocess.Popen] = None
|
||||
_LYRIC_LOG_FH: Optional[Any] = None
|
||||
|
||||
|
||||
def _windows_list_lyric_helper_pids(ipc_path: str) -> List[int]:
|
||||
"""Return PIDs of `python -m MPV.lyric --ipc <ipc_path>` helpers (Windows only)."""
|
||||
if platform.system() != "Windows":
|
||||
return []
|
||||
try:
|
||||
ipc_path = str(ipc_path or "")
|
||||
except Exception:
|
||||
ipc_path = ""
|
||||
if not ipc_path:
|
||||
return []
|
||||
|
||||
# Use CIM to query command lines; output as JSON for robust parsing.
|
||||
# Note: `ConvertTo-Json` returns a number for single item, array for many, or null.
|
||||
ps_script = (
|
||||
"$ipc = "
|
||||
+ json.dumps(ipc_path)
|
||||
+ "; "
|
||||
"Get-CimInstance Win32_Process | "
|
||||
"Where-Object { $_.CommandLine -and $_.CommandLine -match ' -m\\s+MPV\\.lyric(\\s|$)' -and $_.CommandLine -match ('--ipc\\s+' + [regex]::Escape($ipc)) } | "
|
||||
"Select-Object -ExpandProperty ProcessId | ConvertTo-Json -Compress"
|
||||
)
|
||||
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["powershell", "-NoProfile", "-Command", ps_script],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=2,
|
||||
text=True,
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
txt = (out or "").strip()
|
||||
if not txt or txt == "null":
|
||||
return []
|
||||
try:
|
||||
obj = json.loads(txt)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
pids: List[int] = []
|
||||
if isinstance(obj, list):
|
||||
for v in obj:
|
||||
try:
|
||||
pids.append(int(v))
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
pids.append(int(obj))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# De-dupe and filter obvious junk.
|
||||
uniq: List[int] = []
|
||||
for pid in pids:
|
||||
if pid and pid > 0 and pid not in uniq:
|
||||
uniq.append(pid)
|
||||
return uniq
|
||||
|
||||
|
||||
def _windows_kill_pids(pids: List[int]) -> None:
|
||||
if platform.system() != "Windows":
|
||||
return
|
||||
for pid in pids or []:
|
||||
try:
|
||||
subprocess.run(
|
||||
["taskkill", "/PID", str(int(pid)), "/F"],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=2,
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
class MPVIPCError(Exception):
|
||||
"""Raised when MPV IPC communication fails."""
|
||||
pass
|
||||
@@ -38,7 +121,7 @@ class MPV:
|
||||
- Query playlist and currently playing item via IPC
|
||||
|
||||
This class intentionally stays "dumb": it does not implement app logic.
|
||||
App behavior is driven by cmdlets (e.g. `.pipe`) and the bundled Lua script.
|
||||
App behavior is driven by cmdlet (e.g. `.pipe`) and the bundled Lua script.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -55,11 +138,11 @@ class MPV:
|
||||
lua_path = Path(str(lua_script_path)).resolve()
|
||||
self.lua_script_path = str(lua_path)
|
||||
|
||||
def client(self) -> "MPVIPCClient":
|
||||
return MPVIPCClient(socket_path=self.ipc_path, timeout=self.timeout)
|
||||
def client(self, silent: bool = False) -> "MPVIPCClient":
|
||||
return MPVIPCClient(socket_path=self.ipc_path, timeout=self.timeout, silent=bool(silent))
|
||||
|
||||
def is_running(self) -> bool:
|
||||
client = self.client()
|
||||
client = self.client(silent=True)
|
||||
try:
|
||||
ok = client.connect()
|
||||
return bool(ok)
|
||||
@@ -67,7 +150,7 @@ class MPV:
|
||||
client.disconnect()
|
||||
|
||||
def send(self, command: Dict[str, Any] | List[Any], silent: bool = False) -> Optional[Dict[str, Any]]:
|
||||
client = self.client()
|
||||
client = self.client(silent=bool(silent))
|
||||
try:
|
||||
if not client.connect():
|
||||
return None
|
||||
@@ -136,9 +219,109 @@ class MPV:
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def ensure_lyric_loader_running(self) -> None:
|
||||
"""Start (or keep) the Python lyric overlay helper.
|
||||
|
||||
Uses the fixed IPC pipe name so it can follow playback.
|
||||
"""
|
||||
global _LYRIC_PROCESS, _LYRIC_LOG_FH
|
||||
|
||||
# Cross-session guard (Windows): avoid spawning multiple helpers across separate CLI runs.
|
||||
# Also clean up stale helpers when mpv isn't running anymore.
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
existing = _windows_list_lyric_helper_pids(str(self.ipc_path))
|
||||
if existing:
|
||||
if not self.is_running():
|
||||
_windows_kill_pids(existing)
|
||||
return
|
||||
# If multiple exist, kill them and start fresh (prevents double overlays).
|
||||
if len(existing) == 1:
|
||||
return
|
||||
_windows_kill_pids(existing)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if _LYRIC_PROCESS is not None and _LYRIC_PROCESS.poll() is None:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if _LYRIC_PROCESS is not None:
|
||||
try:
|
||||
_LYRIC_PROCESS.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_LYRIC_PROCESS = None
|
||||
try:
|
||||
if _LYRIC_LOG_FH is not None:
|
||||
_LYRIC_LOG_FH.close()
|
||||
except Exception:
|
||||
pass
|
||||
_LYRIC_LOG_FH = None
|
||||
|
||||
try:
|
||||
try:
|
||||
tmp_dir = Path(os.environ.get("TEMP") or os.environ.get("TMP") or ".")
|
||||
except Exception:
|
||||
tmp_dir = Path(".")
|
||||
log_path = str((tmp_dir / "medeia-mpv-lyric.log").resolve())
|
||||
|
||||
# Ensure the module can be imported even when the app is launched from a different cwd.
|
||||
# Repo root = parent of the MPV package directory.
|
||||
try:
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
except Exception:
|
||||
repo_root = Path.cwd()
|
||||
|
||||
cmd: List[str] = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"MPV.lyric",
|
||||
"--ipc",
|
||||
str(self.ipc_path),
|
||||
"--log",
|
||||
log_path,
|
||||
]
|
||||
|
||||
# Redirect helper stdout/stderr to the log file so we can see crashes/import errors.
|
||||
try:
|
||||
_LYRIC_LOG_FH = open(log_path, "a", encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
_LYRIC_LOG_FH = None
|
||||
|
||||
kwargs: Dict[str, Any] = {
|
||||
"stdin": subprocess.DEVNULL,
|
||||
"stdout": _LYRIC_LOG_FH or subprocess.DEVNULL,
|
||||
"stderr": _LYRIC_LOG_FH or subprocess.DEVNULL,
|
||||
}
|
||||
|
||||
# Ensure immediate flushing to the log file.
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
try:
|
||||
existing_pp = env.get("PYTHONPATH")
|
||||
env["PYTHONPATH"] = str(repo_root) if not existing_pp else (str(repo_root) + os.pathsep + str(existing_pp))
|
||||
except Exception:
|
||||
pass
|
||||
kwargs["env"] = env
|
||||
|
||||
# Make the current directory the repo root so `-m MPV.lyric` resolves reliably.
|
||||
kwargs["cwd"] = str(repo_root)
|
||||
if platform.system() == "Windows":
|
||||
kwargs["creationflags"] = 0x00000008 # DETACHED_PROCESS
|
||||
|
||||
_LYRIC_PROCESS = subprocess.Popen(cmd, **kwargs)
|
||||
debug(f"Lyric loader started (log={log_path})")
|
||||
except Exception as exc:
|
||||
debug(f"Lyric loader failed to start: {exc}")
|
||||
|
||||
def wait_for_ipc(self, retries: int = 20, delay_seconds: float = 0.2) -> bool:
|
||||
for _ in range(max(1, retries)):
|
||||
client = self.client()
|
||||
client = self.client(silent=True)
|
||||
try:
|
||||
if client.connect():
|
||||
return True
|
||||
@@ -233,7 +416,7 @@ class MPVIPCClient:
|
||||
It handles platform-specific differences (Windows named pipes vs Unix sockets).
|
||||
"""
|
||||
|
||||
def __init__(self, socket_path: Optional[str] = None, timeout: float = 5.0):
|
||||
def __init__(self, socket_path: Optional[str] = None, timeout: float = 5.0, silent: bool = False):
|
||||
"""Initialize MPV IPC client.
|
||||
|
||||
Args:
|
||||
@@ -244,6 +427,7 @@ class MPVIPCClient:
|
||||
self.socket_path = socket_path or get_ipc_pipe_path()
|
||||
self.sock: socket.socket | BinaryIO | None = None
|
||||
self.is_windows = platform.system() == "Windows"
|
||||
self.silent = bool(silent)
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Connect to mpv IPC socket.
|
||||
@@ -259,17 +443,20 @@ class MPVIPCClient:
|
||||
self.sock = open(self.socket_path, 'r+b', buffering=0)
|
||||
return True
|
||||
except (OSError, IOError) as exc:
|
||||
debug(f"Failed to connect to MPV named pipe: {exc}")
|
||||
if not self.silent:
|
||||
debug(f"Failed to connect to MPV named pipe: {exc}")
|
||||
return False
|
||||
else:
|
||||
# Unix domain socket (Linux, macOS)
|
||||
if not os.path.exists(self.socket_path):
|
||||
debug(f"IPC socket not found: {self.socket_path}")
|
||||
if not self.silent:
|
||||
debug(f"IPC socket not found: {self.socket_path}")
|
||||
return False
|
||||
|
||||
af_unix = getattr(socket, "AF_UNIX", None)
|
||||
if af_unix is None:
|
||||
debug("IPC AF_UNIX is not available on this platform")
|
||||
if not self.silent:
|
||||
debug("IPC AF_UNIX is not available on this platform")
|
||||
return False
|
||||
|
||||
self.sock = socket.socket(af_unix, socket.SOCK_STREAM)
|
||||
@@ -277,7 +464,8 @@ class MPVIPCClient:
|
||||
self.sock.connect(self.socket_path)
|
||||
return True
|
||||
except Exception as exc:
|
||||
debug(f"Failed to connect to MPV IPC: {exc}")
|
||||
if not self.silent:
|
||||
debug(f"Failed to connect to MPV IPC: {exc}")
|
||||
self.sock = None
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user