This commit is contained in:
nose
2025-11-27 10:59:01 -08:00
parent e9b505e609
commit 9eff65d1af
30 changed files with 2099 additions and 1095 deletions

View File

@@ -6,13 +6,15 @@ import shutil as _shutil
import subprocess as _subprocess
import json
import sys
import platform
from helper.logger import log, debug
import uuid as _uuid
import time as _time
from downlow_helpers.progress import print_progress, print_final_progress, format_size
from downlow_helpers.http_client import HTTPClient
from helper.progress import print_progress, print_final_progress
from helper.http_client import HTTPClient
from helper.mpv_ipc import get_ipc_pipe_path, send_to_mpv
import fnmatch as _fnmatch
from . import register
@@ -21,7 +23,7 @@ import pipeline as ctx
from helper import hydrus as hydrus_wrapper
from ._shared import Cmdlet, CmdletArg, normalize_hash, looks_like_hash, create_pipe_object_result
from config import resolve_output_dir, get_hydrus_url, get_hydrus_access_key
from downlow_helpers.alldebrid import AllDebridClient
from helper.alldebrid import AllDebridClient
@@ -248,158 +250,63 @@ def _is_playable_in_mpv(file_path_or_ext: str, mime_type: Optional[str] = None)
return False
def _get_fixed_ipc_pipe() -> str:
"""Get the fixed IPC pipe name for persistent MPV connection.
Uses a fixed name 'mpv-medeia-macina' so all playback sessions
connect to the same MPV window/process instead of creating new instances.
"""
import platform
if platform.system() == 'Windows':
return "\\\\.\\pipe\\mpv-medeia-macina"
else:
return "/tmp/mpv-medeia-macina.sock"
def _send_to_mpv_pipe(file_url: str, ipc_pipe: str, title: str, headers: Optional[Dict[str, str]] = None) -> bool:
"""Send loadfile command to existing MPV via IPC pipe.
Returns True if successfully sent to existing MPV, False if pipe unavailable.
"""
import json
import socket
import platform
try:
# Prepare commands
# Use set_property for headers as loadfile options can be unreliable via IPC
header_str = ""
if headers:
header_str = ",".join([f"{k}: {v}" for k, v in headers.items()])
# Command 1: Set headers (or clear them)
cmd_headers = {
"command": ["set_property", "http-header-fields", header_str],
"request_id": 0
}
# Command 2: Load file using memory:// M3U to preserve title
# Sanitize title to avoid breaking M3U format
safe_title = title.replace("\n", " ").replace("\r", "")
m3u_content = f"#EXTM3U\n#EXTINF:-1,{safe_title}\n{file_url}\n"
cmd_load = {
"command": ["loadfile", f"memory://{m3u_content}", "append-play"],
"request_id": 1
}
if platform.system() == 'Windows':
# Windows named pipes require special handling
try:
# Open in r+b to read response
with open(ipc_pipe, 'r+b', buffering=0) as pipe:
# Send headers
pipe.write((json.dumps(cmd_headers) + "\n").encode('utf-8'))
pipe.flush()
pipe.readline() # Consume response for headers
# Send loadfile
pipe.write((json.dumps(cmd_load) + "\n").encode('utf-8'))
pipe.flush()
# Read response
response_line = pipe.readline()
if response_line:
resp = json.loads(response_line.decode('utf-8'))
if resp.get('error') != 'success':
debug(f"[get-file] MPV error: {resp.get('error')}", file=sys.stderr)
return False
debug(f"[get-file] Sent to existing MPV: {title}", file=sys.stderr)
return True
except (OSError, IOError):
# Pipe not available
return False
else:
# Unix socket for Linux/macOS
if not hasattr(socket, 'AF_UNIX'):
return False
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(ipc_pipe)
# Send headers
sock.sendall((json.dumps(cmd_headers) + "\n").encode('utf-8'))
sock.recv(4096) # Consume response
# Send loadfile
sock.sendall((json.dumps(cmd_load) + "\n").encode('utf-8'))
# Read response
try:
response_data = sock.recv(4096)
if response_data:
resp = json.loads(response_data.decode('utf-8'))
if resp.get('error') != 'success':
debug(f"[get-file] MPV error: {resp.get('error')}", file=sys.stderr)
sock.close()
return False
except:
pass
sock.close()
debug(f"[get-file] Sent to existing MPV: {title}", file=sys.stderr)
return True
except (OSError, socket.error, ConnectionRefusedError):
# Pipe doesn't exist or MPV not listening - will need to start new instance
return False
except Exception as e:
debug(f"[get-file] IPC error: {e}", file=sys.stderr)
return False
def _play_in_mpv(file_url: str, file_title: str, is_stream: bool = False, headers: Optional[Dict[str, str]] = None) -> bool:
"""Play file in MPV using IPC pipe, creating new instance if needed.
"""Play file in MPV using centralized IPC pipe, creating new instance if needed.
Returns True on success, False on error.
"""
ipc_pipe = _get_fixed_ipc_pipe()
import json
import socket
import platform
try:
# First try to send to existing MPV instance
if _send_to_mpv_pipe(file_url, ipc_pipe, file_title, headers):
if send_to_mpv(file_url, file_title, headers):
debug(f"Added to MPV: {file_title}")
return True
# No existing MPV or pipe unavailable - start new instance
ipc_pipe = get_ipc_pipe_path()
debug(f"[get-file] Starting new MPV instance (pipe: {ipc_pipe})", file=sys.stderr)
cmd = ['mpv', file_url, f'--input-ipc-server={ipc_pipe}']
# Set title for new instance
cmd.append(f'--force-media-title={file_title}')
# Build command - start MPV without a file initially, just with IPC server
cmd = ['mpv', f'--input-ipc-server={ipc_pipe}']
if headers:
# Format headers for command line
# --http-header-fields="Header1: Val1,Header2: Val2"
header_str = ",".join([f"{k}: {v}" for k, v in headers.items()])
cmd.append(f'--http-header-fields={header_str}')
# Add --idle flag so MPV stays running and waits for playlist commands
cmd.append('--idle')
# Detach process to prevent freezing parent CLI
kwargs = {}
if platform.system() == 'Windows':
# CREATE_NEW_CONSOLE might be better than CREATE_NO_WINDOW if MPV needs a window
# But usually MPV creates its own window.
# DETACHED_PROCESS (0x00000008) is also an option.
kwargs['creationflags'] = 0x00000008 # DETACHED_PROCESS
kwargs['creationflags'] = 0x00000008 # DETACHED_PROCESS
_subprocess.Popen(cmd, stdin=_subprocess.DEVNULL, stdout=_subprocess.DEVNULL, stderr=_subprocess.DEVNULL, **kwargs)
debug(f"{'Streaming' if is_stream else 'Playing'} in MPV: {file_title}")
debug(f"[get-file] Started MPV with {file_title} (IPC: {ipc_pipe})", file=sys.stderr)
return True
debug(f"[get-file] Started MPV instance (IPC: {ipc_pipe})", file=sys.stderr)
# Give MPV time to start and open IPC pipe
# Windows needs more time than Unix
wait_time = 1.0 if platform.system() == 'Windows' else 0.5
debug(f"[get-file] Waiting {wait_time}s for MPV to initialize IPC...", file=sys.stderr)
_time.sleep(wait_time)
# Try up to 3 times to send the file via IPC
for attempt in range(3):
debug(f"[get-file] Sending file via IPC (attempt {attempt + 1}/3)", file=sys.stderr)
if send_to_mpv(file_url, file_title, headers):
debug(f"{'Streaming' if is_stream else 'Playing'} in MPV: {file_title}")
debug(f"[get-file] Added to new MPV instance (IPC: {ipc_pipe})", file=sys.stderr)
return True
if attempt < 2:
# Wait before retrying
_time.sleep(0.3)
# IPC send failed after all retries
log("Error: Could not send file to MPV via IPC after startup", file=sys.stderr)
return False
except FileNotFoundError:
log("Error: MPV not found. Install mpv to play media files", file=sys.stderr)
@@ -516,7 +423,7 @@ def _handle_hydrus_file(file_hash: Optional[str], file_title: str, config: Dict[
if force_browser:
# User explicitly wants browser
ipc_pipe = _get_fixed_ipc_pipe()
ipc_pipe = get_ipc_pipe_path()
result_dict = create_pipe_object_result(
source='hydrus',
identifier=file_hash,
@@ -559,7 +466,7 @@ def _handle_hydrus_file(file_hash: Optional[str], file_title: str, config: Dict[
return 0
else:
# Not media, open in browser
ipc_pipe = _get_fixed_ipc_pipe()
ipc_pipe = get_ipc_pipe_path()
result_dict = create_pipe_object_result(
source='hydrus',
identifier=file_hash,
@@ -1193,7 +1100,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
# Normal file export (happens regardless of -metadata flag)
try:
from downlow_helpers.hydrus import hydrus_export as _hydrus_export
from helper.hydrus import hydrus_export as _hydrus_export
except Exception:
_hydrus_export = None # type: ignore
if _hydrus_export is None: