Compare commits

...

12 Commits

Author SHA1 Message Date
alexei.dolgolyov c3cb7a4da9 fix(dist): stop stripping .py sources; wipe payload on NSIS upgrade
Release / create-release (push) Successful in 5s
Lint & Test / test (push) Successful in 11s
Release / build-linux (push) Successful in 33s
Release / build-windows (push) Successful in 57s
Two root causes for the 'imaging extension was built for another version
of Pillow' error users hit after install:

1) cleanup_site_packages ran 'find ... -name "*.py" ! -name "__init__.py"
   -delete' with a comment claiming 'keep .pyc only' — but no compileall
   step exists. Result: the dist shipped __init__.py + .pyd only, missing
   every submodule (Image.py, ImageDraw.py, _version.py, ...). Fresh
   installs were broken; in-place upgrades produced a half-old/half-new
   site-packages. Removed the deletion entirely.

2) NSIS installer extracted over the previous install without cleaning
   python/, app/, scripts/. Upgrades left stale files (old PIL/_version.py
   next to new PIL/_imaging.pyd) which raised the Pillow ABI mismatch.
   Wipe those subtrees before File /r, preserving config.yaml at the
   install root.
2026-04-07 22:57:26 +03:00
alexei.dolgolyov e3889fef29 chore: release v0.1.4
Lint & Test / test (push) Successful in 10s
Release / create-release (push) Successful in 3s
Release / build-windows (push) Successful in 56s
Release / build-linux (push) Successful in 31s
2026-04-07 22:43:05 +03:00
alexei.dolgolyov 84500401e7 fix(ci): move pystray to VIS_DEPS so its Pillow resolves with core
Release / create-release (push) Successful in 3s
Lint & Test / test (push) Successful in 10s
Release / build-linux (push) Successful in 32s
Release / build-windows (push) Successful in 1m7s
pystray in WIN_DEPS (per-dep loop) downloaded its own Pillow version,
which overwrote the one resolved alongside CORE_DEPS during unzip.
Result at runtime: '_imaging extension was built for another version
of Pillow'.

Move pystray into VIS_DEPS so it's resolved in the single cross-deps
pip-download call and shares one consistent Pillow version.
2026-04-07 22:35:24 +03:00
alexei.dolgolyov 28293c6340 fix(ci): replace uvicorn[standard] with explicit extras for cross-build
Release / create-release (push) Successful in 3s
Lint & Test / test (push) Successful in 10s
Release / build-linux (push) Successful in 42s
Release / build-windows (push) Successful in 1m9s
uvicorn[standard] pulls uvloop via a 'sys_platform != win32' marker.
pip evaluates env markers against the HOST (Linux in CI), so uvloop
is requested even in a --platform win_amd64 resolve. No uvloop wheel
exists for Windows, so pip backtracks across every uvicorn[standard]
version and fails with ResolutionImpossible.

Use plain uvicorn plus the Windows-compatible extras we actually need
(httptools, websockets, python-dotenv).
2026-04-07 22:29:29 +03:00
alexei.dolgolyov 39b3aed5f3 fix(ci): hybrid pip download - single call for cross-platform deps
Release / create-release (push) Successful in 3s
Lint & Test / test (push) Successful in 10s
Release / build-windows (push) Failing after 31s
Release / build-linux (push) Successful in 47s
The per-dep loop regressed pydantic/pydantic-core compatibility:
each dep resolves transitive versions independently, so 'pydantic'
brings core 2.41.5 while 'pydantic-settings' brings core 2.45.0,
and the later wheel overwrites the earlier during site-packages
unzip, producing:
  SystemError: pydantic-core 2.45.0 is incompatible with
  pydantic, which requires 2.41.5

Fix: single pip-download call for CORE_DEPS + VIS_DEPS so pip
resolves compatible transitive versions. Keep the per-dep loop
with --pre only for WIN_DEPS, where each dep needs its own
platform/non-platform fallback and winsdk requires --pre for
its beta wheels.
2026-04-07 22:24:57 +03:00
alexei.dolgolyov ba90dffa18 fix(ci): revert to per-dep pip download loop with --pre
Release / create-release (push) Successful in 4s
Lint & Test / test (push) Successful in 10s
Release / build-linux (push) Successful in 40s
Release / build-windows (push) Successful in 1m17s
Single pip-download call fails because the second fallback branch
(without --platform) tries to resolve Windows-only deps like winsdk
on Linux, where no wheels exist. The original per-dep loop isolates
each failure so the platform-specific branch handles each dep
independently. Add --pre throughout for winsdk (1.0.0bNN beta).
2026-04-07 19:39:38 +03:00
alexei.dolgolyov 69df9b6b95 fix(ci): normalize non-PEP440 versions before stamping pyproject.toml
Lint & Test / test (push) Successful in 16s
If a tag or CI ref is not PEP 440 compliant (e.g. 'dev', 'nightly',
'snapshot-2024'), the previous detect_version stamped it raw into
pyproject.toml, which then broke 'pip install' with:
  configuration error: project.version must be pep440

