hh
This commit is contained in:
194
cmdlets/pipe.py
194
cmdlets/pipe.py
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user