nh
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -232,6 +232,9 @@ hydrusnetwork
|
|||||||
.style.yapf
|
.style.yapf
|
||||||
.yapfignore
|
.yapfignore
|
||||||
tests/
|
tests/
|
||||||
|
scripts/mm.ps1
|
||||||
|
scripts/mm
|
||||||
|
.style.yapf
|
||||||
|
.yapfignore
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = @'
|
||||||
|
|||||||
@@ -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():
|
||||||
@@ -481,10 +578,36 @@ def main() -> int:
|
|||||||
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 = (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user