Compare commits

...

24 Commits

Author SHA1 Message Date
alexei.dolgolyov 21adeb1070 chore: release v0.1.8
Lint & Test / test (push) Successful in 15s
Release / create-release (push) Successful in 8s
Release / build-linux (push) Successful in 40s
Release / build-windows (push) Successful in 1m7s
2026-04-18 19:49:49 +03:00
alexei.dolgolyov 68614c982d fix(windows): keep required numpy submodules in build cleanup
lib, linalg, ma, polynomial, fft, ctypeslib, matrixlib are imported
unconditionally by numpy/__init__.py and must not be trimmed.
2026-04-18 19:49:09 +03:00
alexei.dolgolyov a2a258e898 chore: release v0.1.8
Lint & Test / test (push) Successful in 10s
Release / create-release (push) Successful in 3s
Release / build-linux (push) Successful in 48s
Release / build-windows (push) Successful in 1m19s
2026-04-18 19:30:02 +03:00
alexei.dolgolyov 456eb3a881 fix(windows): fix numpy DLL loading in embedded Python distribution
- Generate numpy/_distributor_init_local.py during build so libopenblas
  can be located when running from the Windows installer
- Add os.add_dll_directory() call at runtime as a fallback for embedded Python
2026-04-18 19:29:39 +03:00
alexei.dolgolyov c586b1b518 chore: release v0.1.8
Lint & Test / test (push) Successful in 10s
Release / create-release (push) Successful in 3s
Release / build-linux (push) Successful in 47s
Release / build-windows (push) Successful in 1m10s
2026-04-18 17:51:46 +03:00
alexei.dolgolyov ee5184920d fix(visualizer): sync state and re-subscribe from audio device load
- Broaden audio import errors from ImportError to Exception, log at warning
- Move visualizer WS re-subscription into loadAudioDevices() so it runs
  after availability is confirmed from the API
