This commit is contained in:
2025-12-31 22:05:25 -08:00
parent 807ea7f53a
commit cfd791d415
12 changed files with 248 additions and 175 deletions

5
.gitignore vendored
View File

@@ -232,6 +232,9 @@ hydrusnetwork
.style.yapf .style.yapf
.yapfignore .yapfignore
tests/ tests/
scripts/mm.ps1
scripts/mm
.style.yapf
.yapfignore

View File

@@ -35,20 +35,45 @@ def _try_import(module: str) -> bool:
return False return False
_FLORENCEVISION_DEPENDENCIES: List[Tuple[str, str]] = [
("transformers", "transformers>=4.45.0"),
("torch", "torch>=2.4.0"),
("PIL", "Pillow>=10.0.0"),
("einops", "einops>=0.8.0"),
("timm", "timm>=1.0.0"),
]
_PROVIDER_DEPENDENCIES: Dict[str, List[Tuple[str, str]]] = {
"telegram": [("telethon", "telethon>=1.36.0")],
"soulseek": [("aioslsk", "aioslsk>=1.6.0")],
}
def florencevision_missing_modules() -> List[str]: def florencevision_missing_modules() -> List[str]:
missing: List[str] = [] return [
# pillow is already in requirements, but keep the check for robustness. requirement
if not _try_import("transformers"): for import_name, requirement in _FLORENCEVISION_DEPENDENCIES
missing.append("transformers") if not _try_import(import_name)
if not _try_import("torch"): ]
missing.append("torch")
if not _try_import("PIL"):
missing.append("pillow") def _provider_missing_modules(config: Dict[str, Any]) -> Dict[str, List[str]]:
# Florence-2 remote code frequently requires these extras. missing: Dict[str, List[str]] = {}
if not _try_import("einops"): provider_cfg = (config or {}).get("provider")
missing.append("einops") if not isinstance(provider_cfg, dict):
if not _try_import("timm"): return missing
missing.append("timm")
for provider_name, requirements in _PROVIDER_DEPENDENCIES.items():
block = provider_cfg.get(provider_name)
if not isinstance(block, dict) or not block:
continue
missing_for_provider = [
requirement
for import_name, requirement in requirements
if not _try_import(import_name)
]
if missing_for_provider:
missing[provider_name] = missing_for_provider
return missing return missing
@@ -73,47 +98,51 @@ def _pip_install(requirements: List[str]) -> Tuple[bool, str]:
return False, str(exc) return False, str(exc)
def _install_requirements(label: str, requirements: List[str]) -> None:
if not requirements:
return
names = ", ".join(requirements)
status_text = f"Installing {label} dependencies: {names}"
try:
with stdout_console().status(status_text, spinner="dots"):
ok, detail = _pip_install(requirements)
except Exception:
log(f"[startup] {label} dependencies missing ({names}). Attempting auto-install...")
ok, detail = _pip_install(requirements)
if ok:
log(f"[startup] {label} dependency install OK")
else:
log(f"[startup] {label} dependency auto-install failed. {detail}")
def maybe_auto_install_configured_tools(config: Dict[str, Any]) -> None: def maybe_auto_install_configured_tools(config: Dict[str, Any]) -> None:
"""Best-effort dependency auto-installer for configured tools. """Best-effort dependency auto-installer for configured tools and providers.
This is intentionally conservative: This is intentionally conservative:
- Only acts when a tool block is enabled. - Only acts when a configuration block is present/enabled.
- Skips under pytest. - Skips under pytest.
Current supported tool(s): florencevision Current supported features: FlorenceVision tool, Telegram provider, Soulseek provider
""" """
if _is_pytest(): if _is_pytest():
return return
tool_cfg = (config or {}).get("tool") tool_cfg = (config or {}).get("tool")
if not isinstance(tool_cfg, dict): if isinstance(tool_cfg, dict):
return fv = tool_cfg.get("florencevision")
if isinstance(fv, dict) and _as_bool(fv.get("enabled"), False):
auto_install = _as_bool(fv.get("auto_install"), True)
if auto_install:
missing = florencevision_missing_modules()
if missing:
_install_requirements("FlorenceVision", missing)
fv = tool_cfg.get("florencevision") provider_missing = _provider_missing_modules(config)
if isinstance(fv, dict) and _as_bool(fv.get("enabled"), False): for provider_name, requirements in provider_missing.items():
auto_install = _as_bool(fv.get("auto_install"), True) label = f"{provider_name.title()} provider"
if not auto_install: _install_requirements(label, requirements)
return
missing = florencevision_missing_modules()
if not missing:
return
names = ", ".join(missing)
try:
with stdout_console().status(
f"Installing FlorenceVision dependencies: {names}",
spinner="dots",
):
ok, detail = _pip_install(missing)
except Exception:
log(f"[startup] FlorenceVision dependencies missing ({names}). Attempting auto-install...")
ok, detail = _pip_install(missing)
if ok:
log("[startup] FlorenceVision dependency install OK")
else:
log(f"[startup] FlorenceVision dependency auto-install failed. {detail}")
__all__ = ["maybe_auto_install_configured_tools", "florencevision_missing_modules"] __all__ = ["maybe_auto_install_configured_tools", "florencevision_missing_modules"]

