Compare commits

..

12 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
12 changed files with 94 additions and 39 deletions
+13 -17
View File
@@ -1,19 +1,21 @@
## v0.1.4 (2026-04-07)
## v0.1.8 (2026-04-18)
### Bug Fixes
- Prevent dialog `showModal` from auto-focusing first input ([db777fa](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/db777fa))
- 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
- 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))
- 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))
---
@@ -22,14 +24,8 @@
| Hash | Message | Author |
|------|---------|--------|
| [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 |
| [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>
+5 -2
View File
@@ -91,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
+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*
+22
View File
@@ -60,12 +60,18 @@ 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"
)
@@ -113,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"
+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)
+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.4",
"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.4"
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",