Add a regex check after stripping the leading 'v'. If the result
is not PEP 440, substitute '0.0.0.dev0' and warn.

Pattern from ClaudeCodeFacts/gitea-python-ci-cd.md §3.
2026-04-07 19:38:15 +03:00
alexei.dolgolyov 760c3df90c fix(ci): pass --pre to pip download for winsdk beta wheels
Release / create-release (push) Successful in 24s
Lint & Test / test (push) Successful in 30s
Release / build-linux (push) Successful in 38s
Release / build-windows (push) Failing after 43s
The single pip-download call regressed winsdk fetching because pip
won't pick up pre-releases (1.0.0bNN) without --pre. The old per-dep
loop hid this via its fallback branch. Add --pre to both branches.
2026-04-07 19:36:49 +03:00
alexei.dolgolyov 60f287bb40 ci: revert action caching, gitea cache backend not configured
Lint & Test / test (push) Has been cancelled
Release / create-release (push) Successful in 36s
Release / build-windows (push) Failing after 1m59s
Release / build-linux (push) Successful in 2m48s
setup-node and actions/cache@v4 hang trying to talk to a missing
cache server, adding 1-3min per step. Drop the cache: directives
and explicit cache blocks. Keep the single pip-download call in
build-dist-windows.sh which is independent of any cache backend.
2026-04-07 19:30:39 +03:00
alexei.dolgolyov f52af51a20 ci: cache pip wheels, npm deps, and embedded Python in release workflow
Lint & Test / test (push) Successful in 18s
Release / create-release (push) Successful in 13s
Release / build-windows (push) Has been cancelled
Release / build-linux (push) Has been cancelled
- Add pip and npm caching to build-windows and build-linux jobs
- Cache embedded Python zip and Windows wheels across runs
- Collapse per-dep pip download loop into a single resolver call

