diff --git a/.gitignore b/.gitignore index 2adf715..e406c20 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ __pycache__/ config.json # C extensions *.so - # Distribution / packaging .Python build/ @@ -217,3 +216,4 @@ luac.out *.hex +config.json diff --git a/CLI.py b/CLI.py index daee9b1..daa7194 100644 --- a/CLI.py +++ b/CLI.py @@ -30,12 +30,16 @@ try: from prompt_toolkit import PromptSession from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.document import Document + from prompt_toolkit.lexers import Lexer + from prompt_toolkit.styles import Style PROMPT_TOOLKIT_AVAILABLE = True except ImportError: # pragma: no cover - optional dependency PromptSession = None # type: ignore Completer = None # type: ignore Completion = None # type: ignore Document = None # type: ignore + Lexer = None # type: ignore + Style = None # type: ignore PROMPT_TOOLKIT_AVAILABLE = False @@ -531,6 +535,46 @@ if ( async def get_completions_async(self, document: Document, complete_event): # type: ignore[override] for completion in self.get_completions(document, complete_event): 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 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: 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: return session.prompt(prompt) @@ -645,6 +703,7 @@ Example: search-file --help if last_table is None: last_table = ctx.get_last_result_table() + if last_table: print() # Also update current stage table so @N expansion works correctly @@ -779,10 +838,10 @@ def _execute_pipeline(tokens: list): else: # Try command-based expansion first if we have source command info command_expanded = False + selected_row_args = [] if source_cmd: # Try to find row args for the selected indices - selected_row_args = [] for idx in first_stage_selection_indices: row_args = ctx.get_current_stage_table_row_selection_args(idx) if row_args: diff --git a/README copy.md b/README copy.md deleted file mode 100644 index 57480fa..0000000 --- a/README copy.md +++ /dev/null @@ -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. diff --git a/README.md b/README.md index 80fea2e..57480fa 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +### 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. diff --git a/cmdlets/download_data.py b/cmdlets/download_data.py index 222fd7f..f5a981d 100644 --- a/cmdlets/download_data.py +++ b/cmdlets/download_data.py @@ -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: total_files = len(downloaded_files) + files_downloaded_directly 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: db.update_worker_status(worker_id, 'completed') return 0 diff --git a/cmdlets/pipe.py b/cmdlets/pipe.py index e178caa..b87ee81 100644 --- a/cmdlets/pipe.py +++ b/cmdlets/pipe.py @@ -104,6 +104,29 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: clear_mode = parsed.get("clear") 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) if result: @@ -132,15 +155,19 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: 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]" - } + # 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: - 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) if resp is None: @@ -154,6 +181,18 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: log(f"Queued: {title}") else: 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 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} resp = _send_ipc_command(cmd) 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}") return 0 else: @@ -275,8 +318,6 @@ def _start_mpv(items: List[Any]) -> None: 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 @@ -291,10 +332,13 @@ def _start_mpv(items: List[Any]) -> None: 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 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) if len(cmd) > 3: # mpv + ipc + format + at least one file try: @@ -329,6 +373,16 @@ CMDLET = Cmdlet( type="flag", description="List items (default)" ), + CmdletArg( + name="play", + type="flag", + description="Resume playback" + ), + CmdletArg( + name="pause", + type="flag", + description="Pause playback" + ), ], exec=_run )