Versionless installer asset names, stable latest-download README links
This commit is contained in:
@@ -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
@@ -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
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user