First run after this lands populates the caches; subsequent
release builds should drop from ~11min to ~3-5min.
2026-04-07 19:19:46 +03:00
alexei.dolgolyov f2d569a1b0 chore: release v0.1.3
Release / create-release (push) Successful in 4s
Release / build-windows (push) Has been cancelled
Release / build-linux (push) Has been cancelled
Lint & Test / test (push) Has been cancelled
2026-04-07 19:04:03 +03:00
alexei.dolgolyov db777fa64b fix: prevent dialog showModal from auto-focusing first input
Lint & Test / test (push) Successful in 1m18s
Patches HTMLDialogElement.prototype.showModal globally to move focus
onto the dialog element itself instead of the first focusable
descendant. On touch devices the previous behavior popped up the
on-screen keyboard whenever a modal opened, which was confusing.
2026-04-07 19:01:42 +03:00
7 changed files with 113 additions and 23 deletions
+23 -9
View File
@@ -1,11 +1,19 @@
## v0.1.2 (2026-03-29)
### Features
- Redesign media browser UI ([cad6e8a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/cad6e8a))
- Add media folder management from WebUI ([c9ee41a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c9ee41a))
## v0.1.4 (2026-04-07)
### Bug Fixes
- Make folder status visible with dot + text label ([c50a8f4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c50a8f4))
- Prevent dialog `showModal` from auto-focusing first input ([db777fa](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/db777fa))
---
### Development / Internal
#### CI/Build
- Replace `uvicorn[standard]` with explicit Windows-safe extras; avoids uvloop cross-build deadlock ([28293c6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/28293c6))
- Move `pystray` into the unified cross-deps resolve so its Pillow matches the core one ([8450040](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/8450040))
- Hybrid pip-download: single call for cross-platform deps (consistent `pydantic-core`), per-dep loop with `--pre` for Windows-only ([39b3aed](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/39b3aed))
- Normalize non-PEP440 versions before stamping `pyproject.toml` ([69df9b6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/69df9b6))
- Pass `--pre` to `pip download` for winsdk beta wheels ([760c3df](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/760c3df))
- Revert broken action caching (Gitea cache backend not configured) ([60f287b](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/60f287b))
---
@@ -14,8 +22,14 @@
| Hash | Message | Author |
|------|---------|--------|
| [c50a8f4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c50a8f4) | fix: make folder status visible with dot + text label | alexei.dolgolyov |
| [cad6e8a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/cad6e8a) | feat: redesign media browser UI | alexei.dolgolyov |
| [c9ee41a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c9ee41a) | feat: add media folder management from WebUI | alexei.dolgolyov |
| [8450040](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/8450040) | fix(ci): move pystray to VIS_DEPS so its Pillow resolves with core | alexei.dolgolyov |
| [28293c6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/28293c6) | fix(ci): replace uvicorn[standard] with explicit extras for cross-build | alexei.dolgolyov |
| [39b3aed](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/39b3aed) | fix(ci): hybrid pip download - single call for cross-platform deps | alexei.dolgolyov |
| [ba90dff](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ba90dff) | fix(ci): revert to per-dep pip download loop with --pre | alexei.dolgolyov |
| [69df9b6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/69df9b6) | fix(ci): normalize non-PEP440 versions before stamping pyproject.toml | alexei.dolgolyov |
| [760c3df](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/760c3df) | fix(ci): pass --pre to pip download for winsdk beta wheels | alexei.dolgolyov |
| [60f287b](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/60f287b) | ci: revert action caching, gitea cache backend not configured | alexei.dolgolyov |
| [f52af51](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/f52af51) | ci: cache pip wheels, npm deps, and embedded Python in release workflow | alexei.dolgolyov |
| [db777fa](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/db777fa) | fix: prevent dialog showModal from auto-focusing first input | alexei.dolgolyov |
</details>
+20 -2
View File
@@ -23,6 +23,17 @@ detect_version() {
VERSION_CLEAN="${VERSION#v}"
# Normalize non-PEP440 labels (e.g. "dev", "nightly", "snapshot") to a
# valid PEP440 dev release. Without this, pip/setuptools rejects
# pyproject.toml with: `project.version` must be pep440.
#
# Valid forms: 1.2.3, 1.2.3a1, 1.2.3rc2, 1.2.3.dev0, 1.2.3.post1, +local
# Invalid forms: dev, vdev, nightly, snapshot-2024
if ! [[ "$VERSION_CLEAN" =~ ^[0-9]+(\.[0-9]+)*((a|b|rc|\.dev|\.post)[0-9]+)*(\+[a-zA-Z0-9.]+)?$ ]]; then
echo " Warning: '$VERSION_CLEAN' is not PEP440-compliant, using 0.0.0.dev0"
VERSION_CLEAN="0.0.0.dev0"
fi
# Stamp version into pyproject.toml (single source of truth)
sed -i "s/^version = .*/version = \"${VERSION_CLEAN}\"/" pyproject.toml
}
@@ -98,6 +109,13 @@ cleanup_site_packages() {
# 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
# NOTE: do NOT strip .py source files. A previous version of this function
# ran `find ... -name "*.py" ! -name "__init__.py" -delete` with a comment
# claiming "keep .pyc only" — but no compileall step exists, so the dist
# shipped with __init__.py + .pyd only, missing every submodule (Image.py,
# ImageDraw.py, _version.py, ...). Fresh installs would fail with
# ModuleNotFoundError; in-place upgrades over an older install produced a
# half-old/half-new site-packages where PIL/__init__.py was new but
# PIL/_version.py was stale, yielding the runtime "_imaging extension was
# built for another version of Pillow" import error.
}
+38 -10
View File
@@ -19,10 +19,15 @@ BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-win-x64"
clean_dist "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
# --- Download embedded Python ---
echo "Downloading embedded Python ${PYTHON_VERSION}..."
curl -sL "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip" \
-o build/python-embed.zip
# --- Download embedded Python (cache-friendly) ---
mkdir -p build
if [ ! -f build/python-embed.zip ]; then
echo "Downloading embedded Python ${PYTHON_VERSION}..."
curl -sL "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip" \
-o build/python-embed.zip
else
echo "Using cached embedded Python ${PYTHON_VERSION}"
fi
unzip -qo build/python-embed.zip -d "${DIST_DIR}/python"
# Patch ._pth to enable site-packages and app source
@@ -35,9 +40,18 @@ echo '..\app' >> "$PTH_FILE"
echo "Downloading Windows wheels..."
# Core dependencies
# NOTE: uvicorn[standard] pulls in uvloop via `sys_platform != 'win32'` marker.
# pip evaluates env markers against the HOST (Linux in CI), so uvloop is
# requested, but `--platform win_amd64 --only-binary :all:` cannot find a
# Windows wheel for uvloop (none exist). Result: pip backtracks across every
# uvicorn[standard] version and ResolutionImpossible. Fix: use plain uvicorn
# and list only the Windows-compatible standard extras we actually need.
CORE_DEPS=(
"fastapi>=0.109.0"
"uvicorn[standard]>=0.27.0"
"uvicorn>=0.27.0"
"httptools>=0.5.0"
"websockets>=10.4"
"python-dotenv>=0.13"
"pydantic>=2.0"
"pydantic-settings>=2.0"
"pyyaml>=6.0"
@@ -53,23 +67,37 @@ WIN_DEPS=(
"pycaw>=20230407"
"screen-brightness-control>=0.20.0"
"monitorcontrol>=3.0.0"
"pystray>=0.19.0"
)
# Visualizer dependencies
VIS_DEPS=(
"soundcard>=0.4.0"
"numpy>=1.24.0,<2.0"
# pystray lives here (not WIN_DEPS) so its transitive Pillow resolves in the
# same pass as CORE_DEPS. Keeping it in the per-dep WIN_DEPS loop downloaded
# a second Pillow version that clobbered the core one on unzip, producing
# "_imaging extension was built for another version of Pillow" at runtime.
"pystray>=0.19.0"
)
ALL_DEPS=("${CORE_DEPS[@]}" "${WIN_DEPS[@]}" "${VIS_DEPS[@]}")
# Resolve core + visualizer deps in a SINGLE call so pip picks compatible
# transitive versions (notably pydantic/pydantic-core must match).
# Per-dep loops resolve each dep independently and can leave mismatched
# transitive versions that overwrite each other in the site-packages unzip.
CROSS_DEPS=("${CORE_DEPS[@]}" "${VIS_DEPS[@]}")
pip download --quiet --no-cache-dir --dest "$WHEEL_DIR" \
--platform win_amd64 --python-version "${PYTHON_SHORT}" \
--implementation cp --only-binary :all: \
"${CROSS_DEPS[@]}"
for dep in "${ALL_DEPS[@]}"; do
pip download --quiet --no-cache-dir --dest "$WHEEL_DIR" \
# Windows-only deps in a loop with --pre: winsdk only ships beta wheels
# (1.0.0bNN) and each dep needs its own platform/non-platform fallback.
for dep in "${WIN_DEPS[@]}"; do
pip download --pre --quiet --no-cache-dir --dest "$WHEEL_DIR" \
--platform win_amd64 --python-version "${PYTHON_SHORT}" \
--implementation cp --only-binary :all: \
"$dep" 2>/dev/null || \
pip download --quiet --no-cache-dir --dest "$WHEEL_DIR" \
pip download --pre --quiet --no-cache-dir --dest "$WHEEL_DIR" \
--only-binary :all: \
"$dep"
done
+13
View File
@@ -70,6 +70,19 @@ Section "!Core (required)" SecCore
SetOutPath "$INSTDIR"
; Wipe previous payload before extracting so stale files from an older
; version cannot survive an upgrade. Without this, in-place upgrades
; produce a half-old/half-new site-packages — e.g. an old PIL/_version.py
; alongside a new PIL/_imaging.pyd, which raises "_imaging extension was
; built for another version of Pillow" at runtime. config.yaml lives at
; $INSTDIR root and is preserved.
RMDir /r "$INSTDIR\python"
RMDir /r "$INSTDIR\app"
RMDir /r "$INSTDIR\scripts"
Delete "$INSTDIR\${EXENAME}"
Delete "$INSTDIR\VERSION"
Delete "$INSTDIR\config.example.yaml"
; Copy entire distribution
File /r "dist\media-server\*.*"
+17
View File
@@ -133,6 +133,23 @@ Object.assign(window, {
// Initialization (DOMContentLoaded)
// ============================================================
// Prevent <dialog>.showModal() from auto-focusing the first input field.
// On touch devices this pops up the on-screen keyboard, which is confusing
// when the user just opened a dialog. Force focus onto the dialog itself.
const _origShowModal = HTMLDialogElement.prototype.showModal;
HTMLDialogElement.prototype.showModal = function (...args) {
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', '-1');
}
const result = _origShowModal.apply(this, args);
const active = document.activeElement;
if (active && active !== this && this.contains(active)) {
active.blur();
this.focus({ preventScroll: true });
}
return result;
};
window.addEventListener('DOMContentLoaded', async () => {
// Cache DOM references
cacheDom();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "media-server-frontend",
"version": "0.1.2",
"version": "0.1.4",
"private": true,
"description": "Frontend build tooling for media server WebUI",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "media-server"
version = "0.1.2"
version = "0.1.4"
description = "REST API server for controlling system-wide media playback"
readme = "README.md"
license = { text = "MIT" }