This commit is contained in:
nose
2025-11-25 20:09:33 -08:00
parent d75c644a82
commit bd69119996
80 changed files with 39615 additions and 0 deletions

335
cmdlets/pipe.py Normal file
View File

@@ -0,0 +1,335 @@
from typing import Any, Dict, Sequence, List, Optional
import sys
import json
import platform
import socket
import re
import subprocess
from ._shared import Cmdlet, CmdletArg, parse_cmdlet_args
from helper.logger import log
from result_table import ResultTable
from .get_file import _get_fixed_ipc_pipe
import pipeline as ctx
def _send_ipc_command(command: Dict[str, Any]) -> Optional[Any]:
"""Send a command to the MPV IPC pipe and return the response."""
ipc_pipe = _get_fixed_ipc_pipe()
request = json.dumps(command) + "\n"
try:
if platform.system() == 'Windows':
# Windows named pipe
# Opening in r+b mode to read response
try:
with open(ipc_pipe, 'r+b', buffering=0) as pipe:
pipe.write(request.encode('utf-8'))
pipe.flush()
# Read response
# We'll try to read a line. This might block if MPV is unresponsive.
response_line = pipe.readline()
if response_line:
return json.loads(response_line.decode('utf-8'))
except FileNotFoundError:
return None # MPV not running
except Exception as e:
log(f"Windows IPC Error: {e}", file=sys.stderr)
return None
else:
# Unix socket
af_unix = getattr(socket, 'AF_UNIX', None)
if af_unix is None:
log("Unix sockets not supported on this platform", file=sys.stderr)
return None
try:
sock = socket.socket(af_unix, socket.SOCK_STREAM)
sock.settimeout(2.0)
sock.connect(ipc_pipe)
sock.sendall(request.encode('utf-8'))
# Read response
response_data = b""
while True:
try:
chunk = sock.recv(4096)
if not chunk:
break
response_data += chunk
if b"\n" in chunk:
break
except socket.timeout:
break
sock.close()
if response_data:
# Parse lines, look for response to our request
lines = response_data.decode('utf-8').strip().split('\n')
for line in lines:
try:
resp = json.loads(line)
# If it has 'error' field, it's a response
if 'error' in resp:
return resp
except:
pass
except (FileNotFoundError, ConnectionRefusedError):
return None # MPV not running
except Exception as e:
log(f"Unix IPC Error: {e}", file=sys.stderr)
return None
except Exception as e:
log(f"IPC Error: {e}", file=sys.stderr)
return None
return None
def _get_playlist() -> List[Dict[str, Any]]:
"""Get the current playlist from MPV."""
cmd = {"command": ["get_property", "playlist"], "request_id": 100}
resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success":
return resp.get("data", [])
return []
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)
# Handle positional index argument if provided
index_arg = parsed.get("index")
clear_mode = parsed.get("clear")
list_mode = parsed.get("list")
# Handle piped input (add to playlist)
if result:
# If result is a list of items, add them to playlist
items_to_add = []
if isinstance(result, list):
items_to_add = result
elif isinstance(result, dict):
items_to_add = [result]
added_count = 0
for i, item in enumerate(items_to_add):
# Extract URL/Path
target = None
title = None
if isinstance(item, dict):
target = item.get("target") or item.get("url") or item.get("path")
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:
# Add to MPV playlist
# We use loadfile with append flag
# Configure 1080p limit for streams (bestvideo<=1080p + bestaudio)
options = {
"ytdl-format": "bestvideo[height<=?1080]+bestaudio/best[height<=?1080]"
}
if title:
options["force-media-title"] = title
cmd = {"command": ["loadfile", target, "append", options], "request_id": 200}
resp = _send_ipc_command(cmd)
if resp is None:
# MPV not running (or died)
# Start MPV with remaining items
_start_mpv(items_to_add[i:])
return 0
elif resp.get("error") == "success":
added_count += 1
if title:
log(f"Queued: {title}")
else:
log(f"Queued: {target}")
if added_count > 0:
# If we added items, we might want to play the first one if nothing is playing?
# For now, just list the playlist
pass
# Get playlist from MPV
items = _get_playlist()
if not items:
log("MPV playlist is empty or MPV is not running.")
return 0
# If index is provided, perform action (Play or Clear)
if index_arg is not None:
try:
# Handle 1-based index
idx = int(index_arg) - 1
if idx < 0 or idx >= len(items):
log(f"Index {index_arg} out of range (1-{len(items)}).")
return 1
item = items[idx]
title = item.get("title") or item.get("filename") or "Unknown"
if clear_mode:
# Remove item
cmd = {"command": ["playlist-remove", idx], "request_id": 101}
resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success":
log(f"Removed: {title}")
# Refresh items for listing
items = _get_playlist()
list_mode = True
index_arg = None
else:
log(f"Failed to remove item: {resp.get('error') if resp else 'No response'}")
return 1
else:
# Play item
cmd = {"command": ["playlist-play-index", idx], "request_id": 102}
resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success":
log(f"Playing: {title}")
return 0
else:
log(f"Failed to play item: {resp.get('error') if resp else 'No response'}")
return 1
except ValueError:
log(f"Invalid index: {index_arg}")
return 1
# List items (Default action or after clear)
if list_mode or index_arg is None:
if not items:
log("MPV playlist is empty.")
return 0
table = ResultTable("MPV Playlist")
for i, item in enumerate(items):
is_current = item.get("current", False)
title = item.get("title") or ""
filename = item.get("filename") or ""
# Special handling for memory:// M3U playlists (used to pass titles via IPC)
if "memory://" in filename and "#EXTINF:" in filename:
try:
# Extract title from #EXTINF:-1,Title
# Use regex to find title between #EXTINF:-1, and newline
match = re.search(r"#EXTINF:-1,(.*?)(?:\n|\r|$)", filename)
if match:
extracted_title = match.group(1).strip()
if not title or title == "memory://":
title = extracted_title
# Extract actual URL
# Find the first line that looks like a URL and not a directive
lines = filename.splitlines()
for line in lines:
line = line.strip()
if line and not line.startswith('#') and not line.startswith('memory://'):
filename = line
break
except Exception:
pass
# Truncate if too long
if len(title) > 57:
title = title[:57] + "..."
if len(filename) > 27:
filename = filename[:27] + "..."
row = table.add_row()
row.add_column("#", str(i + 1))
row.add_column("Current", "*" if is_current else "")
row.add_column("Title", title)
row.add_column("Filename", filename)
table.set_row_selection_args(i, [str(i + 1)])
table.set_source_command(".pipe")
# Register results with pipeline context so @N selection works
ctx.set_last_result_table_overlay(table, items)
ctx.set_current_stage_table(table)
print(table)
return 0
def _start_mpv(items: List[Any]) -> None:
"""Start MPV with a list of items."""
ipc_pipe = _get_fixed_ipc_pipe()
cmd = ['mpv', f'--input-ipc-server={ipc_pipe}']
cmd.append('--ytdl-format=bestvideo[height<=?1080]+bestaudio/best[height<=?1080]')
# Add items
first_title_set = False
for item in items:
target = None
title = None
if isinstance(item, dict):
target = item.get("target") or item.get("url") or item.get("path")
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 not first_title_set and title:
cmd.append(f'--force-media-title={title}')
first_title_set = True
cmd.append(target)
if len(cmd) > 3: # mpv + ipc + format + at least one file
try:
kwargs = {}
if platform.system() == 'Windows':
kwargs['creationflags'] = 0x00000008 # DETACHED_PROCESS
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs)
log(f"Started MPV with {len(cmd)-3} items")
except Exception as e:
log(f"Error starting MPV: {e}", file=sys.stderr)
CMDLET = Cmdlet(
name=".pipe",
aliases=["pipe", "playlist", "queue", "ls-pipe"],
summary="Manage and play items in the MPV playlist via IPC",
usage=".pipe [index] [-clear]",
args=[
CmdletArg(
name="index",
type="int",
description="Index of item to play or clear",
required=False
),
CmdletArg(
name="clear",
type="flag",
description="Remove the selected item from the playlist"
),
CmdletArg(
name="list",
type="flag",
description="List items (default)"
),
],
exec=_run
)