Compare commits
9 Commits
main
...
47b8e5f80c
| Author | SHA1 | Date | |
|---|---|---|---|
| 47b8e5f80c | |||
| 1011b0639e | |||
|
|
9384169c0e | ||
|
|
4df4fb3bd9 | ||
| 090cb8484e | |||
| e6dfcdafea | |||
| 264f8ee0f8 | |||
| 00a1371793 | |||
| a25f5fff42 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,7 +6,6 @@ __pycache__/
|
||||
config.json
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
@@ -217,3 +216,4 @@ luac.out
|
||||
*.hex
|
||||
|
||||
|
||||
config.json
|
||||
|
||||
63
CLI.py
63
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:
|
||||
|
||||
@@ -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.
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
media management
|
||||
|
||||
python cli.py
|
||||
|
||||
1. search-file -provider youtube "something in the way"
|
||||
|
||||
2. @1
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
32
config.json
Normal file
32
config.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"debug": false,
|
||||
"provider": {
|
||||
"openlibrary": {
|
||||
"email": "openlibrary.org-email",
|
||||
"password": "openlibrary.org-pass"
|
||||
},
|
||||
"soulseek": {
|
||||
"password": "putinsomethingrandom3",
|
||||
"username": "dfsdfsdf"
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"debrid": {
|
||||
"All-debrid": null
|
||||
},
|
||||
"hydrus": {
|
||||
"home": {
|
||||
"key": "access-api-key-goes-here",
|
||||
"url": "http://192.168.#.###:45869"
|
||||
},
|
||||
"work": {
|
||||
"key": null,
|
||||
"url": null
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"path": "C:\\put\\path\\for\\files"
|
||||
}
|
||||
},
|
||||
"temp": "C:\\do\\not\\use\\system\\temp"
|
||||
}
|
||||
Reference in New Issue
Block a user