This commit is contained in:
nose
2025-11-25 22:34:41 -08:00
parent 8d36b87d6e
commit 4187b778d2
6 changed files with 219 additions and 85 deletions

2
.gitignore vendored
View File

@@ -6,7 +6,6 @@ __pycache__/
config.json config.json
# C extensions # C extensions
*.so *.so
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
@@ -217,3 +216,4 @@ luac.out
*.hex *.hex
config.json

63
CLI.py
View File

@@ -30,12 +30,16 @@ try:
from prompt_toolkit import PromptSession from prompt_toolkit import PromptSession
from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style
PROMPT_TOOLKIT_AVAILABLE = True PROMPT_TOOLKIT_AVAILABLE = True
except ImportError: # pragma: no cover - optional dependency except ImportError: # pragma: no cover - optional dependency
PromptSession = None # type: ignore PromptSession = None # type: ignore
Completer = None # type: ignore Completer = None # type: ignore
Completion = None # type: ignore Completion = None # type: ignore
Document = None # type: ignore Document = None # type: ignore
Lexer = None # type: ignore
Style = None # type: ignore
PROMPT_TOOLKIT_AVAILABLE = False PROMPT_TOOLKIT_AVAILABLE = False
@@ -531,6 +535,46 @@ if (
async def get_completions_async(self, document: Document, complete_event): # type: ignore[override] async def get_completions_async(self, document: Document, complete_event): # type: ignore[override]
for completion in self.get_completions(document, complete_event): for completion in self.get_completions(document, complete_event):
yield completion yield completion
class MedeiaLexer(Lexer):
def lex_document(self, document):
def get_line(lineno):
line = document.lines[lineno]
tokens = []
import re
# Match: Whitespace, Pipe, Quoted string, or Word
pattern = re.compile(r'''
(\s+) | # 1. Whitespace
(\|) | # 2. Pipe
("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*') | # 3. Quoted string
([^\s\|]+) # 4. Word
''', re.VERBOSE)
is_cmdlet = True
for match in pattern.finditer(line):
ws, pipe, quote, word = match.groups()
if ws:
tokens.append(('', ws))
elif pipe:
tokens.append(('class:pipe', pipe))
is_cmdlet = True
elif quote:
tokens.append(('class:string', quote))
is_cmdlet = False
elif word:
if is_cmdlet:
tokens.append(('class:cmdlet', word))
is_cmdlet = False
elif word.startswith('-'):
tokens.append(('class:argument', word))
else:
tokens.append(('class:value', word))
return tokens
return get_line
else: # pragma: no cover - prompt toolkit unavailable else: # pragma: no cover - prompt toolkit unavailable
CmdletCompleter = None # type: ignore[assignment] CmdletCompleter = None # type: ignore[assignment]
@@ -586,7 +630,21 @@ Example: search-file --help
if PROMPT_TOOLKIT_AVAILABLE and PromptSession is not None and CmdletCompleter is not None: if PROMPT_TOOLKIT_AVAILABLE and PromptSession is not None and CmdletCompleter is not None:
completer = CmdletCompleter() completer = CmdletCompleter()
session = PromptSession(completer=cast(Any, completer))
# Define style for syntax highlighting
style = Style.from_dict({
'cmdlet': '#ffffff', # white
'argument': '#3b8eea', # blue-ish
'value': '#ce9178', # red-ish
'string': '#ce55ff', # purple
'pipe': '#4caf50', # green
})
session = PromptSession(
completer=cast(Any, completer),
lexer=MedeiaLexer(),
style=style
)
def get_input(prompt: str = ">>>|") -> str: def get_input(prompt: str = ">>>|") -> str:
return session.prompt(prompt) return session.prompt(prompt)
@@ -645,6 +703,7 @@ Example: search-file --help
if last_table is None: if last_table is None:
last_table = ctx.get_last_result_table() last_table = ctx.get_last_result_table()
if last_table: if last_table:
print() print()
# Also update current stage table so @N expansion works correctly # Also update current stage table so @N expansion works correctly
@@ -779,10 +838,10 @@ def _execute_pipeline(tokens: list):
else: else:
# Try command-based expansion first if we have source command info # Try command-based expansion first if we have source command info
command_expanded = False command_expanded = False
selected_row_args = []
if source_cmd: if source_cmd:
# Try to find row args for the selected indices # Try to find row args for the selected indices
selected_row_args = []
for idx in first_stage_selection_indices: for idx in first_stage_selection_indices:
row_args = ctx.get_current_stage_table_row_selection_args(idx) row_args = ctx.get_current_stage_table_row_selection_args(idx)
if row_args: if row_args:

View File

@@ -1,64 +0,0 @@
# Medeia-Macina
A powerful CLI media management and search platform integrating local files, Hydrus, torrents, books, and P2P networks.
## Key Features
* **Unified Search**: Search across Local, Hydrus, LibGen, Soulseek, and Debrid.
* **Pipeline Architecture**: Chain commands like PowerShell (e.g., `search | filter | download`).
* **Smart Selection**: Use `@N` syntax to interact with results.
* **Metadata Management**: Tagging, notes, and relationships.
## Installation
1. Install Python 3.9+ and [Deno](https://deno.com/) (for YouTube support).
2. Install dependencies: `pip install -r requirements.txt`
3. Run the CLI: `python CLI.py`
## Command Examples
### Search & Download
```powershell
# Search and download the first result
search-file "daughter" | @1 | download-data
# Search specific provider and download
search-file -provider libgen "dune" | @1 | download-data
# Download YouTube video (auto-probes formats)
download-data "https://youtube.com/watch?v=..."
# Select format #2 from the list
@2 | download-data
```
### File Management
```powershell
# Add file to Hydrus
add-file -path "C:\Videos\movie.mp4" -storage hydrus
# Upload to 0x0.st and associate URL with Hydrus file
search-file "my_video" | @1 | add-file -provider 0x0
# Add tags to a file
search-file "video" | @1 | add-tag "creator:someone, character:hero"
# Use tag lists (from helper/adjective.json)
@1 | add-tag "{gnostic}"
```
### Metadata & Notes
```powershell
# Add a note
search-file "doc" | @1 | add-note "comment" "This is important"
# Get tags
search-file "image" | @1 | get-tag
```
### Pipeline Syntax
* `|` : Pipe results from one command to another.
* `@N` : Select the Nth item from the previous result (e.g., `@1`).
* `@N-M` : Select a range (e.g., `@1-5`).
* `@{1,3,5}` : Select specific items.
* `@*` : Select all items.
## Configuration
Edit `config.json` to set API keys (AllDebrid, OpenAI), storage paths, and Hydrus credentials.

View File

@@ -1,11 +1,64 @@
# Medios-Macina # Medeia-Macina
media management A powerful CLI media management and search platform integrating local files, Hydrus, torrents, books, and P2P networks.
python cli.py ## Key Features
* **Unified Search**: Search across Local, Hydrus, LibGen, Soulseek, and Debrid.
* **Pipeline Architecture**: Chain commands like PowerShell (e.g., `search | filter | download`).
* **Smart Selection**: Use `@N` syntax to interact with results.
* **Metadata Management**: Tagging, notes, and relationships.
1. search-file -provider youtube "something in the way" ## Installation
1. Install Python 3.9+ and [Deno](https://deno.com/) (for YouTube support).
2. Install dependencies: `pip install -r requirements.txt`
3. Run the CLI: `python CLI.py`
2. @1 ## Command Examples
1. download-data "https://altrusiangrace.bandcamp.com/album/ancient-egyptian-legends-full-audiobook" | merge-file | add-file -storage local ### Search & Download
```powershell
# Search and download the first result
search-file "daughter" | @1 | download-data
# Search specific provider and download
search-file -provider libgen "dune" | @1 | download-data
# Download YouTube video (auto-probes formats)
download-data "https://youtube.com/watch?v=..."
# Select format #2 from the list
@2 | download-data
```
### File Management
```powershell
# Add file to Hydrus
add-file -path "C:\Videos\movie.mp4" -storage hydrus
# Upload to 0x0.st and associate URL with Hydrus file
search-file "my_video" | @1 | add-file -provider 0x0
# Add tags to a file
search-file "video" | @1 | add-tag "creator:someone, character:hero"
# Use tag lists (from helper/adjective.json)
@1 | add-tag "{gnostic}"
```
### Metadata & Notes
```powershell
# Add a note
search-file "doc" | @1 | add-note "comment" "This is important"
# Get tags
search-file "image" | @1 | get-tag
```
### Pipeline Syntax
* `|` : Pipe results from one command to another.
* `@N` : Select the Nth item from the previous result (e.g., `@1`).
* `@N-M` : Select a range (e.g., `@1-5`).
* `@{1,3,5}` : Select specific items.
* `@*` : Select all items.
## Configuration
Edit `config.json` to set API keys (AllDebrid, OpenAI), storage paths, and Hydrus credentials.

View File

@@ -2459,6 +2459,38 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any], emit_results:
if downloaded_files or files_downloaded_directly > 0: if downloaded_files or files_downloaded_directly > 0:
total_files = len(downloaded_files) + files_downloaded_directly total_files = len(downloaded_files) + files_downloaded_directly
log(f"✓ Successfully downloaded {total_files} file(s)", flush=True) log(f"✓ Successfully downloaded {total_files} file(s)", flush=True)
# Create a result table for the downloaded files
# This ensures that subsequent @N commands select from these files
# instead of trying to expand the previous command (e.g. search-file)
if downloaded_files:
from result_table import ResultTable
table = ResultTable("Downloaded Files")
for i, file_path in enumerate(downloaded_files):
row = table.add_row()
row.add_column("#", str(i + 1))
row.add_column("File", file_path.name)
row.add_column("Path", str(file_path))
try:
size_mb = file_path.stat().st_size / (1024*1024)
row.add_column("Size", f"{size_mb:.1f} MB")
except OSError:
row.add_column("Size", "?")
# Set selection args to just the file path (or index if we want item selection)
# For item selection fallback, we don't strictly need row args if source command is None
# But setting them helps if we want to support command expansion later
table.set_row_selection_args(i, [str(file_path)])
# Register the table but DO NOT set a source command
# This forces CLI to use item-based selection (filtering the pipe)
# instead of command expansion
pipeline_context.set_last_result_table_overlay(table, downloaded_files)
pipeline_context.set_current_stage_table(table)
# Also print the table so user sees what they got
log(str(table), flush=True)
if db: if db:
db.update_worker_status(worker_id, 'completed') db.update_worker_status(worker_id, 'completed')
return 0 return 0

View File

@@ -104,6 +104,29 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
clear_mode = parsed.get("clear") clear_mode = parsed.get("clear")
list_mode = parsed.get("list") list_mode = parsed.get("list")
play_mode = parsed.get("play")
pause_mode = parsed.get("pause")
# Handle Play/Pause commands
if play_mode:
cmd = {"command": ["set_property", "pause", False], "request_id": 103}
resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success":
log("Resumed playback")
return 0
else:
log("Failed to resume playback (MPV not running?)", file=sys.stderr)
return 1
if pause_mode:
cmd = {"command": ["set_property", "pause", True], "request_id": 104}
resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success":
log("Paused playback")
return 0
else:
log("Failed to pause playback (MPV not running?)", file=sys.stderr)
return 1
# Handle piped input (add to playlist) # Handle piped input (add to playlist)
if result: if result:
@@ -132,15 +155,19 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
if target: if target:
# Add to MPV playlist # Add to MPV playlist
# We use loadfile with append flag # We use loadfile with append flag
# Configure 1080p limit for streams (bestvideo<=1080p + bestaudio)
options = {
"ytdl-format": "bestvideo[height<=?1080]+bestaudio/best[height<=?1080]"
}
# Use memory:// M3U hack to pass title to MPV
# This avoids "invalid parameter" errors with loadfile options
# and ensures the title is displayed in the playlist/window
if title: if title:
options["force-media-title"] = title # Sanitize title for M3U (remove newlines)
safe_title = title.replace('\n', ' ').replace('\r', '')
m3u_content = f"#EXTM3U\n#EXTINF:-1,{safe_title}\n{target}"
target_to_send = f"memory://{m3u_content}"
else:
target_to_send = target
cmd = {"command": ["loadfile", target, "append", options], "request_id": 200} cmd = {"command": ["loadfile", target_to_send, "append"], "request_id": 200}
resp = _send_ipc_command(cmd) resp = _send_ipc_command(cmd)
if resp is None: if resp is None:
@@ -154,6 +181,18 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
log(f"Queued: {title}") log(f"Queued: {title}")
else: else:
log(f"Queued: {target}") log(f"Queued: {target}")
else:
error_msg = str(resp.get('error'))
log(f"Failed to queue item: {error_msg}", file=sys.stderr)
# If error indicates parameter issues, try without options
# (Though memory:// should avoid this, we keep fallback just in case)
if "option" in error_msg or "parameter" in error_msg:
cmd = {"command": ["loadfile", target, "append"], "request_id": 201}
resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success":
added_count += 1
log(f"Queued (fallback): {title or target}")
if added_count > 0: if added_count > 0:
# If we added items, we might want to play the first one if nothing is playing? # If we added items, we might want to play the first one if nothing is playing?
@@ -198,6 +237,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
cmd = {"command": ["playlist-play-index", idx], "request_id": 102} cmd = {"command": ["playlist-play-index", idx], "request_id": 102}
resp = _send_ipc_command(cmd) resp = _send_ipc_command(cmd)
if resp and resp.get("error") == "success": if resp and resp.get("error") == "success":
# Ensure playback starts (unpause)
unpause_cmd = {"command": ["set_property", "pause", False], "request_id": 103}
_send_ipc_command(unpause_cmd)
log(f"Playing: {title}") log(f"Playing: {title}")
return 0 return 0
else: else:
@@ -275,8 +318,6 @@ def _start_mpv(items: List[Any]) -> None:
cmd.append('--ytdl-format=bestvideo[height<=?1080]+bestaudio/best[height<=?1080]') cmd.append('--ytdl-format=bestvideo[height<=?1080]+bestaudio/best[height<=?1080]')
# Add items # Add items
first_title_set = False
for item in items: for item in items:
target = None target = None
title = None title = None
@@ -291,10 +332,13 @@ def _start_mpv(items: List[Any]) -> None:
target = item target = item
if target: if target:
if not first_title_set and title: if title:
cmd.append(f'--force-media-title={title}') # Use memory:// M3U hack to pass title
first_title_set = True safe_title = title.replace('\n', ' ').replace('\r', '')
cmd.append(target) m3u_content = f"#EXTM3U\n#EXTINF:-1,{safe_title}\n{target}"
cmd.append(f"memory://{m3u_content}")
else:
cmd.append(target)
if len(cmd) > 3: # mpv + ipc + format + at least one file if len(cmd) > 3: # mpv + ipc + format + at least one file
try: try:
@@ -329,6 +373,16 @@ CMDLET = Cmdlet(
type="flag", type="flag",
description="List items (default)" description="List items (default)"
), ),
CmdletArg(
name="play",
type="flag",
description="Resume playback"
),
CmdletArg(
name="pause",
type="flag",
description="Pause playback"
),
], ],
exec=_run exec=_run
) )