This commit is contained in:
2025-12-31 16:10:35 -08:00
parent 9464bd0d21
commit 807ea7f53a
10 changed files with 313 additions and 1179 deletions

View File

@@ -1,214 +0,0 @@
# set up config.conf
the config file is meant to be modular, you only need to put in what you want. technically you do not need a file store, but you will miss out on a bunch features. For this tutorial we are going to make an example folderstore. Add the below to your config.conf, pick a path for you that is a blank folder non-system folder.
<figure>
<figcaption>config.conf</figcaption>
<pre><code class="language-powershell">[store=folder]
name="tutorial"
path="C:\Users\Admin\Downloads\tutorial"
</code></pre>
</figure>
after your done save the file and restart the cli.py
# Downloading from youtube
### cookies.txt required - [cookies.txt guide](cookies.md)
start up the cli and enter this at prompt, you can copy and paste
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">download-media "https://www.youtube.com/watch?v=_23dFb50Z2Y" | add-file -store tutorial
</code></pre>
</figure>
![Available formats](<img/Available formats.svg>)
this shows the available formats you can download, the audio is audio only, and the video will automatically merge audio so you only need to pick the video if you want video and audio. it is ordered best quality at the lowest/highest number down.
the # column is how you select what you want to pick, run this next
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@8
</code></pre>
</figure>
![add-file](<img/add-file.svg>)
files inside of stores are renamed as their hash, you will only be able to find your file by searching for it, the title: tag acts as a psuedo filename, if you search "terry" then the file will be brought up, if you search "archeroflusitania" the file will not come up, you need to prepend the namespace first, "channel:archeroflusitania"; you can also use wild card "channel:arch*" and that will pull it up. lets see what tags the file has, run this.
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@1 | get-tag
</code></pre>
</figure>
![get-tag](<img/get-tag.svg>)
these tags are scraped from the youtube video using yt-dlp, they are stored in your store's database, this is how you will actually find your files so make sure it has either a title: tag that you can look up or you add your custom tags
now lets add more tags, run this
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@ | add-tag "cli,ubuntu,desktop"
</code></pre>
</figure>
![add-tag](<img/add-tag.svg>)
we added freeform tags, freeform tags are tags that dont have colons in them, these tags show up in searches without any special prepends. run the following below to search for our new tags added to the file.
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">search-file "ubuntu"
</code></pre>
</figure>
![search-file](<img/search-file.svg>)
to access your file and view it, you can run either
@1 | get-file
or if you have mpv installed (the preferred way for video files)
(Tip: the bootstrap/setup scripts will try to install `mpv` for you if it's not found on PATH. If it isn't available, install `mpv` manually and ensure `mpv` is on your PATH.)
@1 | .pipe
# Bandcamp downloading (provider method)
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">search-file -provider bandcamp -query "artist:altrusian grace media"
</code></pre>
</figure>
![search-file -provider bandcamp](img/bandcamp-artist.svg)
this brings up special scraper for bandcamp on the artist page, the query syntax of "artist:" is how we parse args for some cmdlets. next run the following
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@1
</code></pre>
</figure>
![bandcamp-artist](img/bandcamp-album.svg)
this shows a list of bandcamp album links, go ahead and enter this below
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@8
</code></pre>
</figure>
![download-media-bandcamp](img/download-media-bandcamp.svg)
this shows a list of bandcamp album link, if you look at the title of the table, download-media -url "https://altrusiangrace.bandcamp.com/album/zetetic-astronomy-earth-not-a-globe-full-audiobook" , you could just copy the url from webpage and paste it as this command to get the table without going through the provider search. In this table, all these are playlist items, each item is its own file but in this case its connected and they are in an album. if you wanted to only download a couple you could do @1,5,7 and that would only download those items, you can also do a range @1-6 and that will download 1 through 6. For this tutorial, run this below.
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@* | merge-file | add-file -store tutorial
</code></pre>
</figure>
this will download the entire playlist, merge them into one file while putting chapter markers for the file merge points.
# screenshot
medios can take screenshots of pages and output them as pdf, jpg, png, and webp. the default is webp as its the smallest format. the screenshot is fullscreen and scrolls. run this below
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">screen-shot "https://www.timecube.net/" | add-tag "title:timecube homepage" | add-file -store tutorial
</code></pre>
</figure>
![screen-shot](img/screen-shot.svg)
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@1 | get-file
</code></pre>
</figure>
this opens up a webrowser with your media file embedded, you can copy and paste or if you want to share on a public hoster, you can do this
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@1 | add-file -provider 0x0
</code></pre>
</figure>
this will upload your media file to 0x0.com and at the end provide you a link, you dont need to worry about saving it, you can always retrieve the link where you downloaded the file by running
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@1 | get-url
</code></pre>
</figure>
![get-url](img/get-url.svg)
# soul seek
## soul-seek requires a psuedo account
to make an account in soulseek, simple enter in random username and password and put in your config.conf. you do not need to create an account on the website just put in random name that isint taken with random password, dont user special symbols or spaces.
<figure>
<figcaption>config.conf</figcaption>
<pre><code class="language-powershell">[provider=soulseek]
username="putinrandomusername"
password="putinrandompassword"
</code></pre>
</figure>
restart the cli and check the startup table, if soulseek says ENABLED then you are good to go, run this.
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">search-file -provider soulseek "erika herms niel"
</code></pre>
</figure>
![soulseek](img/soulseek.svg)
next run
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">@13 | add-file -store tutorial
</code></pre>
</figure>
this will download and inject into your tutorial store, keep in mind that soulseek is p2p, so download speeds will vary and it may take a minute to connect.
# Open Library
## OPENLIBRARY/ARCHIVE.ORG ACCOUNT REQUIRED
[Open Library](https://archive.org/account/signup)
throw away account is fine to create.
<figure>
<figcaption>config.conf</figcaption>
<pre><code class="language-powershell">[provider=OpenLibrary]
email=""
password=""
</code></pre>
</figure>
openlibrary allows us to borrow books, merge them into a permement pdf, then return the book. run this below
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">download-file "https://archive.org/details/powerofsatiremag0000elli_h4h9" | add-file -store tutorial
</code></pre>
</figure>
we could have use the search-file -provider openlibrary, but to show the versatile of the app, we able to use download-file and medios will be able to intelligently direct it the correct provider (with exception of download-media, download-media is just the frontend for yt-dlp).
# Libgen
libgen is self-explanatory,
<figure>
<figcaption><🜂🜄|🜁🜃></figcaption>
<pre><code class="language-powershell">download-file "https://libgen.gl/ads.php?md5=5c258cc177c1d735c7acfb60fbdb14bf&downloadname=10.1515/9781400870080-009" | add-file -store tutorial
</code></pre>
</figure>

View File

@@ -1,61 +0,0 @@
"""Entry point wrapper for Medeia-Macina CLI.
This file is intentionally backwards-compatible. When installed from the
packaged distribution the preferred entry is `medeia_macina.cli_entry.main`.
When running from the repository (or in legacy installs) the module will
attempt to import `MedeiaCLI` from the top-level `CLI` module.
"""
import sys
from pathlib import Path
def _run_packaged_entry(argv=None) -> int:
"""Try to delegate to the packaged entry (`medeia_macina.cli_entry:main`)."""
try:
from medeia_macina.cli_entry import main as _main
return int(_main(argv) or 0)
except Exception:
return -1
def _run_legacy_entry() -> None:
"""Legacy behaviour: make repo root importable and run CLI.
This supports running directly from the source tree where `CLI.py` is
available as a top-level module.
"""
root_dir = Path(__file__).resolve().parent
if str(root_dir) not in sys.path:
sys.path.insert(0, str(root_dir))
try:
from CLI import MedeiaCLI
except Exception as exc: # pragma: no cover - user environment issues
raise ImportError(
"Could not import 'MedeiaCLI' from top-level 'CLI'. "
"If you installed the package into a virtualenv, activate it and run: \n"
" pip install -e .\n"
"or re-run the project bootstrap to ensure an up-to-date install."
) from exc
if __name__ == "__main__":
MedeiaCLI().run()
# Backward-compatibility: try to expose `MedeiaCLI` at import-time when the
# project is being used from a development checkout (so modules that import
# the top-level `medeia_entry` can still access the CLI class).
try:
from CLI import MedeiaCLI as MedeiaCLI # type: ignore
except Exception:
# It's okay if the legacy top-level CLI isn't importable in installed packages.
pass
if __name__ == "__main__":
rc = _run_packaged_entry(sys.argv[1:])
if rc >= 0:
raise SystemExit(rc)
# Fall back to legacy import when packaged entry couldn't be invoked.
_run_legacy_entry()

View File

@@ -1,7 +1,9 @@
"""CLI entrypoint module compatible with console scripts.
"""Packaged CLI entrypoint used by installers and console scripts.
This wraps the existing `medeia_entry.py` runner so installers can set
entry points to `medeia_macina.cli_entry:main`.
This module provides the `main` entrypoint for `mm`/`medeia` and supports
running from a development checkout (by importing the top-level
`CLI.MedeiaCLI`) or when running tests that inject a legacy
`medeia_entry` shim into `sys.modules`.
"""
from __future__ import annotations
@@ -128,86 +130,44 @@ def _parse_mode_and_strip_args(args: List[str]) -> Tuple[Optional[str], List[str
return mode, out
def _import_medeia_entry_module():
"""Import and return the top-level 'medeia_entry' module.
This attempts a regular import first. If that fails with ImportError it will
try a few fallbacks useful for editable installs and running directly from
the repository (searching for .egg-link, walking parents, or checking CWD).
"""
try:
_ensure_repo_root_on_sys_path()
return importlib.import_module("medeia_entry")
except ImportError:
# Try to find the project root next to this installed package
pkg_dir = Path(__file__).resolve().parent
# 1) Look for an .egg-link that points to the project root
try:
for egg in pkg_dir.glob("*.egg-link"):
try:
project_root = egg.read_text().splitlines()[0].strip()
if project_root:
candidate = Path(project_root) / "medeia_entry.py"
if candidate.exists():
if str(Path(project_root)) not in sys.path:
sys.path.insert(0, str(Path(project_root)))
return importlib.import_module("medeia_entry")
except Exception:
continue
except Exception:
pass
# 2) Walk upwards looking for a top-level 'medeia_entry.py'
for parent in pkg_dir.parents:
candidate = parent / "medeia_entry.py"
if candidate.exists():
if str(parent) not in sys.path:
sys.path.insert(0, str(parent))
return importlib.import_module("medeia_entry")
# 3) Check current working directory
candidate = Path.cwd() / "medeia_entry.py"
if candidate.exists():
if str(Path.cwd()) not in sys.path:
sys.path.insert(0, str(Path.cwd()))
return importlib.import_module("medeia_entry")
raise ImportError(
"Could not import 'medeia_entry'. This often means the package is not installed into the active virtualenv or is an outdated install.\n"
"Remedy: activate your venv and run: pip install -e . (or re-run the bootstrap script).\n"
"If problems persist, recreate the venv and reinstall the project."
)
def _run_cli(clean_args: List[str]) -> int:
"""Run the CLI runner (MedeiaCLI) with cleaned argv list."""
"""Run the CLI runner (MedeiaCLI) with cleaned argv list.
The function supports three modes (in order):
1) If a `medeia_entry` module is present in sys.modules (for tests/legacy
scenarios), use it as the source of `MedeiaCLI`.
2) If running from a development checkout, import the top-level `CLI` and
use `MedeiaCLI` from there.
3) Otherwise fail with a helpful message suggesting an editable install.
"""
try:
sys.argv[1:] = list(clean_args)
except Exception:
pass
mod = _import_medeia_entry_module()
# Backwards compatibility: the imported module may not expose `MedeiaCLI` as
# an attribute (for example, the installed `medeia_entry` delegates to the
# packaged entrypoint instead of importing the top-level `CLI` module at
# import-time). Try a few strategies to obtain or invoke the CLI:
# 1) Check for an in-memory 'medeia_entry' module (tests/legacy installs)
MedeiaCLI = None
if "medeia_entry" in sys.modules:
mod = sys.modules.get("medeia_entry")
if hasattr(mod, "MedeiaCLI"):
MedeiaCLI = getattr(mod, "MedeiaCLI")
else:
# Try importing the top-level `CLI` module directly (editable/repo mode).
# Preserve the existing error message used by tests that inject a
# dummy 'medeia_entry' without a `MedeiaCLI` attribute.
raise ImportError(
"Imported module 'medeia_entry' does not define 'MedeiaCLI' and direct import of top-level 'CLI' failed.\n"
"Remedy: ensure the top-level 'medeia_entry' module exports 'MedeiaCLI' or run from the project root/debug the checkout."
)
# 2) If no in-memory module provided the class, try importing the repo-root CLI
if MedeiaCLI is None:
try:
_ensure_repo_root_on_sys_path()
from CLI import MedeiaCLI as _M # type: ignore
MedeiaCLI = _M
except Exception:
raise ImportError(
"Imported module 'medeia_entry' does not define 'MedeiaCLI' and direct import of top-level 'CLI' failed.\n"
"Remedy: activate your venv and run: pip install -e . (or re-run the bootstrap script).\n"
"If problems persist, recreate the venv and reinstall the project."
"Could not import 'MedeiaCLI'. This often means the project is not available on sys.path (run 'pip install -e .' or re-run the bootstrap script)."
)
try:

View File

@@ -7,7 +7,7 @@ name = "medeia-macina"
version = "0.1.0"
description = "Comprehensive media management and search platform with support for local files, Hydrus database, torrents, books, and P2P networks"
readme = "README.md"
requires-python = ">=3.9,<3.12"
requires-python = ">=3.9,<3.14"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your.email@example.com"}
@@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Multimedia",
"Topic :: Internet",
]
@@ -111,7 +112,6 @@ Repository = "https://github.com/yourusername/medeia-macina.git"
Issues = "https://github.com/yourusername/medeia-macina/issues"
[tool.setuptools]
py-modules = ["medeia_entry"]
[tool.setuptools.packages.find]
where = ["."]

View File

@@ -15,6 +15,8 @@ GIT CLONE https://code.glowers.club/goyimnose/Medios-Macina
1. run python scripts\bootstrap.py
- 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.
2. rename config.conf.remove to config.conf the store=folder path should be empty folder with no other files in it.
```ini

View File

@@ -580,11 +580,44 @@ if (Test-Path (Join-Path $repo 'CLI.py')) { & python (Join-Path $repo 'CLI.py')
# fallback
python -m medeia_macina.cli_entry @args
'@
# Inject the actual repo path safely (escape embedded double-quotes if any)
# Thin wrapper: prefer the canonical Python bootstrap installer
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repo = (Resolve-Path (Join-Path $scriptDir "..")).Path
$venvPy = Join-Path $repo '.venv\Scripts\python.exe'
# Normalize incoming quiet flag (-Quiet) into -q for the Python script
$forwardArgs = @()
foreach ($a in $args) {
if ($a -eq '-Quiet') { $forwardArgs += '-q' } else { $forwardArgs += $a }
}
# Debug helper
if ($env:MM_DEBUG) { Write-Host "MM_DEBUG: invoking python installer with args: $forwardArgs" -ForegroundColor Yellow }
if (Test-Path $venvPy) {
& $venvPy (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
exit $LASTEXITCODE
}
# Fall back to any system Python (py -3 -> python3 -> python)
if (Get-Command -Name py -ErrorAction SilentlyContinue) {
& py -3 (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
exit $LASTEXITCODE
}
if (Get-Command -Name python3 -ErrorAction SilentlyContinue) {
& python3 (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
exit $LASTEXITCODE
}
if (Get-Command -Name python -ErrorAction SilentlyContinue) {
& python (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
exit $LASTEXITCODE
}
Write-Host 'Error: no suitable Python 3 interpreter found. Please install Python 3 or use the venv.' -ForegroundColor Red
exit 127 # Inject the actual repo path safely (escape embedded double-quotes if any)
$ps1Text = $ps1Text.Replace('__REPO__', $repo.Replace('"', '""'))
# Ensure the PowerShell shim falls back to the correct module when the venv isn't present
$ps1Text = $ps1Text.Replace(' -m medeia_entry ', ' -m medeia_macina.cli_entry ')
$ps1Text = $ps1Text.Replace('python -m medeia_entry', 'python -m medeia_macina.cli_entry')
# (No legacy 'medeia_entry' shim - use the packaged entry 'medeia_macina.cli_entry')
if (Test-Path $mmPs1) {
$bak = "$mmPs1.bak$(Get-Date -UFormat %s)"
Move-Item -Path $mmPs1 -Destination $bak -Force

View File

@@ -8,6 +8,11 @@ downloads Playwright browser binaries by running `python -m playwright install`.
By default this script installs **Chromium** only to conserve space; pass
`--browsers all` to install all supported engines (chromium, firefox, webkit).
Note: This Python script is the canonical installer for the project — prefer
running `python ./scripts/bootstrap.py` locally. The platform scripts
(`scripts/bootstrap.ps1` and `scripts/bootstrap.sh`) are now thin wrappers
that delegate to this script (they call it with `--no-delegate -q`).
When invoked without any arguments, `bootstrap.py` will automatically select and
run the platform-specific bootstrap helper (`scripts/bootstrap.ps1` on Windows
or `scripts/bootstrap.sh` on POSIX) in **non-interactive (quiet)** mode so a
@@ -230,6 +235,17 @@ def main() -> int:
action="store_true",
help="Only run 'playwright install' (skips dependency installation)",
)
parser.add_argument(
"--no-delegate",
action="store_true",
help="Do not delegate to platform bootstrap scripts; run the Python bootstrap directly.",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Quiet mode: minimize informational output (useful when called from platform wrappers)",
)
parser.add_argument(
"--browsers",
type=str,
@@ -264,18 +280,167 @@ def main() -> int:
action="store_true",
help="Upgrade pip/setuptools/wheel before installing requirements",
)
parser.add_argument(
"--uninstall",
action="store_true",
help="Uninstall local .venv and user shims (non-interactive)",
)
parser.add_argument(
"-y",
"--yes",
action="store_true",
help="Assume yes for confirmation prompts during uninstall",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
# If invoked without any arguments, prefer to delegate to the platform
# bootstrap script (if present). The bootstrap scripts support a quiet/
# non-interactive mode, which we use so "python ./scripts/bootstrap.py" just
# does the right thing on Windows and *nix without extra flags.
if len(sys.argv) == 1:
# Helpers for interactive menu and uninstall detection
def _venv_python_path(p: Path) -> Path | None:
"""Return the path to a python executable inside a venv directory if present."""
if (p / "Scripts" / "python.exe").exists():
return p / "Scripts" / "python.exe"
if (p / "bin" / "python").exists():
return p / "bin" / "python"
return None
def _is_installed() -> bool:
"""Return True if the project appears installed into the local .venv."""
vdir = repo_root / ".venv"
py = _venv_python_path(vdir)
if py is None:
return False
try:
rc = subprocess.run([str(py), "-m", "pip", "show", "medeia-macina"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
return rc.returncode == 0
except Exception:
return False
def _do_uninstall() -> int:
"""Attempt to remove the local venv and any shims written to the user's bin."""
vdir = repo_root / ".venv"
if not vdir.exists():
if not args.quiet:
print("No local .venv found; nothing to uninstall.")
return 0
if not args.yes:
try:
prompt = input(f"Remove local virtualenv at {vdir} and installed user shims? [y/N]: ")
except EOFError:
print("Non-interactive environment; pass --uninstall --yes to uninstall without prompts.", file=sys.stderr)
return 2
if prompt.strip().lower() not in ("y", "yes"):
print("Uninstall aborted.")
return 1
# Remove repo-local launchers
for name in ("mm", "mm.ps1", "mm.bat"):
p = repo_root / name
if p.exists():
try:
p.unlink()
if not args.quiet:
print(f"Removed local launcher: {p}")
except Exception as exc:
print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr)
# Remove user shims that the installer may have written
try:
system = platform.system().lower()
if system == "windows":
user_bin = Path(os.environ.get("USERPROFILE", str(Path.home()))) / "bin"
if user_bin.exists():
for name in ("mm.cmd", "mm.ps1"):
p = user_bin / name
if p.exists():
p.unlink()
if not args.quiet:
print(f"Removed user shim: {p}")
else:
user_bin = Path(os.environ.get("XDG_BIN_HOME", str(Path.home() / ".local/bin")))
if user_bin.exists():
p = user_bin / "mm"
if p.exists():
p.unlink()
if not args.quiet:
print(f"Removed user shim: {p}")
except Exception as exc:
print(f"Warning: failed to remove user shims: {exc}", file=sys.stderr)
# Remove .venv directory
try:
shutil.rmtree(vdir)
if not args.quiet:
print(f"Removed local virtualenv: {vdir}")
except Exception as exc:
print(f"Failed to remove venv: {exc}", file=sys.stderr)
return 1
return 0
def _interactive_menu() -> str | int:
"""Show a simple interactive menu to choose install/uninstall or delegate."""
try:
installed = _is_installed()
while True:
print("\nMedeia-Macina bootstrap - interactive menu")
if installed:
print("1) Install / Reinstall")
print("2) Uninstall")
print("3) Status")
print("q) Quit")
choice = input("Choose an option: ").strip().lower()
if not choice or choice in ("1", "install", "reinstall"):
return "install"
if choice in ("2", "uninstall"):
return "uninstall"
if choice in ("3", "status"):
print("Installation detected." if installed else "Not installed.")
continue
if choice in ("q", "quit", "exit"):
return 0
else:
print("1) Install")
print("q) Quit")
choice = input("Choose an option: ").strip().lower()
if not choice or choice in ("1", "install"):
return "install"
if choice in ("q", "quit", "exit"):
return 0
except EOFError:
# Non-interactive, fall back to delegating to platform helper
return "delegate"
# If the user passed --uninstall explicitly, perform non-interactive uninstall and exit
if args.uninstall:
return _do_uninstall()
# If invoked without any arguments and not asked to skip delegation, prefer
# the interactive menu when running in a TTY; otherwise delegate to the
# platform-specific bootstrap helper (non-interactive).
if len(sys.argv) == 1 and not args.no_delegate:
if sys.stdin.isatty() and not args.quiet:
sel = _interactive_menu()
if sel == "install":
# user chose to install/reinstall; set defaults and continue
args.skip_deps = False
args.install_editable = True
args.no_playwright = False
elif sel == "uninstall":
return _do_uninstall()
elif sel == "delegate":
rc = run_platform_bootstrap(repo_root)
if rc != 0:
return rc
if not args.quiet:
print("Platform bootstrap completed successfully.")
return 0
else:
return int(sel or 0)
else:
rc = run_platform_bootstrap(repo_root)
if rc != 0:
return rc
if not args.quiet:
print("Platform bootstrap completed successfully.")
return 0
@@ -295,14 +460,17 @@ def main() -> int:
try:
if not venv_dir.exists():
if not args.quiet:
print(f"Creating local virtualenv at: {venv_dir}")
run([sys.executable, "-m", "venv", str(venv_dir)])
else:
if not args.quiet:
print(f"Using existing virtualenv at: {venv_dir}")
py = _venv_python(venv_dir)
if not py.exists():
# Try recreating venv if python is missing
if not args.quiet:
print(f"Local venv python not found at {py}; recreating venv")
run([sys.executable, "-m", "venv", str(venv_dir)])
py = _venv_python(venv_dir)
@@ -315,6 +483,7 @@ def main() -> int:
# Ensure a local venv is present and use it for subsequent installs.
venv_python = _ensure_local_venv()
if not args.quiet:
print(f"Using venv python: {venv_python}")
# Enforce opinionated behavior: install deps, playwright, deno, and install project in editable mode.
@@ -326,9 +495,11 @@ def main() -> int:
try:
if args.playwright_only:
if not playwright_package_installed():
if not args.quiet:
print("'playwright' package not found; installing it via pip...")
run([sys.executable, "-m", "pip", "install", "playwright"])
if not args.quiet:
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
@@ -339,10 +510,12 @@ def main() -> int:
return 2
run(cmd)
if not args.quiet:
print("Playwright browsers installed successfully.")
return 0
if args.upgrade_pip:
if not args.quiet:
print("Upgrading pip, setuptools, and wheel in local venv...")
run(
[
@@ -365,6 +538,7 @@ def main() -> int:
file=sys.stderr,
)
else:
if not args.quiet:
print(
f"Installing Python dependencies into local venv from {req_file}..."
)
@@ -372,9 +546,11 @@ def main() -> int:
if not args.no_playwright:
if not playwright_package_installed():
if not args.quiet:
print("'playwright' package not installed in venv; installing it...")
run([str(venv_python), "-m", "pip", "install", "playwright"])
if not args.quiet:
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
@@ -389,10 +565,12 @@ def main() -> int:
run(cmd)
# Install the project into the local venv (editable mode is the default, opinionated)
if not args.quiet:
print("Installing project into local venv (editable mode)")
run([str(venv_python), "-m", "pip", "install", "-e", "."])
# Verify top-level 'CLI' import and, if missing, attempt to make it available
if not args.quiet:
print("Verifying top-level 'CLI' import in venv...")
try:
rc = subprocess.run(
@@ -477,6 +655,7 @@ def main() -> int:
install_deno_requested = True
if install_deno_requested:
if not args.quiet:
print("Installing Deno runtime (local/system)...")
rc = _install_deno(args.deno_version)
if rc != 0:
@@ -615,6 +794,7 @@ python -m medeia_macina.cli_entry @args
except Exception:
pass
if not args.quiet:
print(f"Installed global launchers to: {user_bin}")
else:
@@ -716,6 +896,7 @@ python -m medeia_macina.cli_entry @args
except Exception:
pass
if not args.quiet:
print(f"Installed global launcher to: {mm_sh}")
except Exception as exc: # pragma: no cover - best effort

View File

@@ -1,34 +1,38 @@
#!/usr/bin/env bash
# Bootstrap script for POSIX (Linux/macOS) to create a Python venv and install the project.
# Usage: scripts/bootstrap.sh [--editable] [--venv <path>] [--python <python>] [--desktop] [--no-install]
set -e
# Ensure script is running under Bash. Some users invoke this script with `sh` (dash)
# which does not support the Bash features used below (e.g., [[ ]], arrays, read -p).
# If not running under Bash, re-exec using a discovered bash binary.
if [ -z "${BASH_VERSION:-}" ]; then
if command -v bash >/dev/null 2>&1; then
echo "This script requires Bash; re-execing as 'bash $0'..."
exec bash "$0" "$@"
else
echo "ERROR: This script requires Bash. Please run with 'bash $0' or install Bash." >&2
exit 2
fi
# Thin POSIX wrapper that delegates to the canonical Python installer
# (scripts/bootstrap.py). Platform bootstraps should prefer calling the
# Python script using --no-delegate and -q/--quiet for quiet/non-interactive mode.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$SCRIPT_DIR/.." && pwd -P)"
# Prefer repo venv python, then system pythons
if [ -x "$REPO/.venv/bin/python" ]; then
PY="$REPO/.venv/bin/python"
elif [ -x "$REPO/.venv/bin/python3" ]; then
PY="$REPO/.venv/bin/python3"
elif command -v python3 >/dev/null 2>&1; then
PY="python3"
elif command -v python >/dev/null 2>&1; then
PY="python"
else
echo "Error: No Python interpreter found; please install Python 3 or create the project's .venv." >&2
exit 1
fi
set -euo pipefail
# Translate -q into --quiet for the Python installer
ARGS=()
for a in "$@"; do
if [ "$a" = "-q" ]; then
ARGS+=("--quiet")
else
ARGS+=("$a")
fi
done
VENV_PATH=".venv"
EDITABLE=false
DESKTOP=false
PYTHON_CMD=""
NOINSTALL=false
FORCE=false
QUIET=false
FIX_URLLIB3=false
# Playwright options
PLAYWRIGHT_BROWSERS="chromium" # comma-separated (chromium,firefox,webkit) or 'all'
NO_PLAYWRIGHT=false
REMOVE_PTH=false
exec "$PY" "$REPO/scripts/bootstrap.py" --no-delegate "${ARGS[@]}"
# Prompt helper: read from the controlling terminal so prompts still work
# when stdout/stderr are redirected or piped (e.g., piping output to sed).
@@ -382,13 +386,8 @@ PY
else
echo "WARNING: Could not import 'medeia_macina.cli_entry' from the venv." >&2
# Check if legacy top-level module is present; if so, inform the user to prefer the packaged entrypoint
if "$VENV_PY" -c 'import importlib; importlib.import_module("medeia_entry")' >/dev/null 2>&1; then
echo "Note: 'medeia_entry' top-level module is present. It's recommended to install the project so 'medeia_macina.cli_entry' is available." >&2
else
echo "Action: Try running: $VENV_PY -m pip install -e . or inspect the venv site-packages to verify the installation." >&2
fi
fi
echo "Verifying environment for known issues (urllib3 compatibility)..."
if ! "$VENV_PY" -c 'from SYS.env_check import check_urllib3_compat; ok,msg = check_urllib3_compat(); print(msg); import sys; sys.exit(0 if ok else 2)'; then

View File

@@ -1,724 +0,0 @@
#!/usr/bin/env python3
"""DEPRECATED: scripts/setup.py
This file has been renamed to `scripts/bootstrap.py` to avoid having multiple
`setup.py` files in the repository. Please use:
python ./scripts/bootstrap.py
This shim remains temporarily for backwards compatibility.
---
Original docstring:
scripts/setup.py
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
import platform
import shutil
import os
import time
def run(cmd: list[str]) -> None:
print(f"> {' '.join(cmd)}")
subprocess.check_call(cmd)
# Helpers to find shell executables and to run the platform-specific
# bootstrap script (scripts/bootstrap.sh or scripts/bootstrap.ps1).
def _find_powershell() -> str | None:
for name in ("pwsh", "powershell"):
p = shutil.which(name)
if p:
return p
return None
def _find_shell() -> str | None:
for name in ("bash", "sh"):
p = shutil.which(name)
if p:
return p
return None
def run_platform_bootstrap(repo_root: Path) -> int:
"""Run the platform bootstrap script in quiet/non-interactive mode if present.
Returns the script exit code (0 on success). If no script is present this is a
no-op and returns 0.
"""
ps1 = repo_root / "scripts" / "bootstrap.ps1"
sh_script = repo_root / "scripts" / "bootstrap.sh"
system = platform.system().lower()
if system == "windows" and ps1.exists():
exe = _find_powershell()
if not exe:
print("PowerShell not found; cannot run bootstrap.ps1", file=sys.stderr)
return 1
cmd = [
exe,
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
str(ps1),
"-Quiet"
]
elif sh_script.exists():
shell = _find_shell()
if not shell:
print("Shell not found; cannot run bootstrap.sh", file=sys.stderr)
return 1
# Use -q (quiet) to skip interactive prompts when supported.
cmd = [shell, str(sh_script), "-q"]
else:
# Nothing to run
return 0
print("Running platform bootstrap script:", " ".join(cmd))
rc = subprocess.run(cmd, cwd=str(repo_root))
if rc.returncode != 0:
print(
f"Bootstrap script failed with exit code {rc.returncode}",
file=sys.stderr
)
return int(rc.returncode or 0)
def playwright_package_installed() -> bool:
try:
import playwright # type: ignore
return True
except Exception:
return False
def _build_playwright_install_cmd(browsers: str | None) -> list[str]:
"""Return the command to install Playwright browsers.
- If browsers is None or empty: default to install Chromium only.
- If browsers contains 'all': install all engines by running 'playwright install' with no extra args.
- Otherwise, validate entries and return a command that installs the named engines.
"""
base = [sys.executable, "-m", "playwright", "install"]
if not browsers:
return base + ["chromium"]
items = [b.strip().lower() for b in browsers.split(",") if b.strip()]
if not items:
return base + ["chromium"]
if "all" in items:
return base
allowed = {"chromium",
"firefox",
"webkit"}
invalid = [b for b in items if b not in allowed]
if invalid:
raise ValueError(
f"invalid browsers specified: {invalid}. Valid choices: chromium, firefox, webkit, or 'all'"
)
return base + items
def _install_deno(version: str | None = None) -> int:
"""Install Deno runtime for the current platform.
Uses the official Deno install scripts:
- Unix/macOS: curl -fsSL https://deno.land/x/install/install.sh | sh [-s <version>]
- Windows: powershell iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno [-Version <version>]
Returns exit code 0 on success, non-zero otherwise.
"""
system = platform.system().lower()
try:
if system == "windows":
# Use official PowerShell installer
if version:
ver = version if version.startswith("v") else f"v{version}"
ps_cmd = f"iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno -Version {ver}"
else:
ps_cmd = "iwr https://deno.land/x/install/install.ps1 -useb | iex"
run(
[
"powershell",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
ps_cmd
]
)
else:
# POSIX: use curl + sh installer
if version:
ver = version if version.startswith("v") else f"v{version}"
cmd = f"curl -fsSL https://deno.land/x/install/install.sh | sh -s {ver}"
else:
cmd = "curl -fsSL https://deno.land/x/install/install.sh | sh"
run(["sh", "-c", cmd])
# Check that 'deno' is now available in PATH
if shutil.which("deno"):
print(f"Deno installed at: {shutil.which('deno')}")
return 0
else:
print(
"Deno installation completed but 'deno' not found in PATH. You may need to add Deno's bin directory to your PATH manually.",
file=sys.stderr
)
return 1
except subprocess.CalledProcessError as exc:
print(f"Deno install failed: {exc}", file=sys.stderr)
return int(exc.returncode or 1)
def main() -> int:
parser = argparse.ArgumentParser(
description="Setup Medios-Macina: install deps and Playwright browsers"
)
parser.add_argument(
"--skip-deps",
action="store_true",
help="Skip installing Python dependencies from requirements.txt"
)
parser.add_argument(
"--no-playwright",
action="store_true",
help="Skip running 'playwright install' (only install packages)"
)
parser.add_argument(
"--playwright-only",
action="store_true",
help="Only run 'playwright install' (skips dependency installation)"
)
parser.add_argument(
"--browsers",
type=str,
default="chromium",
help=
"Comma-separated list of browsers to install: chromium,firefox,webkit or 'all' (default: chromium)"
)
parser.add_argument(
"--install-editable",
action="store_true",
help="Install the project in editable mode (pip install -e .) for running tests"
)
deno_group = parser.add_mutually_exclusive_group()
deno_group.add_argument(
"--install-deno",
action="store_true",
help="Install the Deno runtime (default behavior; kept for explicitness)"
)
deno_group.add_argument(
"--no-deno",
action="store_true",
help="Skip installing Deno runtime (opt out)"
)
parser.add_argument(
"--deno-version",
type=str,
default=None,
help="Specific Deno version to install (e.g., v1.34.3)"
)
parser.add_argument(
"--upgrade-pip",
action="store_true",
help="Upgrade pip/setuptools/wheel before installing requirements"
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
# If invoked without any arguments, prefer to delegate to the platform
# bootstrap script (if present). The bootstrap scripts support a quiet/
# non-interactive mode, which we use so "python ./scripts/setup.py" just
# does the right thing on Windows and *nix without extra flags.
if len(sys.argv) == 1:
rc = run_platform_bootstrap(repo_root)
if rc != 0:
return rc
print("Platform bootstrap completed successfully.")
return 0
if sys.version_info < (3, 8):
print("Warning: Python 3.8+ is recommended.", file=sys.stderr)
# Opinionated: always create or use a local venv at the project root (.venv)
venv_dir = repo_root / ".venv"
def _venv_python(p: Path) -> Path:
if platform.system().lower() == "windows":
return p / "Scripts" / "python.exe"
return p / "bin" / "python"
def _ensure_local_venv() -> Path:
"""Create (if missing) and return the path to the venv's python executable.
This is intentionally opinionated: we keep a venv at `./.venv` in the repo root
and use that for all package operations to keep developer environments reproducible.
"""
try:
if not venv_dir.exists():
print(f"Creating local virtualenv at: {venv_dir}")
run([sys.executable, "-m", "venv", str(venv_dir)])
else:
print(f"Using existing virtualenv at: {venv_dir}")
py = _venv_python(venv_dir)
if not py.exists():
# Try recreating venv if python is missing
print(f"Local venv python not found at {py}; recreating venv")
run([sys.executable, "-m", "venv", str(venv_dir)])
py = _venv_python(venv_dir)
if not py.exists():
raise RuntimeError(f"Unable to locate venv python at {py}")
return py
except subprocess.CalledProcessError as exc:
print(f"Failed to create or prepare local venv: {exc}", file=sys.stderr)
raise
# Ensure a local venv is present and use it for subsequent installs.
venv_python = _ensure_local_venv()
print(f"Using venv python: {venv_python}")
# 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.
args.skip_deps = False
args.install_editable = True
args.no_playwright = False
try:
if args.playwright_only:
if not playwright_package_installed():
print("'playwright' package not found; installing it via pip...")
run([sys.executable, "-m", "pip", "install", "playwright"])
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
try:
cmd = _build_playwright_install_cmd(args.browsers)
except ValueError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 2
run(cmd)
print("Playwright browsers installed successfully.")
return 0
if args.upgrade_pip:
print("Upgrading pip, setuptools, and wheel in local venv...")
run(
[
str(venv_python),
"-m",
"pip",
"install",
"--upgrade",
"pip",
"setuptools",
"wheel"
]
)
if not args.skip_deps:
req_file = repo_root / "requirements.txt"
if not req_file.exists():
print(
f"requirements.txt not found at {req_file}; skipping dependency installation.",
file=sys.stderr
)
else:
print(
f"Installing Python dependencies into local venv from {req_file}..."
)
run([str(venv_python), "-m", "pip", "install", "-r", str(req_file)])
if not args.no_playwright:
if not playwright_package_installed():
print("'playwright' package not installed in venv; installing it...")
run([str(venv_python), "-m", "pip", "install", "playwright"])
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
try:
cmd = _build_playwright_install_cmd(args.browsers)
except ValueError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 2
# Run Playwright install using the venv's python so binaries are available in venv
cmd[0] = str(venv_python)
run(cmd)
# Optional: install the project in editable mode so tests can import the package
# Install the project into the local venv (editable mode is the default, opinionated)
print("Installing project into local venv (editable mode)")
run([str(venv_python), "-m", "pip", "install", "-e", "."])
# Verify top-level 'CLI' import and, if missing, attempt to make it available
print("Verifying top-level 'CLI' import in venv...")
try:
import subprocess as _sub
rc = _sub.run(
[
str(venv_python),
"-c",
"import importlib; importlib.import_module('CLI')"
],
check=False
)
if rc.returncode == 0:
print("OK: top-level 'CLI' is importable in the venv.")
else:
print(
"Top-level 'CLI' not importable; attempting to add repo path to venv site-packages via a .pth file..."
)
cmd = [
str(venv_python),
"-c",
(
"import site, sysconfig\n"
"out=[]\n"
"try:\n out.extend(site.getsitepackages())\nexcept Exception:\n pass\n"
"try:\n p = sysconfig.get_paths().get('purelib')\n if p:\n out.append(p)\nexcept Exception:\n pass\n"
"seen=[]; res=[]\n"
"for x in out:\n if x and x not in seen:\n seen.append(x); res.append(x)\n"
"for s in res:\n print(s)\n"
)
]
out = _sub.check_output(cmd, text=True).strip().splitlines()
site_dir = None
for sp in out:
if sp and Path(sp).exists():
site_dir = Path(sp)
break
if site_dir is None:
print(
"Could not determine venv site-packages directory; skipping .pth fallback"
)
else:
pth_file = site_dir / "medeia_repo.pth"
if pth_file.exists():
txt = pth_file.read_text(encoding="utf-8")
if str(repo_root) in txt:
print(f".pth already contains repo root: {pth_file}")
else:
with pth_file.open("a", encoding="utf-8") as fh:
fh.write(str(repo_root) + "\n")
print(f"Appended repo root to existing .pth: {pth_file}")
else:
with pth_file.open("w", encoding="utf-8") as fh:
fh.write(str(repo_root) + "\n")
print(
f"Wrote .pth adding repo root to venv site-packages: {pth_file}"
)
# Re-check whether CLI can be imported now
rc2 = _sub.run(
[
str(venv_python),
"-c",
"import importlib; importlib.import_module('CLI')"
],
check=False
)
if rc2.returncode == 0:
print("Top-level 'CLI' import works after adding .pth")
else:
print(
"Adding .pth did not make top-level 'CLI' importable; consider creating an egg-link or checking the venv."
)
except Exception as exc:
print(
f"Warning: failed to verify or modify site-packages for top-level CLI: {exc}"
)
# Optional: install Deno runtime (default: install unless --no-deno is passed)
install_deno_requested = True
if getattr(args, "no_deno", False):
install_deno_requested = False
elif getattr(args, "install_deno", False):
install_deno_requested = True
if install_deno_requested:
print("Installing Deno runtime (local/system)...")
rc = _install_deno(args.deno_version)
if rc != 0:
print("Deno installation failed.", file=sys.stderr)
return rc
# Write project-local launcher scripts (project root) that prefer the local .venv
def _write_launchers():
sh = repo_root / "mm"
ps1 = repo_root / "mm.ps1"
bat = repo_root / "mm.bat"
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)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repo = $scriptDir
$venv = Join-Path $repo '.venv'
# Ensure venv Scripts dir is on PATH for provider discovery
$venvScripts = Join-Path $venv 'Scripts'
if (Test-Path $venvScripts) { $env:PATH = $venvScripts + ';' + $env:PATH }
$py = Join-Path $venv 'Scripts\python.exe'
$cli = Join-Path $repo 'CLI.py'
if (Test-Path $py) { & $py -m medeia_macina.cli_entry @args; exit $LASTEXITCODE }
if (Test-Path $cli) { & python $cli @args; exit $LASTEXITCODE }
# fallback
python -m medeia_macina.cli_entry @args
"""
try:
ps1.write_text(ps1_text, encoding="utf-8")
except Exception:
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:
# non-interactive mode, which we use so "python ./scripts/bootstrap.py" just
pass
except Exception:
pass
_write_launchers()
# Install user-global shims so `mm` can be executed from any shell session.
def _install_user_shims(repo: Path) -> None:
try:
home = Path.home()
system = platform.system().lower()
if system == "windows":
user_bin = Path(os.environ.get("USERPROFILE", str(home))) / "bin"
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)
mm_ps1 = user_bin / "mm.ps1"
ps1_text = (
"Param([Parameter(ValueFromRemainingArguments=$true)] $args)\n"
f"$repo = \"{repo}\"\n"
"$venv = Join-Path $repo '.venv'\n"
"$exe = Join-Path $venv 'Scripts\\mm.exe'\n"
"if (Test-Path $exe) { & $exe @args; exit $LASTEXITCODE }\n"
"$py = Join-Path $venv 'Scripts\\python.exe'\n"
"if (Test-Path $py) {\n"
" if ($env:MM_DEBUG) {\n"
" Write-Host \"MM_DEBUG: diagnostics\" -ForegroundColor Yellow\n"
" & $py -c \"import sys,importlib,importlib.util,traceback; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);\"\n"
" }\n"
" & $py -m medeia_macina.cli_entry @args; exit $LASTEXITCODE\n"
"}\n"
"python -m medeia_macina.cli_entry @args\n"
)
if mm_ps1.exists():
bak = mm_ps1.with_suffix(f".bak{int(time.time())}")
mm_ps1.replace(bak)
mm_ps1.write_text(ps1_text, encoding="utf-8")
# Attempt to add user_bin to the user's PATH if it's not present.
try:
cur = os.environ.get("PATH", "")
str_bin = str(user_bin)
if str_bin not in cur:
ps_cmd = (
"$bin = '{bin}';"
"$cur = [Environment]::GetEnvironmentVariable('PATH','User');"
"if ($cur -notlike \"*$bin*\") {[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User')}"
).format(bin=str_bin.replace('\\',
'\\\\'))
subprocess.run(
["powershell",
"-NoProfile",
"-Command",
ps_cmd],
check=False
)
except Exception:
pass
print(f"Installed global launchers to: {user_bin}")
else:
# POSIX
user_bin = Path(
os.environ.get("XDG_BIN_HOME",
str(home / ".local/bin"))
)
user_bin.mkdir(parents=True, exist_ok=True)
mm_sh = user_bin / "mm"
sh_text = (
"#!/usr/bin/env bash\n"
"set -e\n"
f"REPO=\"{repo}\"\n"
"# Prefer git top-level when available to avoid embedding a parent path.\n"
"if command -v git >/dev/null 2>&1; then\n"
" gitroot=$(git -C \"$REPO\" rev-parse --show-toplevel 2>/dev/null || true)\n"
" if [ -n \"$gitroot\" ]; then\n"
" REPO=\"$gitroot\"\n"
" fi\n"
"fi\n"
"# If git not available or didn't resolve, walk up from CWD to find a project root.\n"
"if [ ! -f \"$REPO/CLI.py\" ] && [ ! -f \"$REPO/pyproject.toml\" ]; then\n"
" CUR=\"$(pwd -P)\"\n"
" while [ \"$CUR\" != \"/\" ] && [ \"$CUR\" != \"\" ]; do\n"
" if [ -f \"$CUR/CLI.py\" ] || [ -f \"$CUR/pyproject.toml\" ]; then\n"
" REPO=\"$CUR\"\n"
" break\n"
" fi\n"
" CUR=\"$(dirname \"$CUR\")\"\n"
" done\n"
"fi\n"
"VENV=\"$REPO/.venv\"\n"
"# Debug mode: set MM_DEBUG=1 to print repository, venv, and import diagnostics\n"
"if [ -n \"${MM_DEBUG:-}\" ]; then\n"
" echo \"MM_DEBUG: diagnostics\" >&2\n"
" echo \"Resolved REPO: $REPO\" >&2\n"
" echo \"Resolved VENV: $VENV\" >&2\n"
" echo \"VENV exists: $( [ -d \"$VENV\" ] && echo yes || echo no )\" >&2\n"
" echo \"Candidates:\" >&2\n"
" echo \" VENV/bin/mm: $( [ -x \"$VENV/bin/mm\" ] && echo yes || echo no )\" >&2\n"
" echo \" VENV/bin/python3: $( [ -x \"$VENV/bin/python3\" ] && echo yes || echo no )\" >&2\n"
" echo \" VENV/bin/python: $( [ -x \"$VENV/bin/python\" ] && echo yes || echo no )\" >&2\n"
" echo \" system python3: $(command -v python3 || echo none)\" >&2\n"
" echo \" system python: $(command -v python || echo none)\" >&2\n"
" for pycmd in \"$VENV/bin/python3\" \"$VENV/bin/python\" \"$(command -v python3 2>/dev/null)\" \"$(command -v python 2>/dev/null)\"; do\n"
" if [ -n \"$pycmd\" ] && [ -x \"$pycmd\" ]; then\n"
" echo \"---- Testing with: $pycmd ----\" >&2\n"
" $pycmd - <<'PY'\nimport sys, importlib, traceback, importlib.util\nprint('sys.executable:', sys.executable)\nprint('sys.path (first 8):', sys.path[:8])\nfor mod in ('CLI','medeia_macina','medeia_macina.cli_entry'):\n try:\n spec = importlib.util.find_spec(mod)\n print(mod, 'spec:', spec)\n if spec:\n m = importlib.import_module(mod)\n print(mod, 'loaded at', getattr(m, '__file__', None))\n except Exception:\n print(mod, 'import failed')\n traceback.print_exc()\nPY\n"
" fi\n"
" done\n"
" echo \"MM_DEBUG: end diagnostics\" >&2\n"
"fi\n"
"# Packaged console script in the venv if available\n"
"if [ -x \"$VENV/bin/mm\" ]; then\n"
" exec \"$VENV/bin/mm\" \"$@\"\n"
"fi\n"
"# Prefer venv's python3, then venv's python\n"
"if [ -x \"$VENV/bin/python3\" ]; then\n"
" exec \"$VENV/bin/python3\" -m medeia_macina.cli_entry \"$@\"\n"
"fi\n"
"if [ -x \"$VENV/bin/python\" ]; then\n"
" exec \"$VENV/bin/python\" -m medeia_macina.cli_entry \"$@\"\n"
"fi\n"
"# Fallback to system python3, then system python (only if it's Python 3)\n"
"if command -v python3 >/dev/null 2>&1; then\n"
" exec python3 -m medeia_macina.cli_entry \"$@\"\n"
"fi\n"
"if command -v python >/dev/null 2>&1; then\n"
" if python -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)'; then\n"
" exec python -m medeia_macina.cli_entry \"$@\"\n"
" fi\n"
"fi\n"
"echo 'Error: no suitable Python 3 interpreter found. Please install Python 3 or use the venv.' >&2\n"
"exit 127\n"
)
if mm_sh.exists():
bak = mm_sh.with_suffix(f".bak{int(time.time())}")
mm_sh.replace(bak)
mm_sh.write_text(sh_text, encoding="utf-8")
mm_sh.chmod(mm_sh.stat().st_mode | 0o111)
# Ensure the user's bin is on PATH for future sessions by adding to ~/.profile
cur_path = os.environ.get("PATH", "")
if str(user_bin) not in cur_path:
profile = home / ".profile"
snippet = (
"# Added by Medeia-Macina setup: ensure user local bin is on PATH\n"
"if [ -d \"$HOME/.local/bin\" ] && [[ \":$PATH:\" != *\":$HOME/.local/bin:\"* ]]; then\n"
" PATH=\"$HOME/.local/bin:$PATH\"\n"
"fi\n"
)
try:
txt = profile.read_text() if profile.exists() else ""
if snippet.strip() not in txt:
with profile.open("a", encoding="utf-8") as fh:
fh.write("\n" + snippet)
except Exception:
pass
print(f"Installed global launcher to: {mm_sh}")
except Exception as exc: # pragma: no cover - best effort
print(f"Failed to install global shims: {exc}", file=sys.stderr)
_install_user_shims(repo_root)
print("Setup complete.")
return 0
except subprocess.CalledProcessError as exc:
print(
f"Error: command failed with exit {exc.returncode}: {exc}",
file=sys.stderr
)
return int(exc.returncode or 1)
except Exception as exc: # pragma: no cover - defensive
print(f"Unexpected error: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,42 +0,0 @@
"""
Setup configuration for Medeia-Macina.
Medeia-Macina is a comprehensive media and data management system with support for:
- Video downloading from multiple sources (YouTube, etc.)
- Local and cloud-based file storage
- Advanced metadata and tag management
- Full-featured TUI and CLI interfaces
"""
from setuptools import setup, find_packages
with open("requirements.txt") as f:
requirements = [
line.strip() for line in f if line.strip() and not line.startswith("#")
]
setup(
name="medeia-macina",
version="1.0.0",
description="Comprehensive media and data management system",
author="Anonymous",
python_requires=">=3.9",
packages=find_packages(exclude=["tests",
"*.tests"]),
install_requires=requirements,
entry_points={
"console_scripts": [
"mm=medeia_macina.cli_entry:main",
"medeia=medeia_macina.cli_entry:main",
],
},
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
)