This commit is contained in:
nose
2025-12-01 14:42:30 -08:00
parent 6b9ed7d4ab
commit 89aa24961b
8 changed files with 565 additions and 51 deletions

View File

@@ -10,9 +10,11 @@ from helper.logger import log, debug
from result_table import ResultTable
from helper.mpv_ipc import get_ipc_pipe_path, MPVIPCClient
import pipeline as ctx
from helper.download import is_url_supported_by_ytdlp
from helper.local_library import LocalLibrarySearchOptimizer
from config import get_local_storage_path
from hydrus_health_check import get_cookies_file_path
def _send_ipc_command(command: Dict[str, Any], silent: bool = False) -> Optional[Any]:
"""Send a command to the MPV IPC pipe and return the response."""
@@ -70,6 +72,62 @@ def _extract_title_from_item(item: Dict[str, Any]) -> str:
return title or filename or "Unknown"
def _ensure_ytdl_cookies() -> None:
"""Ensure yt-dlp options are set correctly for this session."""
from pathlib import Path
cookies_path = get_cookies_file_path()
if cookies_path:
# Check if file exists and has content (use forward slashes for path checking)
check_path = cookies_path.replace('\\', '/')
file_obj = Path(cookies_path)
if file_obj.exists():
file_size = file_obj.stat().st_size
debug(f"Cookies file verified: {check_path} ({file_size} bytes)")
else:
debug(f"WARNING: Cookies file does not exist: {check_path}", file=sys.stderr)
else:
debug("No cookies file configured")
def _monitor_mpv_logs(duration: float = 3.0) -> None:
"""Monitor MPV logs for a short duration to capture errors."""
try:
client = MPVIPCClient()
if not client.connect():
debug("Failed to connect to MPV for log monitoring", file=sys.stderr)
return
# Request log messages
client.send_command({"command": ["request_log_messages", "warn"]})
import time
start_time = time.time()
while time.time() - start_time < duration:
# We need to read raw lines from the socket
if client.is_windows:
try:
line = client.sock.readline()
if line:
try:
msg = json.loads(line)
if msg.get("event") == "log-message":
text = msg.get("text", "").strip()
prefix = msg.get("prefix", "")
level = msg.get("level", "")
if "ytdl" in prefix or level == "error":
debug(f"[MPV {prefix}] {text}", file=sys.stderr)
except json.JSONDecodeError:
pass
except Exception:
break
else:
# Unix socket handling (simplified)
break
time.sleep(0.05)
client.disconnect()
except Exception:
pass
def _queue_items(items: List[Any], clear_first: bool = False) -> bool:
"""Queue items to MPV, starting it if necessary.
@@ -80,6 +138,9 @@ def _queue_items(items: List[Any], clear_first: bool = False) -> bool:
Returns:
True if MPV was started, False if items were queued via IPC.
"""
# Just verify cookies are configured, don't try to set via IPC
_ensure_ytdl_cookies()
for i, item in enumerate(items):
# Extract URL/Path
target = None
@@ -95,11 +156,14 @@ def _queue_items(items: List[Any], clear_first: bool = False) -> bool:
target = item
if target:
# Add to MPV playlist
# We use loadfile with append flag (or replace if clear_first is set)
# Check if it's a yt-dlp supported URL
is_ytdlp = False
if target.startswith("http") and is_url_supported_by_ytdlp(target):
is_ytdlp = True
# Use memory:// M3U hack to pass title to MPV
if title:
# Skip for yt-dlp URLs to ensure proper handling
if title and not is_ytdlp:
# Sanitize title for M3U (remove newlines)
safe_title = title.replace('\n', ' ').replace('\r', '')
m3u_content = f"#EXTM3U\n#EXTINF:-1,{safe_title}\n{target}"
@@ -164,10 +228,26 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
mpv_started = False
if url_arg:
mpv_started = _queue_items([url_arg])
# If we just queued a URL, we probably want to list the playlist to show it was added
# Auto-play the URL when it's queued via .pipe "url" (without explicit flags)
# unless other flags are present
if not (clear_mode or play_mode or pause_mode or save_mode or load_mode):
list_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
else:
# MPV was already running, get playlist and play the newly added item
playlist = _get_playlist(silent=True)
if playlist and len(playlist) > 0:
# Auto-play the last item in the playlist (the one we just added)
# Use 1-based indexing
index_arg = str(len(playlist))
play_mode = True
else:
# Fallback: just list the playlist if we can't determine index
list_mode = True
# Handle Save Playlist
if save_mode:
@@ -290,8 +370,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
print(table)
return 0
# Handle Play/Pause commands
if play_mode:
# Handle Play/Pause commands (but skip if we have index_arg to play a specific item)
if play_mode and index_arg is None:
cmd = {"command": ["set_property", "pause", False], "request_id": 103}
resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success":
@@ -345,13 +425,19 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
if items is None:
if mpv_started:
# MPV was just started, so we can't list items yet.
# But we know it's running (or trying to start), so don't start another instance.
return 0
# MPV was just started, retry getting playlist after a brief delay
import time
time.sleep(0.3)
items = _get_playlist(silent=True)
debug("MPV is not running. Starting new instance...")
_start_mpv([])
return 0
if items is None:
# Still can't connect, but MPV is starting
debug("MPV is starting up...")
return 0
else:
debug("MPV is not running. Starting new instance...")
_start_mpv([])
return 0
if not items:
debug("MPV playlist is empty.")
@@ -393,11 +479,13 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
_send_ipc_command(unpause_cmd)
debug(f"Playing: {title}")
# Monitor logs briefly for errors (e.g. ytdl failures)
_monitor_mpv_logs(3.0)
return 0
else:
debug(f"Failed to play item: {resp.get('error') if resp else 'No response'}")
return 1
except ValueError:
debug(f"Invalid index: {index_arg}")
return 1
@@ -443,44 +531,70 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
def _start_mpv(items: List[Any]) -> None:
"""Start MPV with a list of items."""
import subprocess
import time as _time_module
# Kill any existing MPV processes to ensure clean start
try:
subprocess.run(['taskkill', '/IM', 'mpv.exe', '/F'],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=2)
_time_module.sleep(0.5) # Wait for process to die
except Exception:
pass
ipc_pipe = get_ipc_pipe_path()
# Start MPV in idle mode with IPC server
cmd = ['mpv', f'--input-ipc-server={ipc_pipe}', '--idle', '--force-window']
cmd.append('--ytdl-format=bestvideo[height<=?1080]+bestaudio/best[height<=?1080]')
# Add items
for item in items:
target = None
title = None
if isinstance(item, dict):
target = item.get("target") or item.get("url") or item.get("path") or item.get("filename")
title = item.get("title") or item.get("name")
elif hasattr(item, "target"):
target = item.target
title = getattr(item, "title", None)
elif isinstance(item, str):
target = item
if target:
if title:
# Use memory:// M3U hack to pass title
safe_title = title.replace('\n', ' ').replace('\r', '')
m3u_content = f"#EXTM3U\n#EXTINF:-1,{safe_title}\n{target}"
cmd.append(f"memory://{m3u_content}")
else:
cmd.append(target)
# Use cookies.txt if available, otherwise fallback to browser cookies
cookies_path = get_cookies_file_path()
if cookies_path:
# yt-dlp on Windows needs forward slashes OR properly escaped backslashes
# Using forward slashes is more reliable across systems
cookies_path_normalized = cookies_path.replace('\\', '/')
debug(f"Starting MPV with cookies file: {cookies_path_normalized}")
# yt-dlp expects the cookies option with file path
cmd.append(f'--ytdl-raw-options=cookies={cookies_path_normalized}')
else:
# Use cookies from browser (Chrome) to handle age-restricted content
debug("Starting MPV with browser cookies: chrome")
cmd.append('--ytdl-raw-options=cookies-from-browser=chrome')
try:
kwargs = {}
if platform.system() == 'Windows':
kwargs['creationflags'] = 0x00000008 # DETACHED_PROCESS
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs)
debug(f"Started MPV with {len(items)} items")
# Log the complete MPV command being executed
debug(f"DEBUG: Full MPV command: {' '.join(cmd)}")
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
debug(f"Started MPV process")
# Wait for IPC pipe to be ready
import time
max_retries = 20
for i in range(max_retries):
time.sleep(0.2)
client = MPVIPCClient(socket_path=ipc_pipe)
if client.connect():
client.disconnect()
break
else:
debug("Timed out waiting for MPV IPC connection", file=sys.stderr)
return
# Queue items via IPC
if items:
_queue_items(items)
except Exception as e:
debug(f"Error starting MPV: {e}", file=sys.stderr)
CMDLET = Cmdlet(
name=".pipe",
aliases=["pipe", "playlist", "queue", "ls-pipe"],