This commit is contained in:
2026-02-04 20:51:54 -08:00
parent b714d477a6
commit d806ebad85
9 changed files with 257 additions and 63 deletions

View File

@@ -132,6 +132,40 @@ def _lua_log_file() -> Path:
return _repo_log_dir() / "medeia-mpv-lua.log"
def _extract_log_filter(args: Sequence[str]) -> tuple[List[str], Optional[str]]:
normalized: List[str] = []
log_filter: Optional[str] = None
i = 0
while i < len(args):
token = str(args[i])
token_lower = token.lower()
if token_lower in {"-log", "--log"}:
normalized.append(token)
if log_filter is None and i + 1 < len(args):
candidate = str(args[i + 1])
if candidate and not candidate.startswith("-"):
log_filter = candidate
i += 2
continue
i += 1
continue
normalized.append(token)
i += 1
return normalized, log_filter
def _apply_log_filter(lines: Sequence[str], filter_text: Optional[str]) -> List[str]:
if not filter_text:
return list(lines)
needle = filter_text.lower()
filtered: List[str] = []
for line in lines:
text = str(line)
if needle in text.lower():
filtered.append(text)
return filtered
def _try_enable_mpv_file_logging(mpv_log_path: str, *, attempts: int = 3) -> bool:
"""Best-effort enable mpv log-file + verbose level on a running instance.
@@ -646,8 +680,8 @@ def _build_ytdl_options(config: Optional[Dict[str,
if cookies_path:
opts.append(f"cookies={cookies_path.replace('\\', '/')}")
else:
opts.append("cookies-from-browser=chrome")
# Do not force chrome cookies if none are found; let yt-dlp use its defaults or fail gracefully.
if hydrus_header:
opts.append(f"add-header={hydrus_header}")
return ",".join(opts) if opts else None
@@ -676,6 +710,35 @@ def _is_hydrus_path(path: str, hydrus_url: Optional[str]) -> bool:
return False
def _is_probable_ytdl_url(url: str) -> bool:
"""Check if the URL is likely meant to be handled by MPV's ytdl-hook.
We use this to avoid wrapping these URLs in memory:// M3U payloads,
since the wrapper can sometimes prevent the ytdl-hook from triggering.
"""
if not isinstance(url, str):
return False
lower = url.lower().strip()
if not lower.startswith(("http://", "https://")):
return False
# Exclude Hydrus API file links (we handle headers for these separately)
if "/get_files/file" in lower:
return False
# Exclude Tidal manifest redirects if they've been resolved already
if "tidal.com" in lower and "/manifest" in lower:
return False
# Exclude AllDebrid protected links
if "alldebrid.com/f/" in lower:
return False
# Most other HTTP links (YouTube, Bandcamp, etc) are candidates for yt-dlp resolution in MPV
return True
def _ensure_ytdl_cookies(config: Optional[Dict[str, Any]] = None) -> None:
"""Ensure yt-dlp options are set correctly for this session."""
from pathlib import Path
@@ -1014,6 +1077,7 @@ def _queue_items(
existing_targets.add(norm)
# Remove duplicates from playlist starting from the end to keep indices valid
# Use wait=False for better performance, especially over slow IPC
for idx in reversed(dup_indexes):
try:
_send_ipc_command(
@@ -1022,7 +1086,8 @@ def _queue_items(
idx],
"request_id": 106
},
silent=True
silent=True,
wait=False
)
except Exception:
pass
@@ -1060,6 +1125,7 @@ def _queue_items(
"request_id": 198,
},
silent=True,
wait=False
)
except Exception:
pass
@@ -1124,9 +1190,8 @@ def _queue_items(
new_targets.add(norm_key)
# Use memory:// M3U hack to pass title to MPV.
# This is especially important for remote URLs (e.g., YouTube) where MPV may otherwise
# show the raw URL as the playlist title.
if title:
# Avoid this for probable ytdl URLs because it can prevent the hook from triggering.
if title and not _is_probable_ytdl_url(target):
# Sanitize title for M3U (remove newlines)
safe_title = title.replace("\n", " ").replace("\r", "")
@@ -1173,7 +1238,7 @@ def _queue_items(
"request_id":
199,
}
_send_ipc_command(header_cmd, silent=True)
_send_ipc_command(header_cmd, silent=True, wait=False)
if effective_ytdl_opts:
ytdl_cmd = {
"command":
@@ -1182,7 +1247,7 @@ def _queue_items(
effective_ytdl_opts],
"request_id": 197,
}
_send_ipc_command(ytdl_cmd, silent=True)
_send_ipc_command(ytdl_cmd, silent=True, wait=False)
# For memory:// M3U payloads (used to carry titles), use loadlist so mpv parses
# the content as a playlist and does not expose #EXTINF lines as entries.
@@ -1228,7 +1293,13 @@ def _queue_items(
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Manage and play items in the MPV playlist via IPC."""
parsed = parse_cmdlet_args(args, CMDLET)
log_filter_text: Optional[str] = None
args_for_parse, log_filter_text = _extract_log_filter(args)
parsed = parse_cmdlet_args(args_for_parse, CMDLET)
if log_filter_text:
log_filter_text = log_filter_text.strip()
if not log_filter_text:
log_filter_text = None
log_requested = bool(parsed.get("log"))
borderless = bool(parsed.get("borderless"))
@@ -1312,13 +1383,20 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
# If index_arg is provided but is not an integer, treat it as a URL
# This allows .pipe "http://..." without -url flag
if index_arg is not None:
try:
int(index_arg)
except ValueError:
# Avoid exception-based check to prevent debugger breaks on caught exceptions
index_str = str(index_arg).strip()
is_int = False
if index_str:
if index_str.isdigit():
is_int = True
elif index_str.startswith("-") and index_str[1:].isdigit():
is_int = True
if not is_int:
# Not an integer, treat as URL if url_arg is not set
if not url_arg:
url_arg = index_arg
index_arg = None
index_arg = None
clear_mode = parsed.get("clear")
list_mode = parsed.get("list")
@@ -1390,24 +1468,25 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
mpv_started = _queue_items([url_arg], clear_first=queue_replace, config=config, start_opts=start_opts, wait=False)
ctx.emit({"path": url_arg, "title": url_arg, "source": "load-url", "queued": True})
if not (clear_mode or play_mode or pause_mode or save_mode or load_mode or replace_mode):
play_mode = True
if mpv_started:
# MPV was just started, wait a moment for it to be ready, then play first item
import time
time.sleep(0.5)
index_arg = "1" # 1-based index for first item
play_mode = True
index_arg = "1"
else:
# MPV was already running, just show the updated playlist.
list_mode = True
# If already running, we want to play the item we just added (last one).
# We need to fetch the current playlist to find the count.
current_playlist = _get_playlist(silent=True) or []
if current_playlist:
index_arg = str(len(current_playlist))
# If we used queue_replace, the URL is already playing. Clear play/index args to avoid redundant commands.
if queue_replace:
play_mode = False
index_arg = None
# Ensure lyric overlay is running (auto-discovery handled by MPV.lyric).
try:
mpv = MPV()
_ensure_lyric_overlay(mpv)
@@ -1968,18 +2047,26 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
except Exception:
break
if tail_lines:
print("MPV log (tail):")
for ln in tail_lines:
filtered_tail = _apply_log_filter(tail_lines, log_filter_text)
if filtered_tail:
title = "MPV log (tail"
if log_filter_text:
title += f" filtered by '{log_filter_text}'"
title += "):"
print(title)
for ln in filtered_tail:
print(ln)
else:
print("MPV log (tail): <empty>")
print(
"Note: On some Windows builds, mpv cannot start writing to --log-file after launch."
)
print(
"If you need full [main2] logs, restart mpv so it starts with --log-file."
)
if log_filter_text:
print(f"MPV log (tail): <no entries match filter '{log_filter_text}'>")
else:
print("MPV log (tail): <empty>")
print(
"Note: On some Windows builds, mpv cannot start writing to --log-file after launch."
)
print(
"If you need full [main2] logs, restart mpv so it starts with --log-file."
)
# Print database logs for mpv module (helper output)
try:
@@ -1987,19 +2074,28 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
log_db_path = str((Path(__file__).resolve().parent.parent / "logs.db"))
conn = sqlite3.connect(log_db_path, timeout=5.0)
cur = conn.cursor()
cur.execute(
"SELECT level, module, message FROM logs WHERE module = 'mpv' ORDER BY timestamp DESC LIMIT 200"
)
query = "SELECT level, module, message FROM logs WHERE module = 'mpv'"
params: List[str] = []
if log_filter_text:
query += " AND LOWER(message) LIKE ?"
params.append(f"%{log_filter_text.lower()}%")
query += " ORDER BY timestamp DESC LIMIT 200"
cur.execute(query, tuple(params))
mpv_logs = cur.fetchall()
cur.close()
conn.close()
print("Helper logs from database (mpv module, most recent first):")
if log_filter_text:
print(f"Helper logs from database (mpv module, filtered by '{log_filter_text}', most recent first):")
else:
print("Helper logs from database (mpv module, most recent first):")
if mpv_logs:
for level, module, message in mpv_logs:
print(f"[{level}] {message}")
else:
print("(no helper logs found)")
if log_filter_text:
print(f"(no helper logs found matching '{log_filter_text}')")
else:
print("(no helper logs found)")
except Exception as e:
debug(f"Could not fetch database logs: {e}")
pass
@@ -2009,13 +2105,17 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
try:
helper_path = _helper_log_file()
helper_tail = _tail_text_file(str(helper_path), max_lines=200)
filtered_helper = _apply_log_filter(helper_tail, log_filter_text)
print(f"Helper log file: {str(helper_path)}")
if helper_tail:
if filtered_helper:
print("Helper log (tail):")
for ln in helper_tail:
for ln in filtered_helper:
print(ln)
else:
print("Helper log (tail): <empty>")
if log_filter_text:
print(f"(no helper file logs found matching '{log_filter_text}')")
else:
print("Helper log (tail): <empty>")
except Exception:
pass
@@ -2023,13 +2123,17 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
try:
lua_path = _lua_log_file()
lua_tail = _tail_text_file(str(lua_path), max_lines=200)
filtered_lua = _apply_log_filter(lua_tail, log_filter_text)
print(f"Lua log file: {str(lua_path)}")
if lua_tail:
if filtered_lua:
print("Lua log (tail):")
for ln in lua_tail:
for ln in filtered_lua:
print(ln)
else:
print("Lua log (tail): <empty>")
if log_filter_text:
print(f"(no lua file logs found matching '{log_filter_text}')")
else:
print("Lua log (tail): <empty>")
except Exception:
pass
except Exception:
@@ -2182,7 +2286,7 @@ CMDLET = Cmdlet(
name=".mpv",
alias=[".pipe", "pipe", "playlist", "queue", "ls-pipe"],
summary="Manage and play items in the MPV playlist via IPC",
usage=".mpv [index|url] [-current] [-clear] [-list] [-url URL] [-log] [-borderless]",
usage=".mpv [index|url] [-current] [-clear] [-list] [-url URL] [-log [filter text]] [-borderless]",
arg=[
CmdletArg(
name="index",
@@ -2224,7 +2328,7 @@ CMDLET = Cmdlet(
CmdletArg(
name="log",
type="flag",
description="Enable pipeable debug output and write an mpv log file",
description="Enable pipeable debug output, write an mpv log file, and optionally specify a filter string right after -log to search the stored logs",
),
CmdletArg(
name="borderless",