Versionless installer asset names, stable latest-download README links
release / gui-build (linux, bash) (push) Successful in 1m0s
release / gui-build (windows, cmd) (push) Successful in 1m18s
release / gui-build (macos, bash) (push) Successful in 1m41s

This commit is contained in:
2026-09-23 19:29:02 +00:00
parent 550131d43b
commit 5fc20c5578
3 changed files with 64 additions and 50 deletions
+17 -6
View File
@@ -4,13 +4,24 @@
Netflix style browsing of your local media archive. Supports keyboard, mouse and gamepad navigation. Uses your favorite movie player. 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. ### Linux
- 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`. ```
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 ## 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 - Remembers per-episode playback positions and offers series continue points
- Hand off playback to your preferred system player - 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. 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.
+9 -6
View File
@@ -88,9 +88,12 @@ def _platform() -> _Platform:
return _Platform("linux", "linux", "linux-x64", "MediaHive", "mediahive.png", "MediaHive", ".AppImage") 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/<name> links stay valid."""
p = _platform() 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: 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}") raise RuntimeError(f"vpk produced no *{plat.setup_ext} in {releases_dir}")
if sys.platform == "darwin": if sys.platform == "darwin":
force_macos_user_install(setup) 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) artifact.unlink(missing_ok=True)
setup.rename(artifact) setup.rename(artifact)
rename_feed_package(releases_dir, version, plat.channel) 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}") 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. """Create the Windows portable ZIP of the build/MediaHive folder.
Velopack-less plain-folder distribution for users who cannot or do not 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(): if not dist_folder.exists():
raise FileNotFoundError(f"Distribution folder not found: {dist_folder}") 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}...") print(f"Creating {zip_path}...")
shutil.make_archive( shutil.make_archive(
str(zip_path.with_suffix("")), # removes .zip so make_archive can add it str(zip_path.with_suffix("")), # removes .zip so make_archive can add it
@@ -553,7 +556,7 @@ def main() -> None:
artifacts = [build_velopack(version)] artifacts = [build_velopack(version)]
if sys.platform == "win32": if sys.platform == "win32":
artifacts.append(create_portable_zip(version)) artifacts.append(create_portable_zip())
for artifact_path in artifacts: for artifact_path in artifacts:
print(f"✓ Built successfully: {artifact_path}") print(f"✓ Built successfully: {artifact_path}")
+38 -38
View File
@@ -9,8 +9,9 @@ Reads from [project.urls] Repository in pyproject.toml.
Token: GITEA_TOKEN environment variable Token: GITEA_TOKEN environment variable
Steps: Steps:
1. Find clean-versioned platform artifacts in build/ and matching dist/ wheels/sdists 1. Read the clean tag version via setuptools_scm, find platform artifacts
2. Abort if any dist files are missing for a found artifact version 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 3. Create a Gitea release for each version (or reuse the existing one
for the tag, skipping already-uploaded assets) and upload all assets for the tag, skipping already-uploaded assets) and upload all assets
4. Remind the user to run: uv publish 4. Remind the user to run: uv publish
@@ -28,6 +29,7 @@ from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
import setuptools_scm
REPO_ROOT = Path(__file__).parent.parent REPO_ROOT = Path(__file__).parent.parent
@@ -67,23 +69,27 @@ def load_token() -> str:
# ZIP + dist helpers # ZIP + dist helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Matches MediaHive-1.2.3-win64-portable.zip, MediaHive-1.2.3-win64-setup.exe, # Installer artifacts are versionless (MediaHive-win64-setup.exe,
# MediaHive-1.2.3-macos-setup.pkg, MediaHive-1.2.3-linux-setup.AppImage, etc. # MediaHive-macos-setup.pkg, MediaHive-linux-setup.AppImage,
# Rejects dev/dirty versions like MediaHive-1.2.3.dev0+gabcd-win64-portable.zip # MediaHive-win64-portable.zip) so /releases/download/latest/<name> links
_CLEAN_ARTIFACT_RE = re.compile( # stay valid. The version comes from setuptools_scm instead.
r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.(?:zip|dmg|exe|pkg|AppImage)$" _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]]: def read_version() -> str:
"""Return (path, version, platform_tag) for clean-versioned artifacts in build/.""" """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" build_dir = REPO_ROOT / "build"
results = [] return [p for p in sorted(build_dir.glob("MediaHive-*")) if _ARTIFACT_RE.match(p.name)]
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
def find_dist_files(version: str) -> list[Path]: def find_dist_files(version: str) -> list[Path]:
@@ -244,46 +250,40 @@ def main() -> None:
try: try:
cfg = load_gitea_config() cfg = load_gitea_config()
token = load_token() token = load_token()
version = read_version()
artifacts = find_releasable_artifacts() artifacts = find_releasable_artifacts()
if not artifacts: if not artifacts:
print( print(
"No clean-versioned platform artifacts found in build/.\n" "No platform artifacts found in build/.\n"
"Run scripts/guibuild.py first.", "Run scripts/guibuild.py first.",
file=sys.stderr, file=sys.stderr,
) )
sys.exit(1) sys.exit(1)
# Validate all dist files exist before touching Gitea # Validate all dist files exist before touching Gitea
dist_files: dict[str, list[Path]] = {} dist_files: list[Path] = [] if args.no_dist else find_dist_files(version)
if not args.no_dist:
for _, version, _platform_tag in artifacts:
dist_files[version] = find_dist_files(version)
base_url = cfg["url"].rstrip("/") base_url = cfg["url"].rstrip("/")
repo = cfg["repo"] repo = cfg["repo"]
with httpx.Client(headers=gitea_headers(token)) as client: with httpx.Client(headers=gitea_headers(token)) as client:
releases: dict[str, tuple[int, set[str]]] = {} print(f"\nReleasing {version} ...")
for artifact_path, version, platform_tag in artifacts: tag = f"v{version}"
print(f"\nReleasing {version} ...") release_id, uploaded = create_release(
tag = f"v{version}" client, base_url, repo, tag, version, args.notes, args.draft
if version not in releases: )
releases[version] = create_release( for path in dist_files:
client, base_url, repo, tag, version, args.notes, args.draft if path.name in uploaded:
) print(f"Skipping {path.name}, already on the release.")
release_id, uploaded = releases[version] continue
for path in dist_files.get(version, []): upload_asset(client, base_url, repo, release_id, path)
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: if artifact_path.name in uploaded:
print(f"Skipping {artifact_path.name}, already on the release.") print(f"Skipping {artifact_path.name}, already on the release.")
continue 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) upload_asset(client, base_url, repo, release_id, artifact_path)
uploaded.add(artifact_path.name) uploaded.add(artifact_path.name)
for feed_file in find_velopack_feed_files(): for feed_file in find_velopack_feed_files():
@@ -292,7 +292,7 @@ def main() -> None:
continue continue
upload_asset(client, base_url, repo, release_id, feed_file) upload_asset(client, base_url, repo, release_id, feed_file)
uploaded.add(feed_file.name) uploaded.add(feed_file.name)
print(f"{tag} published") print(f"{tag} published")
print("\nDone. To publish to PyPI, run:") print("\nDone. To publish to PyPI, run:")
print(" uv publish") print(" uv publish")