f
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -234,11 +234,10 @@ hydrusnetwork
|
||||
tests/
|
||||
scripts/mm.ps1
|
||||
scripts/mm
|
||||
.style.yapf
|
||||
..style.yapf
|
||||
.yapfignore
|
||||
tmp_*
|
||||
*.secret
|
||||
# Ignore local ZeroTier auth tokens (project-local copy)
|
||||
authtoken.secret
|
||||
|
||||
mypy.
|
||||
|
||||
114
CLI.py
114
CLI.py
@@ -97,7 +97,7 @@ from ProviderCore.registry import provider_inline_query_choices
|
||||
|
||||
|
||||
# Selection parsing and REPL lexer moved to SYS.cli_parsing
|
||||
from SYS.cli_parsing import Lexer, DRIVE_RE, KEY_PREFIX_RE, TOKEN_PATTERN, SELECTION_RANGE_RE, SelectionSyntax, SelectionFilterSyntax
|
||||
from SYS.cli_parsing import Lexer, DRIVE_RE, KEY_PREFIX_RE, TOKEN_PATTERN, SELECTION_RANGE_RE, SelectionSyntax, SelectionFilterSyntax, MedeiaLexer
|
||||
|
||||
|
||||
# SelectionFilterSyntax moved to SYS.cli_parsing (imported above)
|
||||
@@ -2353,114 +2353,12 @@ Lexer = _PTK_Lexer
|
||||
|
||||
|
||||
|
||||
class MedeiaLexer(Lexer):
|
||||
def lex_document(self, document: "Document") -> Callable[[int], List[Tuple[str, str]]]: # type: ignore[override]
|
||||
# The REPL lexer implementation is provided by `SYS.cli_parsing.MedeiaLexer`.
|
||||
# We import and expose it from there to avoid duplication and keep parsing
|
||||
# helpers centralized for testing and reuse.
|
||||
#
|
||||
# Note: The class previously defined here has been moved to `SYS.cli_parsing`.
|
||||
|
||||
def get_line(lineno: int) -> List[Tuple[str, str]]:
|
||||
"""Return token list for a single input line (used by prompt-toolkit)."""
|
||||
line = document.lines[lineno]
|
||||
tokens: List[Tuple[str, str]] = []
|
||||
|
||||
# Using TOKEN_PATTERN precompiled at module scope.
|
||||
|
||||
is_cmdlet = True
|
||||
|
||||
def _emit_keyed_value(word: str) -> bool:
|
||||
"""Emit `key:` prefixes (comma-separated) as argument tokens.
|
||||
|
||||
Designed for values like:
|
||||
clip:3m4s-3m14s,1h22m-1h33m,item:2-3
|
||||
|
||||
Avoids special-casing URLs (://) and Windows drive paths (C:\\...).
|
||||
Returns True if it handled the token.
|
||||
"""
|
||||
if not word or ":" not in word:
|
||||
return False
|
||||
# Avoid URLs and common scheme patterns.
|
||||
if "://" in word:
|
||||
return False
|
||||
# Avoid Windows drive paths (e.g., C:\\foo or D:/bar)
|
||||
if DRIVE_RE.match(word):
|
||||
return False
|
||||
|
||||
parts = word.split(",")
|
||||
handled_any = False
|
||||
for i, part in enumerate(parts):
|
||||
if i > 0:
|
||||
tokens.append(("class:value", ","))
|
||||
if part == "":
|
||||
continue
|
||||
m = KEY_PREFIX_RE.match(part)
|
||||
if m:
|
||||
tokens.append(("class:argument", m.group(1)))
|
||||
if m.group(2):
|
||||
tokens.append(("class:value", m.group(2)))
|
||||
handled_any = True
|
||||
else:
|
||||
tokens.append(("class:value", part))
|
||||
handled_any = True
|
||||
|
||||
return handled_any
|
||||
|
||||
for match in TOKEN_PATTERN.finditer(line):
|
||||
ws, pipe, quote, word = match.groups()
|
||||
if ws:
|
||||
tokens.append(("", ws))
|
||||
continue
|
||||
if pipe:
|
||||
tokens.append(("class:pipe", pipe))
|
||||
is_cmdlet = True
|
||||
continue
|
||||
if quote:
|
||||
# If the quoted token contains a keyed spec (clip:/item:/hash:),
|
||||
# highlight the `key:` portion in argument-blue even inside quotes.
|
||||
if len(quote) >= 2 and quote[0] == quote[-1] and quote[0] in ('"', "'"):
|
||||
q = quote[0]
|
||||
inner = quote[1:-1]
|
||||
start_index = len(tokens)
|
||||
if _emit_keyed_value(inner):
|
||||
# _emit_keyed_value already appended tokens for inner; insert opening quote
|
||||
# before that chunk, then add the closing quote.
|
||||
tokens.insert(start_index, ("class:string", q))
|
||||
tokens.append(("class:string", q))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
|
||||
tokens.append(("class:string", quote))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
if not word:
|
||||
continue
|
||||
|
||||
if word.startswith("@"): # selection tokens
|
||||
rest = word[1:]
|
||||
if rest and SELECTION_RANGE_RE.fullmatch(rest):
|
||||
tokens.append(("class:selection_at", "@"))
|
||||
tokens.append(("class:selection_range", rest))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
if rest and ":" in rest:
|
||||
tokens.append(("class:selection_at", "@"))
|
||||
tokens.append(("class:selection_filter", rest))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
if rest == "":
|
||||
tokens.append(("class:selection_at", "@"))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
|
||||
if is_cmdlet:
|
||||
tokens.append(("class:cmdlet", word))
|
||||
is_cmdlet = False
|
||||
elif word.startswith("-"):
|
||||
tokens.append(("class:argument", word))
|
||||
else:
|
||||
if not _emit_keyed_value(word):
|
||||
tokens.append(("class:value", word))
|
||||
|
||||
return tokens
|
||||
|
||||
return get_line
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -7,7 +7,7 @@ these pure helpers are easier to test.
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
|
||||
# Prompt-toolkit lexer types are optional at import time; fall back to lightweight
|
||||
# stubs if prompt_toolkit is not available so imports remain safe for testing.
|
||||
@@ -363,3 +363,111 @@ class SelectionFilterSyntax:
|
||||
return True
|
||||
|
||||
|
||||
class MedeiaLexer(Lexer):
|
||||
def lex_document(self, document: "Document") -> Callable[[int], List[Tuple[str, str]]]: # type: ignore[override]
|
||||
|
||||
def get_line(lineno: int) -> List[Tuple[str, str]]:
|
||||
"""Return token list for a single input line (used by prompt-toolkit)."""
|
||||
line = document.lines[lineno]
|
||||
tokens: List[Tuple[str, str]] = []
|
||||
|
||||
# Using TOKEN_PATTERN precompiled at module scope.
|
||||
|
||||
is_cmdlet = True
|
||||
|
||||
def _emit_keyed_value(word: str) -> bool:
|
||||
"""Emit `key:` prefixes (comma-separated) as argument tokens.
|
||||
|
||||
Designed for values like:
|
||||
clip:3m4s-3m14s,1h22m-1h33m,item:2-3
|
||||
|
||||
Avoids special-casing URLs (://) and Windows drive paths (C:\\...).
|
||||
Returns True if it handled the token.
|
||||
"""
|
||||
if not word or ":" not in word:
|
||||
return False
|
||||
# Avoid URLs and common scheme patterns.
|
||||
if "://" in word:
|
||||
return False
|
||||
# Avoid Windows drive paths (e.g., C:\\foo or D:/bar)
|
||||
if DRIVE_RE.match(word):
|
||||
return False
|
||||
|
||||
parts = word.split(",")
|
||||
handled_any = False
|
||||
for i, part in enumerate(parts):
|
||||
if i > 0:
|
||||
tokens.append(("class:value", ","))
|
||||
if part == "":
|
||||
continue
|
||||
m = KEY_PREFIX_RE.match(part)
|
||||
if m:
|
||||
tokens.append(("class:argument", m.group(1)))
|
||||
if m.group(2):
|
||||
tokens.append(("class:value", m.group(2)))
|
||||
handled_any = True
|
||||
else:
|
||||
tokens.append(("class:value", part))
|
||||
handled_any = True
|
||||
|
||||
return handled_any
|
||||
|
||||
for match in TOKEN_PATTERN.finditer(line):
|
||||
ws, pipe, quote, word = match.groups()
|
||||
if ws:
|
||||
tokens.append(("", ws))
|
||||
continue
|
||||
if pipe:
|
||||
tokens.append(("class:pipe", pipe))
|
||||
is_cmdlet = True
|
||||
continue
|
||||
if quote:
|
||||
# If the quoted token contains a keyed spec (clip:/item:/hash:),
|
||||
# highlight the `key:` portion in argument-blue even inside quotes.
|
||||
if len(quote) >= 2 and quote[0] == quote[-1] and quote[0] in ('"', "'"):
|
||||
q = quote[0]
|
||||
inner = quote[1:-1]
|
||||
start_index = len(tokens)
|
||||
if _emit_keyed_value(inner):
|
||||
tokens.insert(start_index, ("class:string", q))
|
||||
tokens.append(("class:string", q))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
|
||||
tokens.append(("class:string", quote))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
if not word:
|
||||
continue
|
||||
|
||||
if word.startswith("@"): # selection tokens
|
||||
rest = word[1:]
|
||||
if rest and SELECTION_RANGE_RE.fullmatch(rest):
|
||||
tokens.append(("class:selection_at", "@"))
|
||||
tokens.append(("class:selection_range", rest))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
if rest and ":" in rest:
|
||||
tokens.append(("class:selection_at", "@"))
|
||||
tokens.append(("class:selection_filter", rest))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
if rest == "":
|
||||
tokens.append(("class:selection_at", "@"))
|
||||
is_cmdlet = False
|
||||
continue
|
||||
|
||||
if is_cmdlet:
|
||||
tokens.append(("class:cmdlet", word))
|
||||
is_cmdlet = False
|
||||
elif word.startswith("-"):
|
||||
tokens.append(("class:argument", word))
|
||||
else:
|
||||
if not _emit_keyed_value(word):
|
||||
tokens.append(("class:value", word))
|
||||
|
||||
return tokens
|
||||
|
||||
return get_line
|
||||
|
||||
|
||||
|
||||
@@ -477,7 +477,7 @@ def load_config() -> Dict[str, Any]:
|
||||
stores = list(db_config.get("store", {}).keys()) if isinstance(db_config.get("store"), dict) else []
|
||||
mtime = None
|
||||
try:
|
||||
mtime = datetime.datetime.utcfromtimestamp(db.db_path.stat().st_mtime).isoformat() + "Z"
|
||||
mtime = datetime.datetime.fromtimestamp(db.db_path.stat().st_mtime, datetime.timezone.utc).isoformat().replace('+00:00', 'Z')
|
||||
except Exception:
|
||||
mtime = None
|
||||
summary = (
|
||||
|
||||
2
TUI.py
2
TUI.py
@@ -533,7 +533,7 @@ class PipelineHubApp(App):
|
||||
stores = list(cfg.get("store", {}).keys()) if isinstance(cfg.get("store"), dict) else []
|
||||
prov_display = ", ".join(provs[:10]) + ("..." if len(provs) > 10 else "")
|
||||
store_display = ", ".join(stores[:10]) + ("..." if len(stores) > 10 else "")
|
||||
self._append_log_line(f"Startup config: providers={len(provs)} ({prov_display or '(none)'}), stores={len(stores)} ({store_display or '(none)')}, db={db.db_path.name}")
|
||||
self._append_log_line(f"Startup config: providers={len(provs)} ({prov_display or '(none)'}), stores={len(stores)} ({store_display or '(none)'}), db={db.db_path.name}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
13
readme.md
13
readme.md
@@ -37,7 +37,7 @@ Medios-Macina is a API driven file media manager and virtual toolbox capable of
|
||||
<li><b>Module Mixing:</b> *[Playwright](https://github.com/microsoft/playwright), [yt-dlp](https://github.com/yt-dlp/yt-dlp), [typer](https://github.com/fastapi/typer)*</li>
|
||||
<li><b>Optional stacks:</b> Telethon (Telegram), aioslsk (Soulseek), and the FlorenceVision tooling install automatically when you configure the corresponding provider/tool blocks.
|
||||
<li><b>MPV Manager:</b> Play audio, video, and even images in a custom designed MPV with trimming, screenshotting, and more built right in!</li>
|
||||
<li><i>Works well with ZeroTier, you can create a private VPN that connects you to your media server from any smart device, you can even have a private sharing libraries with friends!</i></li>
|
||||
<li><i>Supports remote access and networked setups for offsite servers and sharing workflows.</i></li>
|
||||
</ul>
|
||||
</ul
|
||||
|
||||
@@ -48,19 +48,14 @@ Medios-Macina is a API driven file media manager and virtual toolbox capable of
|
||||
<br>
|
||||
|
||||
<details>
|
||||
<summary>LINUX</summary>
|
||||
<summary>COMMAND LINE</summary>
|
||||
|
||||
<pre><code>curl -sSL https://code.glowers.club/goyimnose/Medios-Macina/raw/branch/main/scripts/bootstrap.py | python3 -
|
||||
<pre><code>
|
||||
</code></pre>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>WINDOWS</summary>
|
||||
<pre><code>Invoke-RestMethod 'https://code.glowers.club/goyimnose/Medios-Macina/raw/branch/main/scripts/bootstrap.py' | python -
|
||||
</code></pre>
|
||||
|
||||
</details>
|
||||
you may need to change python3 to python depending on your python installation
|
||||
<br>
|
||||
<b>After install, start the CLI by simply inputting "mm" into terminal/console, once the application is up and running you will need to connect to a HydrusNetwork sever to get the full experience. To access the config simply input ".config" while the application is running</b>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user