- Rename GITEA_TOKEN to DEPLOY_TOKEN in release workflow - Extract shared version detection into build-common.sh - Use importlib.metadata for runtime version instead of hardcoded string - Use PEP 440 parsing (packaging lib) for update version comparison - Add packaging>=23.0 to dependencies - Fix update banner close button alignment (CSS) - Update CLAUDE.md with versioning docs and frontend rebuild notes
This commit is contained in:
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Create Gitea release
|
||||
id: create
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||
run: |
|
||||
TAG="${{ gitea.ref_name }}"
|
||||
VERSION="${TAG#v}"
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
")
|
||||
|
||||
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Authorization: token $DEPLOY_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"tag_name\": \"$TAG\",
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
" 2>&1) || {
|
||||
echo "Create failed, fetching existing release for tag $TAG..."
|
||||
RELEASE=$(curl -s "$BASE_URL/releases/tags/$TAG" \
|
||||
-H "Authorization: token $GITEA_TOKEN")
|
||||
-H "Authorization: token $DEPLOY_TOKEN")
|
||||
RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||
}
|
||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||
@@ -103,19 +103,45 @@ jobs:
|
||||
|
||||
- name: Upload assets to release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||
run: |
|
||||
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||
|
||||
upload_asset() {
|
||||
local file="$1"
|
||||
local name
|
||||
name=$(basename "$file")
|
||||
|
||||
# Delete existing asset with the same name (idempotent re-runs)
|
||||
EXISTING=$(curl -s "$BASE_URL/releases/$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $DEPLOY_TOKEN")
|
||||
ASSET_ID=$(echo "$EXISTING" | python3 -c "
|
||||
import sys, json
|
||||
assets = json.load(sys.stdin)
|
||||
for a in assets:
|
||||
if a['name'] == '$name':
|
||||
print(a['id'])
|
||||
break
|
||||
" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$ASSET_ID" ]; then
|
||||
echo "Replacing existing asset: $name (id=$ASSET_ID)"
|
||||
curl -s -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$ASSET_ID" \
|
||||
-H "Authorization: token $DEPLOY_TOKEN"
|
||||
fi
|
||||
|
||||
echo "Uploading $name..."
|
||||
curl -s -X POST \
|
||||
"$BASE_URL/releases/$RELEASE_ID/assets?name=$name" \
|
||||
-H "Authorization: token $DEPLOY_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$file"
|
||||
}
|
||||
|
||||
for FILE in build/MediaServer-*.zip build/MediaServer-*-setup.exe; do
|
||||
[ -f "$FILE" ] || continue
|
||||
echo "Uploading $(basename "$FILE")..."
|
||||
curl -s -X POST \
|
||||
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$FILE")" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$FILE"
|
||||
upload_asset "$FILE"
|
||||
done
|
||||
|
||||
# --- Build Linux tarball ---
|
||||
@@ -143,15 +169,35 @@ jobs:
|
||||
|
||||
- name: Upload assets to release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||
run: |
|
||||
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||
|
||||
FILE=$(ls build/MediaServer-*-linux-x64.tar.gz | head -1)
|
||||
echo "Uploading $(basename "$FILE")..."
|
||||
NAME=$(basename "$FILE")
|
||||
|
||||
# Delete existing asset with the same name (idempotent re-runs)
|
||||
EXISTING=$(curl -s "$BASE_URL/releases/$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $DEPLOY_TOKEN")
|
||||
ASSET_ID=$(echo "$EXISTING" | python3 -c "
|
||||
import sys, json
|
||||
assets = json.load(sys.stdin)
|
||||
for a in assets:
|
||||
if a['name'] == '$NAME':
|
||||
print(a['id'])
|
||||
break
|
||||
" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$ASSET_ID" ]; then
|
||||
echo "Replacing existing asset: $NAME (id=$ASSET_ID)"
|
||||
curl -s -X DELETE "$BASE_URL/releases/$RELEASE_ID/assets/$ASSET_ID" \
|
||||
-H "Authorization: token $DEPLOY_TOKEN"
|
||||
fi
|
||||
|
||||
echo "Uploading $NAME..."
|
||||
curl -s -X POST \
|
||||
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$FILE")" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"$BASE_URL/releases/$RELEASE_ID/assets?name=$NAME" \
|
||||
-H "Authorization: token $DEPLOY_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$FILE"
|
||||
|
||||
@@ -41,10 +41,20 @@ Unregister-ScheduledTask -TaskName "MediaServer" -Confirm:$false
|
||||
|
||||
**When restart is NOT needed:**
|
||||
|
||||
- Static file changes (`*.html`, `*.css`, `*.js`, `*.json`) - browser refresh is enough
|
||||
- Static file changes (`*.html`, `*.css`, `*.json`) - browser refresh is enough
|
||||
- README or documentation updates
|
||||
- Changes to install/service scripts (only affects new installations)
|
||||
|
||||
### Frontend Rebuild After JS Changes
|
||||
|
||||
**CRITICAL:** The frontend is bundled via esbuild into `static/dist/app.bundle.js`. After modifying ANY JavaScript file in `media_server/static/js/`, you **MUST** run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Raw JS file edits have **NO effect** until the bundle is rebuilt. After rebuilding, a browser hard-refresh (Ctrl+Shift+R) is sufficient — no server restart needed.
|
||||
|
||||
**How to restart during development:**
|
||||
|
||||
1. Find the running server process:
|
||||
@@ -124,12 +134,17 @@ To add support for a new language:
|
||||
|
||||
## Versioning
|
||||
|
||||
Version is tracked in two files that must be kept in sync:
|
||||
**`pyproject.toml`** is the single source of truth for the version string.
|
||||
|
||||
- `pyproject.toml` - `[project].version`
|
||||
- `media_server/__init__.py` - `__version__`
|
||||
At runtime, `media_server/__init__.py` reads the version via `importlib.metadata.version()` — no manual syncing needed.
|
||||
|
||||
When releasing a new version, update both files with the same version string.
|
||||
Version flow:
|
||||
1. `git tag v0.3.0` → CI reads the tag
|
||||
2. Build scripts stamp `pyproject.toml` with the clean version via `sed`
|
||||
3. `pip install` bakes the version into package metadata
|
||||
4. `importlib.metadata.version("media-server")` reads it at runtime
|
||||
|
||||
When bumping the version for a new release, only `pyproject.toml` needs to be updated.
|
||||
|
||||
**Important:** After making any changes, always ask the user if the version needs to be incremented.
|
||||
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-common.sh — shared functions for platform build scripts
|
||||
# Source this file, do not execute directly.
|
||||
|
||||
# --- Version detection ---
|
||||
# Fallback chain: CLI arg → git tag → CI env var → pyproject.toml
|
||||
detect_version() {
|
||||
local arg="${1:-}"
|
||||
VERSION="${arg}"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(git describe --tags --exact-match 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(grep -oP '^version\s*=\s*"\K[^"]+' \
|
||||
pyproject.toml 2>/dev/null || echo "0.0.0")
|
||||
fi
|
||||
|
||||
VERSION_CLEAN="${VERSION#v}"
|
||||
|
||||
# Stamp version into pyproject.toml (single source of truth)
|
||||
sed -i "s/^version = .*/version = \"${VERSION_CLEAN}\"/" pyproject.toml
|
||||
}
|
||||
|
||||
# --- Clean dist/build directories ---
|
||||
clean_dist() {
|
||||
rm -rf dist build
|
||||
mkdir -p "$@"
|
||||
}
|
||||
|
||||
# --- Verify frontend bundle exists ---
|
||||
verify_frontend() {
|
||||
if [ ! -f "media_server/static/dist/app.bundle.js" ]; then
|
||||
echo "ERROR: Frontend bundle not found. Run 'npm ci && npm run build' first."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Copy application files into dist ---
|
||||
# Args: $1 = DIST_DIR
|
||||
copy_app_files() {
|
||||
local dist_dir="$1"
|
||||
|
||||
echo "Copying application files..."
|
||||
mkdir -p "${dist_dir}/app"
|
||||
cp -r media_server "${dist_dir}/app/"
|
||||
|
||||
# Remove source JS (bundle is in dist/)
|
||||
rm -rf "${dist_dir}/app/media_server/static/js"
|
||||
# Remove source maps from release
|
||||
rm -f "${dist_dir}/app/media_server/static/dist/"*.map
|
||||
|
||||
# Copy config example
|
||||
cp config.example.yaml "${dist_dir}/"
|
||||
|
||||
# Write version file
|
||||
echo "$VERSION_CLEAN" > "${dist_dir}/VERSION"
|
||||
}
|
||||
|
||||
# --- Clean up site-packages for smaller distribution ---
|
||||
# Args: $1 = site-packages path, $2 = ext suffix (pyd|so), $3 = lib suffix (dll|so)
|
||||
# Windows: cleanup_site_packages "$SP" "pyd" "dll"
|
||||
# Linux: cleanup_site_packages "$SP" "so" "so"
|
||||
cleanup_site_packages() {
|
||||
local sp_dir="$1"
|
||||
local ext_suffix="${2:-so}"
|
||||
local lib_suffix="${3:-so}"
|
||||
|
||||
echo "Optimizing size..."
|
||||
|
||||
# Generic cleanup
|
||||
find "$sp_dir" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$sp_dir" -type d -name tests -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$sp_dir" -type d -name "*.dist-info" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$sp_dir" -name "*.pyi" -delete 2>/dev/null || true
|
||||
rm -rf "$sp_dir"/{pip,setuptools,pkg_resources,_distutils_hack}* 2>/dev/null || true
|
||||
|
||||
# Trim numpy if present
|
||||
for mod in polynomial linalg ma lib distutils f2py typing _pyinstaller; do
|
||||
rm -rf "$sp_dir/numpy/$mod" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Trim OpenCV if present
|
||||
rm -f "$sp_dir"/cv2/opencv_videoio_ffmpeg*."$lib_suffix" 2>/dev/null || true
|
||||
rm -rf "$sp_dir"/cv2/{data,gapi,misc,utils,typing_stubs,typing} 2>/dev/null || true
|
||||
|
||||
# Trim Pillow unused plugins if present
|
||||
rm -rf "$sp_dir"/PIL/{FpxImagePlugin,MicImagePlugin,McIdasImagePlugin}* 2>/dev/null || true
|
||||
|
||||
# Trim zeroconf service DB if present
|
||||
rm -rf "$sp_dir"/zeroconf/_services 2>/dev/null || true
|
||||
|
||||
# Strip debug symbols from native extensions
|
||||
find "$sp_dir" -name "*.$ext_suffix" -exec strip --strip-debug {} \; 2>/dev/null || true
|
||||
|
||||
# Remove .py source files (keep .pyc only) — saves ~30-40% on pure-Python packages
|
||||
find "$sp_dir" -name "*.py" ! -name "__init__.py" -delete 2>/dev/null || true
|
||||
}
|
||||
+8
-38
@@ -4,37 +4,17 @@ set -euo pipefail
|
||||
# Build Linux distribution (self-contained venv + tarball)
|
||||
# Usage: ./build-dist-linux.sh [VERSION]
|
||||
|
||||
# --- Version detection ---
|
||||
VERSION="${1:-}"
|
||||
source "$(dirname "$0")/build-common.sh"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(git describe --tags --exact-match 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' \
|
||||
media_server/__init__.py 2>/dev/null || echo "0.0.0")
|
||||
fi
|
||||
|
||||
VERSION_CLEAN="${VERSION#v}"
|
||||
detect_version "${1:-}"
|
||||
echo "Building Media Server v${VERSION_CLEAN} for Linux"
|
||||
|
||||
# --- Configuration ---
|
||||
DIST_DIR="dist/media-server"
|
||||
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-linux-x64"
|
||||
|
||||
rm -rf dist build
|
||||
mkdir -p "${DIST_DIR}" build
|
||||
|
||||
# --- Verify frontend bundle ---
|
||||
if [ ! -f "media_server/static/dist/app.bundle.js" ]; then
|
||||
echo "ERROR: Frontend bundle not found. Run 'npm ci && npm run build' first."
|
||||
exit 1
|
||||
fi
|
||||
clean_dist "${DIST_DIR}" build
|
||||
verify_frontend
|
||||
|
||||
# --- Create self-contained virtualenv ---
|
||||
echo "Creating virtualenv..."
|
||||
@@ -49,21 +29,11 @@ rm -rf "${DIST_DIR}"/venv/lib/python*/site-packages/media_server*.dist-info
|
||||
|
||||
deactivate
|
||||
|
||||
# --- Copy application ---
|
||||
echo "Copying application files..."
|
||||
mkdir -p "${DIST_DIR}/app"
|
||||
cp -r media_server "${DIST_DIR}/app/"
|
||||
# Trim venv site-packages
|
||||
LINUX_SP=$(echo "${DIST_DIR}"/venv/lib/python*/site-packages)
|
||||
cleanup_site_packages "$LINUX_SP" "so" "so"
|
||||
|
||||
# Remove source JS (bundle is in dist/)
|
||||
rm -rf "${DIST_DIR}/app/media_server/static/js"
|
||||
# Remove source maps from release
|
||||
rm -f "${DIST_DIR}/app/media_server/static/dist/"*.map
|
||||
|
||||
# Copy config example
|
||||
cp config.example.yaml "${DIST_DIR}/"
|
||||
|
||||
# --- Write version ---
|
||||
echo "$VERSION_CLEAN" > "${DIST_DIR}/VERSION"
|
||||
copy_app_files "$DIST_DIR"
|
||||
|
||||
# --- Create launcher ---
|
||||
cat > "${DIST_DIR}/media-server.sh" << 'LAUNCHER'
|
||||
|
||||
+7
-50
@@ -4,23 +4,9 @@ set -euo pipefail
|
||||
# Cross-build Windows distribution on Linux
|
||||
# Usage: ./build-dist-windows.sh [VERSION]
|
||||
|
||||
# --- Version detection ---
|
||||
VERSION="${1:-}"
|
||||
source "$(dirname "$0")/build-common.sh"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(git describe --tags --exact-match 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' \
|
||||
media_server/__init__.py 2>/dev/null || echo "0.0.0")
|
||||
fi
|
||||
|
||||
VERSION_CLEAN="${VERSION#v}"
|
||||
detect_version "${1:-}"
|
||||
echo "Building Media Server v${VERSION_CLEAN} for Windows"
|
||||
|
||||
# --- Configuration ---
|
||||
@@ -31,8 +17,7 @@ WHEEL_DIR="build/win-wheels"
|
||||
SITE_PACKAGES="${DIST_DIR}/python/Lib/site-packages"
|
||||
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-win-x64"
|
||||
|
||||
rm -rf dist build
|
||||
mkdir -p "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
||||
clean_dist "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
||||
|
||||
# --- Download embedded Python ---
|
||||
echo "Downloading embedded Python ${PYTHON_VERSION}..."
|
||||
@@ -58,6 +43,7 @@ CORE_DEPS=(
|
||||
"pyyaml>=6.0"
|
||||
"mutagen>=1.47.0"
|
||||
"pillow>=10.0.0"
|
||||
"packaging>=23.0"
|
||||
)
|
||||
|
||||
# Windows-specific dependencies
|
||||
@@ -100,43 +86,14 @@ for whl in "$WHEEL_DIR"/*.whl; do
|
||||
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
||||
done
|
||||
|
||||
# --- Size optimization ---
|
||||
echo "Optimizing size..."
|
||||
find "$SITE_PACKAGES" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$SITE_PACKAGES" -type d -name tests -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$SITE_PACKAGES" -type d -name "*.dist-info" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "$SITE_PACKAGES" -name "*.pyi" -delete 2>/dev/null || true
|
||||
rm -rf "$SITE_PACKAGES"/{pip,setuptools,pkg_resources}* 2>/dev/null || true
|
||||
|
||||
# Trim numpy if present
|
||||
rm -rf "$SITE_PACKAGES"/numpy/{tests,f2py,typing} 2>/dev/null || true
|
||||
|
||||
# --- Verify frontend bundle ---
|
||||
if [ ! -f "media_server/static/dist/app.bundle.js" ]; then
|
||||
echo "ERROR: Frontend bundle not found. Run 'npm ci && npm run build' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Copy application ---
|
||||
echo "Copying application files..."
|
||||
mkdir -p "${DIST_DIR}/app"
|
||||
cp -r media_server "${DIST_DIR}/app/"
|
||||
|
||||
# Remove source JS (bundle is in dist/)
|
||||
rm -rf "${DIST_DIR}/app/media_server/static/js"
|
||||
# Remove source maps from release
|
||||
rm -f "${DIST_DIR}/app/media_server/static/dist/"*.map
|
||||
|
||||
# Copy config example
|
||||
cp config.example.yaml "${DIST_DIR}/"
|
||||
cleanup_site_packages "$SITE_PACKAGES" "pyd" "dll"
|
||||
verify_frontend
|
||||
copy_app_files "$DIST_DIR"
|
||||
|
||||
# Copy scripts needed for auto-start
|
||||
mkdir -p "${DIST_DIR}/scripts"
|
||||
cp scripts/start-hidden.vbs "${DIST_DIR}/scripts/"
|
||||
|
||||
# --- Write version ---
|
||||
echo "$VERSION_CLEAN" > "${DIST_DIR}/VERSION"
|
||||
|
||||
# --- Create launcher ---
|
||||
cat > "${DIST_DIR}/media-server.bat" << 'LAUNCHER'
|
||||
@echo off
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
"""Media Server - REST API for controlling system media playback."""
|
||||
|
||||
__version__ = "1.0.1"
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _detect_version() -> str:
|
||||
# 1. Package metadata (works when pip-installed in dev)
|
||||
try:
|
||||
return version("media-server")
|
||||
except PackageNotFoundError:
|
||||
pass
|
||||
|
||||
# 2. VERSION file written by build scripts (production builds)
|
||||
# Located at install root, two levels up from this package
|
||||
version_file = Path(__file__).resolve().parent.parent.parent / "VERSION"
|
||||
if version_file.is_file():
|
||||
return version_file.read_text().strip()
|
||||
|
||||
return "0.0.0-dev"
|
||||
|
||||
|
||||
__version__ = _detect_version()
|
||||
|
||||
@@ -2,30 +2,36 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
from .release_provider import ReleaseProvider
|
||||
from .websocket_manager import ws_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PRE_PATTERN = re.compile(
|
||||
r"^(\d+\.\d+\.\d+)[-.]?(alpha|beta|rc)[.-]?(\d+)$", re.IGNORECASE
|
||||
)
|
||||
_PRE_MAP = {"alpha": "a", "beta": "b", "rc": "rc"}
|
||||
|
||||
def _parse_version(version: str) -> tuple[int, ...]:
|
||||
"""Parse a version string into a comparable tuple.
|
||||
|
||||
Handles versions like "1.0.0", "1.2.3", ignoring non-numeric suffixes.
|
||||
def _parse_version(raw: str) -> Version:
|
||||
"""Normalize a version tag to PEP 440 for correct comparison.
|
||||
|
||||
Examples:
|
||||
v0.3.0-alpha.1 → 0.3.0a1 (pre-release, sorts below 0.3.0)
|
||||
v0.3.0-rc.3 → 0.3.0rc3
|
||||
v1.0.0 → 1.0.0
|
||||
"""
|
||||
parts: list[int] = []
|
||||
for part in version.split("."):
|
||||
digits = ""
|
||||
for ch in part:
|
||||
if ch.isdigit():
|
||||
digits += ch
|
||||
else:
|
||||
break
|
||||
if digits:
|
||||
parts.append(int(digits))
|
||||
return tuple(parts)
|
||||
cleaned = raw.lstrip("v").strip()
|
||||
m = _PRE_PATTERN.match(cleaned)
|
||||
if m:
|
||||
base, pre_label, pre_num = m.group(1), m.group(2).lower(), m.group(3)
|
||||
cleaned = f"{base}{_PRE_MAP[pre_label]}{pre_num}"
|
||||
return Version(cleaned)
|
||||
|
||||
|
||||
class UpdateChecker:
|
||||
|
||||
@@ -3537,6 +3537,8 @@ footer .separator {
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.update-banner-close:hover {
|
||||
|
||||
@@ -32,6 +32,7 @@ dependencies = [
|
||||
"pyyaml>=6.0",
|
||||
"mutagen>=1.47.0",
|
||||
"pillow>=10.0.0",
|
||||
"packaging>=23.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user