- Show/hide the visualizer toggle button based on fetched availability
2026-04-18 17:48:49 +03:00
alexei.dolgolyov af556e0bff chore: release v0.1.7
Lint & Test / test (push) Successful in 14s
Release / create-release (push) Successful in 3s
Release / build-windows (push) Successful in 47s
Release / build-linux (push) Successful in 29s
2026-04-17 23:40:45 +03:00
alexei.dolgolyov 26b4672a99 chore: release v0.1.6
Release / create-release (push) Successful in 3s
Lint & Test / test (push) Successful in 10s
Release / build-linux (push) Successful in 31s
Release / build-windows (push) Successful in 52s
2026-04-11 03:36:55 +03:00
alexei.dolgolyov 2e3bebfeb8 chore: release v0.1.5
Release / create-release (push) Successful in 10s
Lint & Test / test (push) Successful in 15s
Release / build-linux (push) Successful in 48s
Release / build-windows (push) Successful in 1m10s
2026-04-11 02:09:29 +03:00
alexei.dolgolyov 34eb7c7b19 fix(ws): make WebSocket token parameter optional
Required token query param caused connection failures for clients
that authenticate via other means.
2026-04-11 02:04:36 +03:00
alexei.dolgolyov 972ee54b91 chore: release v0.1.5
Release / create-release (push) Successful in 3s
Lint & Test / test (push) Successful in 10s
Release / build-linux (push) Successful in 53s
Release / build-windows (push) Successful in 1m17s
2026-04-11 01:43:43 +03:00
alexei.dolgolyov d09a0b90e4 fix(ws): fetch status eagerly on new WebSocket connection
Instead of waiting for the next poll cycle, new clients now get the
current playback status immediately on connect by calling get_status_func
if no cached status is available yet.
2026-04-11 01:40:40 +03:00
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
14 changed files with 188 additions and 43 deletions
+19 -9
View File
@@ -1,11 +1,21 @@
## 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.8 (2026-04-18)
### Bug Fixes
- Make folder status visible with dot + text label ([c50a8f4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c50a8f4))
- Fix numpy failing to import in the Windows installer — preserve required numpy submodules (`lib`, `linalg`, `ma`, `polynomial`, `fft`, `ctypeslib`, `matrixlib`) during build cleanup ([68614c9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/68614c9))
- Fix numpy failing to locate `libopenblas` DLL in the Windows installer — generate `_distributor_init_local.py` at build time and call `os.add_dll_directory()` at runtime ([456eb3a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/456eb3a))
- Fix visualizer toggle button not reflecting actual availability after audio device load ([ee51849](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ee51849))
- Fix visualizer WebSocket re-subscription firing before availability is confirmed from the API — moved from `connectWebSocket` to `loadAudioDevices` ([ee51849](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ee51849))
---
### Development / Internal
#### CI/Build
- Generate `numpy/_distributor_init_local.py` in Windows build script to fix DLL loading in embedded Python ([456eb3a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/456eb3a))
#### Other
- Broaden audio library import errors from `ImportError` to `Exception` and log at `warning` level with error details ([ee51849](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ee51849))
---
@@ -14,8 +24,8 @@
| 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 |
| [68614c9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/68614c9) | fix(windows): keep required numpy submodules in build cleanup | alexei.dolgolyov |
| [456eb3a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/456eb3a) | fix(windows): fix numpy DLL loading in embedded Python distribution | alexei.dolgolyov |
| [ee51849](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ee51849) | fix(visualizer): sync state and re-subscribe from audio device load | alexei.dolgolyov |
</details>
+25 -4
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
}
@@ -80,8 +91,11 @@ cleanup_site_packages() {
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
# Trim numpy if present.
# Keep only modules that numpy/__init__.py does NOT import unconditionally —
# lib, linalg, ma, polynomial, fft, ctypeslib, matrixlib are all required for
# `import numpy` to succeed, so they MUST stay.
for mod in distutils f2py typing _pyinstaller; do
rm -rf "$sp_dir/numpy/$mod" 2>/dev/null || true
done
@@ -98,6 +112,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.
}
+1 -1
View File
@@ -21,7 +21,7 @@ echo "Creating virtualenv..."
python3 -m venv "${DIST_DIR}/venv"
source "${DIST_DIR}/venv/bin/activate"
pip install --quiet --upgrade pip
pip install --quiet ".[visualizer]"
pip install --quiet "."
# Remove the installed package (app source is on PYTHONPATH via launcher)
rm -rf "${DIST_DIR}"/venv/lib/python*/site-packages/media_server*
+60 -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"
@@ -46,30 +60,50 @@ CORE_DEPS=(
)
# Windows-specific dependencies
# NOTE: wmi is a transitive dep of screen-brightness-control gated on
# `platform_system == "Windows"`. pip evaluates env markers against the HOST
# (Linux in CI), so it gets skipped during cross-build. Listed explicitly here
# so the wheel actually lands in the Windows bundle. Same gotcha as the
# uvicorn[standard]/uvloop case documented above.
WIN_DEPS=(
"winsdk>=1.0.0b10"
"pywin32>=306"
"comtypes>=1.2.0"
"pycaw>=20230407"
"screen-brightness-control>=0.20.0"
"wmi>=1.5.1"
"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
@@ -85,6 +119,22 @@ for whl in "$WHEEL_DIR"/*.whl; do
unzip -qo "$whl" -d "$SITE_PACKAGES"
done
# numpy wheels from PyPI don't include _distributor_init_local.py unless
# patched by delvewheel. In embedded Python, os.add_dll_directory() is never
# called, so libopenblas can't be found and numpy fails to import.
# Generate the missing loader here instead.
if [ -d "${SITE_PACKAGES}/numpy" ]; then
cat > "${SITE_PACKAGES}/numpy/_distributor_init_local.py" << 'EOF'
import os
import sys
if sys.platform == 'win32':
_libs = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'numpy.libs'))
if os.path.isdir(_libs):
os.add_dll_directory(_libs)
EOF
echo "Generated numpy/_distributor_init_local.py"
fi
cleanup_site_packages "$SITE_PACKAGES" "pyd" "dll"
verify_frontend
copy_app_files "$DIST_DIR"
+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\*.*"
+1 -1
View File
@@ -323,7 +323,7 @@ async def set_visualizer_device(
@router.websocket("/ws")
async def websocket_endpoint(
websocket: WebSocket,
token: str = Query(..., description="API authentication token"),
token: str | None = Query(None, description="API authentication token"),
) -> None:
"""WebSocket endpoint for real-time media status updates.
+19 -4
View File
@@ -15,10 +15,25 @@ def _load_numpy():
global _np
if _np is None:
try:
import os
import sys
if sys.platform == 'win32':
# Embedded Python doesn't auto-load DLLs from numpy.libs;
# add the directory explicitly so libopenblas can be found.
try:
import importlib.util
spec = importlib.util.find_spec('numpy')
if spec and spec.submodule_search_locations:
numpy_dir = list(spec.submodule_search_locations)[0]
libs_dir = os.path.join(os.path.dirname(numpy_dir), 'numpy.libs')
if os.path.isdir(libs_dir):
os.add_dll_directory(libs_dir)
except Exception:
pass
import numpy as np
_np = np
except ImportError:
logger.info("numpy not installed - audio visualizer unavailable")
except Exception as e:
logger.warning("numpy unavailable - audio visualizer disabled: %s", e)
return _np
@@ -28,8 +43,8 @@ def _load_soundcard():
try:
import soundcard as sc
_sc = sc
except ImportError:
logger.info("soundcard not installed - audio visualizer unavailable")
except Exception as e:
logger.warning("soundcard unavailable - audio visualizer disabled: %s", e)
return _sc
+12 -2
View File
@@ -19,6 +19,7 @@ class ConnectionManager:
self._active_connections: set[WebSocket] = set()
self._lock = asyncio.Lock()
self._last_status: dict[str, Any] | None = None
self._get_status_func: Callable[[], Coroutine[Any, Any, Any]] | None = None
self._broadcast_task: asyncio.Task | None = None
self._poll_interval: float = 0.5 # Internal poll interval for change detection
self._position_broadcast_interval: float = 5.0 # Send position updates every 5s during playback
@@ -39,9 +40,17 @@ class ConnectionManager:
)
# Send current status immediately upon connection
if self._last_status:
status = self._last_status
if not status and self._get_status_func:
try:
await websocket.send_json({"type": "status", "data": self._last_status})
result = await self._get_status_func()
status = result.model_dump()
self._last_status = status
except Exception as e:
logger.debug("Failed to fetch initial status: %s", e)
if status:
try:
await websocket.send_json({"type": "status", "data": status})
except Exception as e:
logger.debug("Failed to send initial status: %s", e)
@@ -251,6 +260,7 @@ class ConnectionManager:
if self._running:
return
self._get_status_func = get_status_func
self._running = True
self._broadcast_task = asyncio.create_task(
self._status_monitor_loop(get_status_func)
+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();
+13
View File
@@ -485,7 +485,20 @@ export async function loadAudioDevices() {
});
_audioDeviceIconSelect.setValue(select.value, false);
// Sync visualizerAvailable from the fetched status so that
// applyVisualizerMode() and the toggle button are consistent.
visualizerAvailable = status.available;
const btn = document.getElementById('visualizerToggle');
if (btn) btn.style.display = visualizerAvailable ? '' : 'none';
updateAudioDeviceStatus(status);
// Re-subscribe the WebSocket if the user had the visualizer enabled.
if (visualizerEnabled && visualizerAvailable) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'enable_visualizer' }));
}
}
} catch (e) {
section.style.display = 'none';
}
+1 -4
View File
@@ -8,7 +8,7 @@ import {
WS_MAX_RECONNECT_ATTEMPTS, WS_PING_INTERVAL_MS,
authRequired, showUpdateBanner,
} from './core.js';
import { updateUI, visualizerEnabled, visualizerAvailable, setFrequencyData, stopPositionInterpolation, loadAudioDevices } from './player.js';
import { updateUI, setFrequencyData, stopPositionInterpolation, loadAudioDevices } from './player.js';
import { loadScripts, loadScriptsTable, displayQuickAccess } from './scripts.js';
import { loadCallbacksTable } from './callbacks.js';
import { loadHeaderLinks, loadLinksTable } from './links.js';
@@ -81,9 +81,6 @@ export function connectWebSocket(token) {
loadLinksTable();
loadHeaderLinks();
loadAudioDevices();
if (visualizerEnabled && visualizerAvailable) {
newWs.send(JSON.stringify({ type: 'enable_visualizer' }));
}
};
newWs.onmessage = (event) => {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "media-server-frontend",
"version": "0.1.2",
"version": "0.1.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "media-server-frontend",
"version": "0.1.2",
"version": "0.1.8",
"devDependencies": {
"esbuild": "^0.27.4"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "media-server-frontend",
"version": "0.1.2",
"version": "0.1.8",
"private": true,
"description": "Frontend build tooling for media server WebUI",
"scripts": {
+4 -5
View File
@@ -1,6 +1,6 @@
[project]
name = "media-server"
version = "0.1.2"
version = "0.1.8"
description = "REST API server for controlling system-wide media playback"
readme = "README.md"
license = { text = "MIT" }
@@ -32,6 +32,8 @@ dependencies = [
"pyyaml>=6.0",
"mutagen>=1.47.0",
"pillow>=10.0.0",
"soundcard>=0.4.0",
"numpy>=1.24.0,<2.0",
]
[project.optional-dependencies]
@@ -41,13 +43,10 @@ windows = [
"comtypes>=1.2.0",
"pycaw>=20230407",
"screen-brightness-control>=0.20.0",
"wmi>=1.5.1",
"monitorcontrol>=3.0.0",
"pystray>=0.19.0",
]
visualizer = [
"soundcard>=0.4.0",
"numpy>=1.24.0,<2.0",
]
dev = [
"pytest>=7.0",
"pytest-asyncio>=0.21",