View File

@@ -2567,13 +2567,6 @@ def resolve_tidal_manifest_path(item: Any) -> Optional[str]:
metadata["_tidal_manifest_url"] = selected_url metadata["_tidal_manifest_url"] = selected_url
except Exception: except Exception:
pass pass
try:
log(
f"[hifi] Resolved JSON manifest for track {metadata.get('trackId') or metadata.get('id')} to {selected_url}",
file=sys.stderr,
)
except Exception:
pass
return selected_url return selected_url
try: try:
metadata["_tidal_manifest_error"] = "JSON manifest contained no urls" metadata["_tidal_manifest_error"] = "JSON manifest contained no urls"

View File

@@ -640,11 +640,13 @@ class Add_File(Cmdlet):
or tidal_metadata.get("_tidal_manifest_url") or tidal_metadata.get("_tidal_manifest_url")
) )
if not manifest_source: if not manifest_source:
target_is_mpd = False
if isinstance(media_path_or_url, Path): if isinstance(media_path_or_url, Path):
manifest_source = media_path_or_url target_is_mpd = str(media_path_or_url).lower().endswith(".mpd")
elif isinstance(media_path_or_url, str): elif isinstance(media_path_or_url, str):
if media_path_or_url.lower().endswith(".mpd"): target_is_mpd = media_path_or_url.lower().endswith(".mpd")
manifest_source = media_path_or_url if target_is_mpd:
manifest_source = media_path_or_url
if manifest_source: if manifest_source:
downloaded, tmp_dir = self._download_manifest_with_ffmpeg(manifest_source) downloaded, tmp_dir = self._download_manifest_with_ffmpeg(manifest_source)

View File

