From 5fc20c5578a540ba34b9786252953920c8cf8085 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 23 Sep 2026 19:09:09 +0000 Subject: [PATCH] Versionless installer asset names, stable latest-download README links --- README.md | 23 ++++++++++---- scripts/guibuild.py | 15 +++++---- scripts/release.py | 76 ++++++++++++++++++++++----------------------- 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 062c21b..7f62b89 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,24 @@ Netflix style browsing of your local media archive. Supports keyboard, mouse and gamepad navigation. Uses your favorite movie player. -**[Windows, Mac and Linux downloads](https://git.zi.fi/LeoVasanko/mediahive/releases)** +## Downloads -## Getting Started +- **Windows**: [Installer](https://git.zi.fi/LeoVasanko/mediahive/releases/download/latest/MediaHive-win64-setup.exe) ยท [Portable ZIP](https://git.zi.fi/LeoVasanko/mediahive/releases/download/latest/MediaHive-win64-portable.zip) +- **macOS**: [Installer](https://git.zi.fi/LeoVasanko/mediahive/releases/download/latest/MediaHive-macos.pkg) +- **Linux**: [AppImage](https://git.zi.fi/LeoVasanko/mediahive/releases/download/latest/MediaHive-linux.AppImage) -- Windows: Download `*-win64-setup.exe` from the releases page and run it (no admin needed; auto-updates included). A `-win64-portable.zip` is also available. -- macOS: Download `*-macos-setup.pkg` and install (auto-updates included). -- Linux: Download the `.AppImage`, `chmod +x` it, and run. Alternatively install [UV](https://docs.astral.sh/uv/getting-started/installation/) and run directly with `uvx --from mediahive[gui] mediahive`. +### Linux + +``` +wget https://git.zi.fi/LeoVasanko/mediahive/releases/download/latest/MediaHive-linux.AppImage +chmod +x MediaHive-linux.AppImage && ./MediaHive-linux.AppImage +``` + +You may also run without installing via + +``` +uvx --from mediahive[gui] mediahive +``` ## What It Does @@ -20,7 +31,7 @@ Netflix style browsing of your local media archive. Supports keyboard, mouse and - Remembers per-episode playback positions and offers series continue points - Hand off playback to your preferred system player -Windows and macOS builds are currently portable-only (no installer). On first startup, the app asks for your media folder, which you can later change using the in-app folder icon. +On first startup, the app asks for your media folder, which you can later change using the in-app folder icon. Note that `.mediahive` folder is created in your media folder to hold all the metadata and preview clips, avoiding the lengthy processing that you will see on initial startup. diff --git a/scripts/guibuild.py b/scripts/guibuild.py index 1474220..9166d87 100755 --- a/scripts/guibuild.py +++ b/scripts/guibuild.py @@ -88,9 +88,12 @@ def _platform() -> _Platform: return _Platform("linux", "linux", "linux-x64", "MediaHive", "mediahive.png", "MediaHive", ".AppImage") -def setup_artifact_name(version: str) -> str: +def setup_artifact_name() -> str: + """Versionless name so releases/download/latest/ links stay valid.""" p = _platform() - return f"MediaHive-{version}-{p.tag}-setup{p.setup_ext}" + # Windows keeps the -setup suffix: a bare .exe isn't self-explanatory. + suffix = "-setup" if sys.platform == "win32" else "" + return f"MediaHive-{p.tag}{suffix}{p.setup_ext}" def fetch_ffmpeg() -> Path: @@ -381,7 +384,7 @@ def build_velopack(version: str) -> Path: raise RuntimeError(f"vpk produced no *{plat.setup_ext} in {releases_dir}") if sys.platform == "darwin": force_macos_user_install(setup) - artifact = _REPO_ROOT / "build" / setup_artifact_name(version) + artifact = _REPO_ROOT / "build" / setup_artifact_name() artifact.unlink(missing_ok=True) setup.rename(artifact) rename_feed_package(releases_dir, version, plat.channel) @@ -509,7 +512,7 @@ def build_executable() -> None: raise RuntimeError(f"PyInstaller failed with exit code {result.returncode}") -def create_portable_zip(version: str) -> Path: +def create_portable_zip() -> Path: """Create the Windows portable ZIP of the build/MediaHive folder. Velopack-less plain-folder distribution for users who cannot or do not @@ -520,7 +523,7 @@ def create_portable_zip(version: str) -> Path: if not dist_folder.exists(): raise FileNotFoundError(f"Distribution folder not found: {dist_folder}") - zip_path = _REPO_ROOT / "build" / f"MediaHive-{version}-win64-portable.zip" + zip_path = _REPO_ROOT / "build" / "MediaHive-win64-portable.zip" print(f"Creating {zip_path}...") shutil.make_archive( str(zip_path.with_suffix("")), # removes .zip so make_archive can add it @@ -553,7 +556,7 @@ def main() -> None: artifacts = [build_velopack(version)] if sys.platform == "win32": - artifacts.append(create_portable_zip(version)) + artifacts.append(create_portable_zip()) for artifact_path in artifacts: print(f"โœ“ Built successfully: {artifact_path}") diff --git a/scripts/release.py b/scripts/release.py index bf4496a..be3d38f 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -9,8 +9,9 @@ Reads from [project.urls] Repository in pyproject.toml. Token: GITEA_TOKEN environment variable Steps: - 1. Find clean-versioned platform artifacts in build/ and matching dist/ wheels/sdists - 2. Abort if any dist files are missing for a found artifact version + 1. Read the clean tag version via setuptools_scm, find platform artifacts + in build/ and matching dist/ wheels/sdists + 2. Abort if any dist files are missing 3. Create a Gitea release for each version (or reuse the existing one for the tag, skipping already-uploaded assets) and upload all assets 4. Remind the user to run: uv publish @@ -28,6 +29,7 @@ from pathlib import Path from urllib.parse import urlparse import httpx +import setuptools_scm REPO_ROOT = Path(__file__).parent.parent @@ -67,23 +69,27 @@ def load_token() -> str: # ZIP + dist helpers # --------------------------------------------------------------------------- -# Matches MediaHive-1.2.3-win64-portable.zip, MediaHive-1.2.3-win64-setup.exe, -# MediaHive-1.2.3-macos-setup.pkg, MediaHive-1.2.3-linux-setup.AppImage, etc. -# Rejects dev/dirty versions like MediaHive-1.2.3.dev0+gabcd-win64-portable.zip -_CLEAN_ARTIFACT_RE = re.compile( - r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.(?:zip|dmg|exe|pkg|AppImage)$" -) +# Installer artifacts are versionless (MediaHive-win64-setup.exe, +# MediaHive-macos-setup.pkg, MediaHive-linux-setup.AppImage, +# MediaHive-win64-portable.zip) so /releases/download/latest/ links +# stay valid. The version comes from setuptools_scm instead. +_ARTIFACT_RE = re.compile(r"^MediaHive-(?!\d)[A-Za-z0-9._-]+\.(?:zip|dmg|exe|pkg|AppImage)$") -def find_releasable_artifacts() -> list[tuple[Path, str, str]]: - """Return (path, version, platform_tag) for clean-versioned artifacts in build/.""" +def read_version() -> str: + """Read version via setuptools_scm, refusing dev/dirty versions.""" + version = setuptools_scm.get_version(root=str(REPO_ROOT)) + if not re.fullmatch(r"\d+(?:\.\d+)*", version): + raise RuntimeError( + f"Refusing to release non-clean version {version!r}. Tag a release first." + ) + return version + + +def find_releasable_artifacts() -> list[Path]: + """Return platform artifact paths in build/.""" build_dir = REPO_ROOT / "build" - results = [] - for p in sorted(build_dir.glob("MediaHive-*")): - m = _CLEAN_ARTIFACT_RE.match(p.name) - if m: - results.append((p, m.group(1), m.group(2))) - return results + return [p for p in sorted(build_dir.glob("MediaHive-*")) if _ARTIFACT_RE.match(p.name)] def find_dist_files(version: str) -> list[Path]: @@ -244,46 +250,40 @@ def main() -> None: try: cfg = load_gitea_config() token = load_token() + version = read_version() artifacts = find_releasable_artifacts() if not artifacts: print( - "No clean-versioned platform artifacts found in build/.\n" + "No platform artifacts found in build/.\n" "Run scripts/guibuild.py first.", file=sys.stderr, ) sys.exit(1) # Validate all dist files exist before touching Gitea - dist_files: dict[str, list[Path]] = {} - if not args.no_dist: - for _, version, _platform_tag in artifacts: - dist_files[version] = find_dist_files(version) + dist_files: list[Path] = [] if args.no_dist else find_dist_files(version) base_url = cfg["url"].rstrip("/") repo = cfg["repo"] with httpx.Client(headers=gitea_headers(token)) as client: - releases: dict[str, tuple[int, set[str]]] = {} - for artifact_path, version, platform_tag in artifacts: - print(f"\nReleasing {version} ...") - tag = f"v{version}" - if version not in releases: - releases[version] = create_release( - client, base_url, repo, tag, version, args.notes, args.draft - ) - release_id, uploaded = releases[version] - for path in dist_files.get(version, []): - if path.name in uploaded: - print(f"Skipping {path.name}, already on the release.") - continue - upload_asset(client, base_url, repo, release_id, path) + print(f"\nReleasing {version} ...") + tag = f"v{version}" + release_id, uploaded = create_release( + client, base_url, repo, tag, version, args.notes, args.draft + ) + for path in dist_files: + if path.name in uploaded: + print(f"Skipping {path.name}, already on the release.") + continue + upload_asset(client, base_url, repo, release_id, path) - release_id, uploaded = releases[version] + for artifact_path in artifacts: if artifact_path.name in uploaded: print(f"Skipping {artifact_path.name}, already on the release.") continue - print(f"Uploading platform artifact: {platform_tag}") + print(f"Uploading platform artifact: {artifact_path.name}") upload_asset(client, base_url, repo, release_id, artifact_path) uploaded.add(artifact_path.name) for feed_file in find_velopack_feed_files(): @@ -292,7 +292,7 @@ def main() -> None: continue upload_asset(client, base_url, repo, release_id, feed_file) uploaded.add(feed_file.name) - print(f" โœ“ {tag} published") + print(f" โœ“ {tag} published") print("\nDone. To publish to PyPI, run:") print(" uv publish")