Compare commits
7 Commits
main
...
9384169c0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9384169c0e | ||
|
|
4df4fb3bd9 | ||
| 090cb8484e | |||
| e6dfcdafea | |||
| 264f8ee0f8 | |||
| 00a1371793 | |||
| a25f5fff42 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
63
CLI.py
@@ -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:
|
||||||
|
|||||||
@@ -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
|
media management
|
||||||
|
|
||||||
|
python cli.py
|
||||||
|
|
||||||
1. search-file -provider youtube "something in the way"
|
1. search-file -provider youtube "something in the way"
|
||||||
|
|
||||||
2. @1
|
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:
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
32
config.json
Normal file
32
config.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"debug": false,
|
||||||
|
"provider": {
|
||||||
|
"openlibrary": {
|
||||||
|
"email": "dewibi7691@lagsixtome.com",
|
||||||
|
"password": "3t4J3NKSrV8sPsoY2"
|
||||||
|
},
|
||||||
|
"soulseek": {
|
||||||
|
"password": "rndpass",
|
||||||
|
"username": "doioae3432"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"debrid": {
|
||||||
|
"All-debrid": "YutC4nxq3zimg6ttMUji"
|
||||||
|
},
|
||||||
|
"hydrus": {
|
||||||
|
"home": {
|
||||||
|
"key": "d4321f178b10cb40b3ef604f864aa90bd6c27b3b44361f8e701315dcc22f1e1e",
|
||||||
|
"url": "http://192.168.1.230:45869"
|
||||||
|
},
|
||||||
|
"work": {
|
||||||
|
"key": null,
|
||||||
|
"url": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"local": {
|
||||||
|
"path": "C:\\Media Machina"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temp": "C:\\Users\\Admin\\Downloads"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user