@@ -39,6 +39,8 @@ dependencies = [
"yt-dlp[default]>=2023.11.0", "yt-dlp[default]>=2023.11.0",
"yt-dlp-ejs", # EJS challenge solver scripts for YouTube JavaScript challenges "yt-dlp-ejs", # EJS challenge solver scripts for YouTube JavaScript challenges
"requests>=2.31.0", "requests>=2.31.0",
"charset-normalizer>=3.2.0",
"certifi>=2024.12.0",
"httpx>=0.25.0", "httpx>=0.25.0",
# Document and data handling # Document and data handling
@@ -57,7 +59,7 @@ dependencies = [
"lxml>=4.9.0", "lxml>=4.9.0",
# Advanced searching and libraries # Advanced searching and libraries
"aioslsk>=1.6.0", # Optional Soulseek support installs aioslsk>=1.6.0 when [provider=soulseek] is configured.
"imdbinfo>=0.1.10", "imdbinfo>=0.1.10",
# Encryption and security # Encryption and security

View File

@@ -6,7 +6,8 @@ Medios-Macina is a CLI media manager and toolkit focused on downloading, tagging
- **Flexible syntax structure:** chain commands with `|` and select options from tables with `@N`. - **Flexible syntax structure:** chain commands with `|` and select options from tables with `@N`.
- **Multiple file stores:** *HYDRUSNETWORK, FOLDER* - **Multiple file stores:** *HYDRUSNETWORK, FOLDER*
- **Provider plugin integration:** *YOUTUBE, OPENLIBRARY, INTERNETARCHIVE, SOULSEEK, LIBGEN, ALLDEBRID, TELEGRAM, BANDCAMP* - **Provider plugin integration:** *YOUTUBE, OPENLIBRARY, INTERNETARCHIVE, SOULSEEK, LIBGEN, ALLDEBRID, TELEGRAM, BANDCAMP*
- **Module Mixing:** *[Playwright](https://github.com/microsoft/playwright), [yt-dlp](https://github.com/yt-dlp/yt-dlp), [aioslsk](https://github.com/JurgenR/aioslsk), [telethon](https://github.com/LonamiWebs/Telethon),[typer](https://github.com/fastapi/typer)* - **Module Mixing:** *[Playwright](https://github.com/microsoft/playwright), [yt-dlp](https://github.com/yt-dlp/yt-dlp), [typer](https://github.com/fastapi/typer)*
- **Optional stacks:** Telethon (Telegram), aioslsk (Soulseek), and the FlorenceVision tooling install automatically when you configure the corresponding provider/tool blocks.
- **MPV Manager:** Play audio, video, and even images in a custom designed MPV with trimming, screenshotting, and more built right in! - **MPV Manager:** Play audio, video, and even images in a custom designed MPV with trimming, screenshotting, and more built right in!
## installation ⚡ ## installation ⚡
@@ -17,6 +18,10 @@ GIT CLONE https://code.glowers.club/goyimnose/Medios-Macina
- When run interactively (a normal terminal), `bootstrap.py` will show a short menu to Install or Uninstall the project. For non-interactive runs use flags such as `--no-playwright`, `--uninstall`, or `-y` to assume yes for confirmations. - When run interactively (a normal terminal), `bootstrap.py` will show a short menu to Install or Uninstall the project. For non-interactive runs use flags such as `--no-playwright`, `--uninstall`, or `-y` to assume yes for confirmations.
*Note:* If you run `--uninstall` while the local `.venv` is activated, the uninstaller will try to re-run the uninstall using a Python interpreter outside the venv so files can be removed safely (this is helpful on Windows). If no suitable interpreter can be found, deactivate the venv (`deactivate`) and re-run the uninstall.
Optional providers/tools bring their own dependencies instead of shipping as part of the base install; just configure `[provider=telegram]`, `[provider=soulseek]`, or `[tool=florencevision]` and the CLI will install the required packages on startup.
2. rename config.conf.remove to config.conf the store=folder path should be empty folder with no other files in it. 2. rename config.conf.remove to config.conf the store=folder path should be empty folder with no other files in it.
```ini ```ini

View File

@@ -526,41 +526,10 @@ try {
$globalBin = Join-Path $env:USERPROFILE 'bin' $globalBin = Join-Path $env:USERPROFILE 'bin'
New-Item -ItemType Directory -Path $globalBin -Force | Out-Null New-Item -ItemType Directory -Path $globalBin -Force | Out-Null
$mmCmd = Join-Path $globalBin 'mm.cmd'
$mmPs1 = Join-Path $globalBin 'mm.ps1' $mmPs1 = Join-Path $globalBin 'mm.ps1'
$repo = $repoRoot $repo = $repoRoot
$cmdText = @"
@echo off
set "REPO=__REPO__"
if exist "%REPO%\.venv\Scripts\mm.exe" "%REPO%\.venv\Scripts\mm.exe" %*
if defined MM_DEBUG (
echo MM_DEBUG: REPO=%REPO%
if exist "%REPO%\.venv\Scripts\python.exe" (
"%REPO%\.venv\Scripts\python.exe" -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);"
) else (
python -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);"
)
)
if exist "%REPO%\.venv\Scripts\python.exe" (
"%REPO%\.venv\Scripts\python.exe" -m medeia_macina.cli_entry %*
exit /b %ERRORLEVEL%
)
if exist "%REPO%\CLI.py" (
python "%REPO%\CLI.py" %*
exit /b %ERRORLEVEL%
)
python -m medeia_macina.cli_entry %*
"@
# Inject actual repo path safely (escape double-quotes if any)
$cmdText = $cmdText.Replace('__REPO__', $repo.Replace('"', '""'))
if (Test-Path $mmCmd) {
$bak = "$mmCmd.bak$(Get-Date -UFormat %s)"
Move-Item -Path $mmCmd -Destination $bak -Force
}
Set-Content -LiteralPath $mmCmd -Value $cmdText -Encoding UTF8
# PowerShell shim: use single-quoted here-string so literal PowerShell variables # PowerShell shim: use single-quoted here-string so literal PowerShell variables
# (like $args) are not expanded by this script when writing the file. # (like $args) are not expanded by this script when writing the file.
$ps1Text = @' $ps1Text = @'

View File

@@ -3,7 +3,7 @@
Unified project bootstrap helper (Python-only). Unified project bootstrap helper (Python-only).
This script installs Python dependencies from `requirements.txt` and then This script installs Python dependencies from `scripts/requirements.txt` and then
downloads Playwright browser binaries by running `python -m playwright install`. downloads Playwright browser binaries by running `python -m playwright install`.
By default this script installs **Chromium** only to conserve space; pass By default this script installs **Chromium** only to conserve space; pass
`--browsers all` to install all supported engines (chromium, firefox, webkit). `--browsers all` to install all supported engines (chromium, firefox, webkit).
@@ -30,7 +30,7 @@ Usage:
python ./scripts/bootstrap.py --playwright-only python ./scripts/bootstrap.py --playwright-only
Optional flags: Optional flags:
--skip-deps Skip `pip install -r requirements.txt` step --skip-deps Skip `pip install -r scripts/requirements.txt` step
--no-playwright Skip running `python -m playwright install` (still installs deps) --no-playwright Skip running `python -m playwright install` (still installs deps)
--playwright-only Install only Playwright browsers (installs playwright package if missing) --playwright-only Install only Playwright browsers (installs playwright package if missing)
--browsers Comma-separated list of Playwright browsers to install (default: chromium) --browsers Comma-separated list of Playwright browsers to install (default: chromium)
@@ -223,7 +223,7 @@ def main() -> int:
parser.add_argument( parser.add_argument(
"--skip-deps", "--skip-deps",
action="store_true", action="store_true",
help="Skip installing Python dependencies from requirements.txt", help="Skip installing Python dependencies from scripts/requirements.txt",
) )
parser.add_argument( parser.add_argument(
"--no-playwright", "--no-playwright",
@@ -317,12 +317,102 @@ def main() -> int:
return False return False
def _do_uninstall() -> int: def _do_uninstall() -> int:
"""Attempt to remove the local venv and any shims written to the user's bin.""" """Attempt to remove the local venv and any shims written to the user's bin.
If this script is running using the Python inside the local `.venv`, we
attempt to re-run the uninstall using a Python interpreter outside the
venv (so files can be removed on Windows). If no suitable external
interpreter can be found, the user is asked to deactivate the venv and
re-run the uninstall.
"""
vdir = repo_root / ".venv" vdir = repo_root / ".venv"
if not vdir.exists(): if not vdir.exists():
if not args.quiet: if not args.quiet:
print("No local .venv found; nothing to uninstall.") print("No local .venv found; nothing to uninstall.")
return 0 return 0
# If the current interpreter is the one inside the local venv, try to
# run the uninstall via a Python outside the venv so files (including
# the interpreter binary) can be removed on Windows.
try:
current_exe = Path(sys.executable).resolve()
in_venv = str(current_exe).lower().startswith(str(vdir.resolve()).lower())
except Exception:
in_venv = False
if in_venv:
if not args.quiet:
print(f"Detected local venv Python in use: {current_exe}")
if not args.yes:
try:
resp = input("Uninstall will be attempted using a system Python outside the .venv. Continue? [Y/n]: ")
except EOFError:
print("Non-interactive environment; pass --uninstall --yes to uninstall without prompts.", file=sys.stderr)
return 2
if resp.strip().lower() in ("n", "no"):
print("Uninstall aborted.")
return 1
def _find_external_python() -> list[str] | None:
"""Return a command (list) for a Python interpreter outside the venv, or None."""
try:
base = Path(sys.base_prefix)
candidates: list[Path | str] = []
if platform.system().lower() == "windows":
candidates.append(base / "python.exe")
else:
candidates.extend([base / "bin" / "python3", base / "bin" / "python"])
for name in ("python3", "python"):
p = shutil.which(name)
if p:
candidates.append(Path(p))
# Special-case the Windows py launcher: ensure it resolves
# to a Python outside the venv before returning ['py','-3']
if platform.system().lower() == "windows":
py_launcher = shutil.which("py")
if py_launcher:
try:
out = subprocess.check_output(["py", "-3", "-c", "import sys; print(sys.executable)"], text=True).strip()
if out and not str(Path(out).resolve()).lower().startswith(str(vdir.resolve()).lower()):
return ["py", "-3"]
except Exception:
pass
for c in candidates:
try:
if isinstance(c, Path) and c.exists():
c_resolved = Path(c).resolve()
if not str(c_resolved).lower().startswith(str(vdir.resolve()).lower()) and c_resolved != current_exe:
return [str(c_resolved)]
except Exception:
continue
except Exception:
pass
return None
ext = _find_external_python()
if ext:
cmd = ext + [str(repo_root / "scripts" / "bootstrap.py"), "--uninstall", "--yes"]
if not args.quiet:
print("Attempting uninstall using external Python:", " ".join(cmd))
rc = subprocess.run(cmd)
if rc.returncode != 0:
print(
f"External uninstall exited with {rc.returncode}; ensure no processes are using files in {vdir} and try again.",
file=sys.stderr,
)
return int(rc.returncode or 0)
print(
"Could not find a Python interpreter outside the local .venv. Please deactivate your venv (run 'deactivate') or run the uninstall from a system Python:\n python ./scripts/bootstrap.py --uninstall --yes",
file=sys.stderr,
)
return 2
# Normal (non-venv) uninstall flow: confirm and remove launchers, shims, and venv
if not args.yes: if not args.yes:
try: try:
prompt = input(f"Remove local virtualenv at {vdir} and installed user shims? [y/N]: ") prompt = input(f"Remove local virtualenv at {vdir} and installed user shims? [y/N]: ")
@@ -334,15 +424,19 @@ def main() -> int:
return 1 return 1
# Remove repo-local launchers # Remove repo-local launchers
for name in ("mm", "mm.ps1", "mm.bat"): def _remove_launcher(path: Path) -> None:
p = repo_root / name if path.exists():
if p.exists():
try: try:
p.unlink() path.unlink()
if not args.quiet: if not args.quiet:
print(f"Removed local launcher: {p}") print(f"Removed local launcher: {path}")
except Exception as exc: except Exception as exc:
print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr) print(f"Warning: failed to remove {path}: {exc}", file=sys.stderr)
scripts_launcher = repo_root / "scripts" / "mm.ps1"
_remove_launcher(scripts_launcher)
for legacy in ("mm", "mm.ps1", "mm.bat"):
_remove_launcher(repo_root / legacy)
# Remove user shims that the installer may have written # Remove user shims that the installer may have written
try: try:
@@ -350,12 +444,15 @@ def main() -> int:
if system == "windows": if system == "windows":
user_bin = Path(os.environ.get("USERPROFILE", str(Path.home()))) / "bin" user_bin = Path(os.environ.get("USERPROFILE", str(Path.home()))) / "bin"
if user_bin.exists(): if user_bin.exists():
for name in ("mm.cmd", "mm.ps1"): for name in ("mm.ps1",):
p = user_bin / name p = user_bin / name
if p.exists(): if p.exists():
p.unlink() try:
if not args.quiet: p.unlink()
print(f"Removed user shim: {p}") if not args.quiet:
print(f"Removed user shim: {p}")
except Exception as exc:
print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr)
else: else:
user_bin = Path(os.environ.get("XDG_BIN_HOME", str(Path.home() / ".local/bin"))) user_bin = Path(os.environ.get("XDG_BIN_HOME", str(Path.home() / ".local/bin")))
if user_bin.exists(): if user_bin.exists():
@@ -480,11 +577,37 @@ def main() -> int:
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
print(f"Failed to create or prepare local venv: {exc}", file=sys.stderr) print(f"Failed to create or prepare local venv: {exc}", file=sys.stderr)
raise raise
def _ensure_pip_available(python_path: Path) -> None:
"""Ensure pip is available inside the venv; fall back to ensurepip if needed."""
try:
subprocess.run(
[str(python_path), "-m", "pip", "--version"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
return
except Exception:
pass
if not args.quiet:
print("Bootstrapping pip inside the local virtualenv...")
try:
run([str(python_path), "-m", "ensurepip", "--upgrade"])
except subprocess.CalledProcessError as exc:
print(
"Failed to install pip inside the local virtualenv via ensurepip; ensure your Python build includes ensurepip and retry.",
file=sys.stderr,
)
raise
# Ensure a local venv is present and use it for subsequent installs. # Ensure a local venv is present and use it for subsequent installs.
venv_python = _ensure_local_venv() venv_python = _ensure_local_venv()
if not args.quiet: if not args.quiet:
print(f"Using venv python: {venv_python}") print(f"Using venv python: {venv_python}")
_ensure_pip_available(venv_python)
# Enforce opinionated behavior: install deps, playwright, deno, and install project in editable mode. # Enforce opinionated behavior: install deps, playwright, deno, and install project in editable mode.
# Ignore `--skip-deps` and `--install-editable` flags to keep the setup deterministic. # Ignore `--skip-deps` and `--install-editable` flags to keep the setup deterministic.
@@ -531,7 +654,7 @@ def main() -> int:
) )
if not args.skip_deps: if not args.skip_deps:
req_file = repo_root / "requirements.txt" req_file = repo_root / "scripts" / "requirements.txt"
if not req_file.exists(): if not req_file.exists():
print( print(
f"requirements.txt not found at {req_file}; skipping dependency installation.", f"requirements.txt not found at {req_file}; skipping dependency installation.",
@@ -662,35 +785,15 @@ def main() -> int:
print("Deno installation failed.", file=sys.stderr) print("Deno installation failed.", file=sys.stderr)
return rc return rc
# Write project-local launcher scripts (project root) that prefer the local .venv # Write project-local launcher script under scripts/ to keep the repo root uncluttered.
def _write_launchers() -> None: def _write_launchers() -> None:
sh = repo_root / "mm" launcher_dir = repo_root / "scripts"
ps1 = repo_root / "mm.ps1" launcher_dir.mkdir(parents=True, exist_ok=True)
bat = repo_root / "mm.bat" ps1 = launcher_dir / "mm.ps1"
sh_text = """#!/usr/bin/env bash
set -e
SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"
REPO=\"$SCRIPT_DIR\"
VENV=\"$REPO/.venv\"
# Make tools installed into the local venv available in PATH for provider discovery
export PATH=\"$VENV/bin:$PATH\"
PY=\"$VENV/bin/python\"
if [ -x \"$PY\" ]; then
exec \"$PY\" -m medeia_macina.cli_entry \"$@\"
else
exec python -m medeia_macina.cli_entry \"$@\"
fi
"""
try:
sh.write_text(sh_text, encoding="utf-8")
sh.chmod(sh.stat().st_mode | 0o111)
except Exception:
pass
ps1_text = r"""Param([Parameter(ValueFromRemainingArguments=$true)] $args) ps1_text = r"""Param([Parameter(ValueFromRemainingArguments=$true)] $args)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repo = $scriptDir $repo = (Resolve-Path (Join-Path $scriptDir "..")).Path
$venv = Join-Path $repo '.venv' $venv = Join-Path $repo '.venv'
# Ensure venv Scripts dir is on PATH for provider discovery # Ensure venv Scripts dir is on PATH for provider discovery
$venvScripts = Join-Path $venv 'Scripts' $venvScripts = Join-Path $venv 'Scripts'
@@ -707,19 +810,6 @@ python -m medeia_macina.cli_entry @args
except Exception: except Exception:
pass pass
bat_text = (
"@echo off\r\n"
"set SCRIPT_DIR=%~dp0\r\n"
"set PATH=%SCRIPT_DIR%\\.venv\\Scripts;%PATH%\r\n"
'if exist "%SCRIPT_DIR%\\.venv\\Scripts\\python.exe" "%SCRIPT_DIR%\\.venv\\Scripts\\python.exe" -m medeia_macina.cli_entry %*\r\n'
'if exist "%SCRIPT_DIR%\\CLI.py" python "%SCRIPT_DIR%\\CLI.py" %*\r\n'
"python -m medeia_macina.cli_entry %*\r\n"
)
try:
bat.write_text(bat_text, encoding="utf-8")
except Exception:
pass
_write_launchers() _write_launchers()
# Install user-global shims so `mm` can be executed from any shell session. # Install user-global shims so `mm` can be executed from any shell session.
@@ -732,24 +822,6 @@ python -m medeia_macina.cli_entry @args
user_bin = Path(os.environ.get("USERPROFILE", str(home))) / "bin" user_bin = Path(os.environ.get("USERPROFILE", str(home))) / "bin"
user_bin.mkdir(parents=True, exist_ok=True) user_bin.mkdir(parents=True, exist_ok=True)
# Write mm.cmd (CMD shim)
mm_cmd = user_bin / "mm.cmd"
cmd_text = (
f"@echo off\r\n"
f"set REPO={repo}\r\n"
f'if exist "%REPO%\\.venv\\Scripts\\mm.exe" "%REPO%\\.venv\\Scripts\\mm.exe" %*\r\n'
f"if defined MM_DEBUG (\r\n"
f" echo MM_DEBUG: REPO=%REPO%\r\n"
f' if exist "%REPO%\\.venv\\Scripts\\python.exe" "%REPO%\\.venv\\Scripts\\python.exe" -c "import sys,importlib,importlib.util; print(\'sys.executable:\', sys.executable); print(\'sys.path (first 8):\', sys.path[:8]);" \r\n'
f")\r\n"
f'if exist "%REPO%\\.venv\\Scripts\\python.exe" "%REPO%\\.venv\\Scripts\\python.exe" -m medeia_macina.cli_entry %*\r\n'
f"python -m medeia_macina.cli_entry %*\r\n"
)
if mm_cmd.exists():
bak = mm_cmd.with_suffix(f".bak{int(time.time())}")
mm_cmd.replace(bak)
mm_cmd.write_text(cmd_text, encoding="utf-8")
# Write mm.ps1 (PowerShell shim) # Write mm.ps1 (PowerShell shim)
mm_ps1 = user_bin / "mm.ps1" mm_ps1 = user_bin / "mm.ps1"
ps1_text = ( ps1_text = (

View File

@@ -10,7 +10,7 @@ Works on Linux and Windows. Behavior:
2) Update hydrus (git pull) 2) Update hydrus (git pull)
3) Re-clone (remove and re-clone) 3) Re-clone (remove and re-clone)
- If `git` is not available, the script will fall back to downloading the repository ZIP and extracting it. - If `git` is not available, the script will fall back to downloading the repository ZIP and extracting it.
- By default the script will create a repository-local virtual environment `./<dest>/.venv` after cloning/extraction; use `--no-venv` to skip this. By default the script will install dependencies from `requirements.txt` into that venv (use `--no-install-deps` to skip). After setup the script will print instructions for running the client; use `--run-client` to *launch* `hydrus_client.py` using the created repo venv's Python (use `--run-client-detached` to run it in the background). - By default the script will create a repository-local virtual environment `./<dest>/.venv` after cloning/extraction; use `--no-venv` to skip this. By default the script will install dependencies from `scripts/requirements.txt` into that venv (use `--no-install-deps` to skip). After setup the script will print instructions for running the client; use `--run-client` to *launch* `hydrus_client.py` using the created repo venv's Python (use `--run-client-detached` to run it in the background).
Examples: Examples:
python scripts/hydrusnetwork.py python scripts/hydrusnetwork.py
@@ -510,17 +510,18 @@ def fix_permissions(
def find_requirements(root: Path) -> Optional[Path]: def find_requirements(root: Path) -> Optional[Path]:
"""Return a requirements.txt Path if found in common locations (root, client, requirements) or via a shallow search. """Return a requirements.txt Path if found in common locations (scripts, root, client, requirements) or via a shallow search.
This tries a few sensible locations used by various projects and performs a shallow This tries a few sensible locations used by various projects and performs a shallow
two-level walk to find a requirements.txt so installation works even if the file is two-level walk to find a requirements.txt so installation works even if the file is
not at the repository root. not at the repository root.
""" """
candidates = [ candidates = [
root / "requirements.txt", root / "scripts" / "requirements.txt",
root / "client" / "requirements.txt", root / "requirements.txt",
root / "requirements" / "requirements.txt", root / "client" / "requirements.txt",
] root / "requirements" / "requirements.txt",
]
for c in candidates: for c in candidates:
if c.exists(): if c.exists():
return c return c

View File

@@ -8,7 +8,10 @@ textual>=0.30.0
yt-dlp[default]>=2023.11.0 yt-dlp[default]>=2023.11.0
requests>=2.31.0 requests>=2.31.0
httpx>=0.25.0 httpx>=0.25.0
telethon>=1.36.0 # Ensure requests can detect encodings and ship certificates
charset-normalizer>=3.2.0
certifi>=2024.12.0
# Optional Telegram support installs telethon>=1.36.0 when [provider=telegram] is configured.
internetarchive>=4.1.0 internetarchive>=4.1.0
# Document and data handling # Document and data handling
@@ -22,18 +25,12 @@ Pillow>=10.0.0
python-bidi>=0.4.2 python-bidi>=0.4.2
ffmpeg-python>=0.2.0 ffmpeg-python>=0.2.0
# AI tagging (FlorenceVision tool)
transformers>=4.45.0
torch>=2.4.0
einops>=0.8.0
timm>=1.0.0
# Metadata extraction and processing # Metadata extraction and processing
musicbrainzngs>=0.7.0 musicbrainzngs>=0.7.0
lxml>=4.9.0 lxml>=4.9.0
# Advanced searching and libraries # Advanced searching and libraries
aioslsk>=1.6.0 # Optional Soulseek support installs aioslsk>=1.6.0 when [provider=soulseek] is configured.
imdbinfo>=0.1.10 imdbinfo>=0.1.10
# Encryption and security (if needed by Crypto usage) # Encryption and security (if needed by Crypto usage)

View File

@@ -7,7 +7,7 @@ present or its copy of this helper gets removed.
Features (subset of the repo helper): Features (subset of the repo helper):
- Locate repository venv (default: <workspace>/hydrusnetwork/.venv) - Locate repository venv (default: <workspace>/hydrusnetwork/.venv)
- Install or reinstall requirements.txt into the venv - Install or reinstall scripts/requirements.txt into the venv
- Verify key imports - Verify key imports
- Launch hydrus_client.py (foreground or detached) - Launch hydrus_client.py (foreground or detached)
- Install/uninstall simple user-level start-on-boot services (schtasks/systemd/crontab) - Install/uninstall simple user-level start-on-boot services (schtasks/systemd/crontab)
@@ -50,7 +50,7 @@ def get_python_in_venv(venv_dir: Path) -> Optional[Path]:
def find_requirements(root: Path) -> Optional[Path]: def find_requirements(root: Path) -> Optional[Path]:
candidates = [root / "requirements.txt", root / "client" / "requirements.txt"] candidates = [root / "scripts" / "requirements.txt", root / "requirements.txt", root / "client" / "requirements.txt"]
for c in candidates: for c in candidates:
if c.exists(): if c.exists():
return c return c