- 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
|
- name: Create Gitea release
|
||||||
id: create
|
id: create
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ gitea.ref_name }}"
|
TAG="${{ gitea.ref_name }}"
|
||||||
VERSION="${TAG#v}"
|
VERSION="${TAG#v}"
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
")
|
")
|
||||||
|
|
||||||
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \
|
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $DEPLOY_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{
|
-d "{
|
||||||
\"tag_name\": \"$TAG\",
|
\"tag_name\": \"$TAG\",
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
" 2>&1) || {
|
" 2>&1) || {
|
||||||
echo "Create failed, fetching existing release for tag $TAG..."
|
echo "Create failed, fetching existing release for tag $TAG..."
|
||||||
RELEASE=$(curl -s "$BASE_URL/releases/tags/$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'])")
|
RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||||
}
|
}
|
||||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||||
@@ -103,19 +103,45 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload assets to release
|
- name: Upload assets to release
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||||
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
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
|
for FILE in build/MediaServer-*.zip build/MediaServer-*-setup.exe; do
|
||||||
[ -f "$FILE" ] || continue
|
[ -f "$FILE" ] || continue
|
||||||
echo "Uploading $(basename "$FILE")..."
|
upload_asset "$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"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- Build Linux tarball ---
|
# --- Build Linux tarball ---
|
||||||
@@ -143,15 +169,35 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload assets to release
|
- name: Upload assets to release
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||||
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
|
|
||||||
FILE=$(ls build/MediaServer-*-linux-x64.tar.gz | head -1)
|
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 \
|
curl -s -X POST \
|
||||||
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$FILE")" \
|
"$BASE_URL/releases/$RELEASE_ID/assets?name=$NAME" \
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $DEPLOY_TOKEN" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
--data-binary "@$FILE"
|
--data-binary "@$FILE"
|
||||||
|
|||||||
@@ -41,10 +41,20 @@ Unregister-ScheduledTask -TaskName "MediaServer" -Confirm:$false
|
|||||||
|
|
||||||
**When restart is NOT needed:**
|
**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
|
- README or documentation updates
|
||||||
- Changes to install/service scripts (only affects new installations)
|
- 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:**
|
**How to restart during development:**
|
||||||
|
|
||||||
1. Find the running server process:
|
1. Find the running server process:
|
||||||
@@ -124,12 +134,17 @@ To add support for a new language:
|
|||||||
|
|
||||||
## Versioning
|
## 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`
|
At runtime, `media_server/__init__.py` reads the version via `importlib.metadata.version()` — no manual syncing needed.
|
||||||
- `media_server/__init__.py` - `__version__`
|
|
||||||
|
|
||||||
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.
|
**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)
|
# Build Linux distribution (self-contained venv + tarball)
|
||||||
# Usage: ./build-dist-linux.sh [VERSION]
|
# Usage: ./build-dist-linux.sh [VERSION]
|
||||||
|
|
||||||
# --- Version detection ---
|
source "$(dirname "$0")/build-common.sh"
|
||||||
VERSION="${1:-}"
|
|
||||||
|
|
||||||
if [ -z "$VERSION" ]; then
|
detect_version "${1:-}"
|
||||||
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}"
|
|
||||||
echo "Building Media Server v${VERSION_CLEAN} for Linux"
|
echo "Building Media Server v${VERSION_CLEAN} for Linux"
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
DIST_DIR="dist/media-server"
|
DIST_DIR="dist/media-server"
|
||||||
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-linux-x64"
|
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-linux-x64"
|
||||||
|
|
||||||
rm -rf dist build
|
clean_dist "${DIST_DIR}" build
|
||||||
mkdir -p "${DIST_DIR}" build
|
verify_frontend
|
||||||
|
|
||||||
# --- 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
|
|
||||||
|
|
||||||
# --- Create self-contained virtualenv ---
|
# --- Create self-contained virtualenv ---
|
||||||
echo "Creating virtualenv..."
|
echo "Creating virtualenv..."
|
||||||
@@ -49,21 +29,11 @@ rm -rf "${DIST_DIR}"/venv/lib/python*/site-packages/media_server*.dist-info
|
|||||||
|
|
||||||
deactivate
|
deactivate
|
||||||
|
|
||||||
# --- Copy application ---
|
# Trim venv site-packages
|
||||||
echo "Copying application files..."
|
LINUX_SP=$(echo "${DIST_DIR}"/venv/lib/python*/site-packages)
|
||||||
mkdir -p "${DIST_DIR}/app"
|
cleanup_site_packages "$LINUX_SP" "so" "so"
|
||||||
cp -r media_server "${DIST_DIR}/app/"
|
|
||||||
|
|
||||||
# Remove source JS (bundle is in dist/)
|
copy_app_files "$DIST_DIR"
|
||||||
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"
|
|
||||||
|
|
||||||
# --- Create launcher ---
|
# --- Create launcher ---
|
||||||
cat > "${DIST_DIR}/media-server.sh" << 'LAUNCHER'
|
cat > "${DIST_DIR}/media-server.sh" << 'LAUNCHER'
|
||||||
|
|||||||
+7
-50
@@ -4,23 +4,9 @@ set -euo pipefail
|
|||||||
# Cross-build Windows distribution on Linux
|
# Cross-build Windows distribution on Linux
|
||||||
# Usage: ./build-dist-windows.sh [VERSION]
|
# Usage: ./build-dist-windows.sh [VERSION]
|
||||||
|
|
||||||
# --- Version detection ---
|
source "$(dirname "$0")/build-common.sh"
|
||||||
VERSION="${1:-}"
|
|
||||||
|
|
||||||
if [ -z "$VERSION" ]; then
|
detect_version "${1:-}"
|
||||||
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}"
|
|
||||||
echo "Building Media Server v${VERSION_CLEAN} for Windows"
|
echo "Building Media Server v${VERSION_CLEAN} for Windows"
|
||||||
|
|
||||||
# --- Configuration ---
|
# --- Configuration ---
|
||||||
@@ -31,8 +17,7 @@ WHEEL_DIR="build/win-wheels"
|
|||||||
SITE_PACKAGES="${DIST_DIR}/python/Lib/site-packages"
|
SITE_PACKAGES="${DIST_DIR}/python/Lib/site-packages"
|
||||||
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-win-x64"
|
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-win-x64"
|
||||||
|
|
||||||
rm -rf dist build
|
clean_dist "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
||||||
mkdir -p "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
|
||||||
|
|
||||||
# --- Download embedded Python ---
|
# --- Download embedded Python ---
|
||||||
echo "Downloading embedded Python ${PYTHON_VERSION}..."
|
echo "Downloading embedded Python ${PYTHON_VERSION}..."
|
||||||
@@ -58,6 +43,7 @@ CORE_DEPS=(
|
|||||||
"pyyaml>=6.0"
|
"pyyaml>=6.0"
|
||||||
"mutagen>=1.47.0"
|
"mutagen>=1.47.0"
|
||||||
"pillow>=10.0.0"
|
"pillow>=10.0.0"
|
||||||
|
"packaging>=23.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Windows-specific dependencies
|
# Windows-specific dependencies
|
||||||
@@ -100,43 +86,14 @@ for whl in "$WHEEL_DIR"/*.whl; do
|
|||||||
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
||||||
done
|
done
|
||||||
|
|
||||||
# --- Size optimization ---
|
cleanup_site_packages "$SITE_PACKAGES" "pyd" "dll"
|
||||||
echo "Optimizing size..."
|
verify_frontend
|
||||||
find "$SITE_PACKAGES" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
copy_app_files "$DIST_DIR"
|
||||||
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}/"
|
|
||||||
|
|
||||||
# Copy scripts needed for auto-start
|
# Copy scripts needed for auto-start
|
||||||
mkdir -p "${DIST_DIR}/scripts"
|
mkdir -p "${DIST_DIR}/scripts"
|
||||||
cp scripts/start-hidden.vbs "${DIST_DIR}/scripts/"
|
cp scripts/start-hidden.vbs "${DIST_DIR}/scripts/"
|
||||||
|
|
||||||
# --- Write version ---
|
|
||||||
echo "$VERSION_CLEAN" > "${DIST_DIR}/VERSION"
|
|
||||||
|
|
||||||
# --- Create launcher ---
|
# --- Create launcher ---
|
||||||
cat > "${DIST_DIR}/media-server.bat" << 'LAUNCHER'
|
cat > "${DIST_DIR}/media-server.bat" << 'LAUNCHER'
|
||||||
@echo off
|
@echo off
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
"""Media Server - REST API for controlling system media playback."""
|
"""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 asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
from .release_provider import ReleaseProvider
|
from .release_provider import ReleaseProvider
|
||||||
from .websocket_manager import ws_manager
|
from .websocket_manager import ws_manager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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] = []
|
cleaned = raw.lstrip("v").strip()
|
||||||
for part in version.split("."):
|
m = _PRE_PATTERN.match(cleaned)
|
||||||
digits = ""
|
if m:
|
||||||
for ch in part:
|
base, pre_label, pre_num = m.group(1), m.group(2).lower(), m.group(3)
|
||||||
if ch.isdigit():
|
cleaned = f"{base}{_PRE_MAP[pre_label]}{pre_num}"
|
||||||
digits += ch
|
return Version(cleaned)
|
||||||
else:
|
|
||||||
break
|
|
||||||
if digits:
|
|
||||||
parts.append(int(digits))
|
|
||||||
return tuple(parts)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateChecker:
|
class UpdateChecker:
|
||||||
|
|||||||
@@ -3537,6 +3537,8 @@ footer .separator {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-banner-close:hover {
|
.update-banner-close:hover {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ dependencies = [
|
|||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"mutagen>=1.47.0",
|
"mutagen>=1.47.0",
|
||||||
"pillow>=10.0.0",
|
"pillow>=10.0.0",
|
||||||
|
"packaging>=23.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user