Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21adeb1070 | |||
| 68614c982d | |||
| a2a258e898 | |||
| 456eb3a881 | |||
| c586b1b518 | |||
| ee5184920d | |||
| af556e0bff | |||
| 26b4672a99 | |||
| 2e3bebfeb8 | |||
| 34eb7c7b19 | |||
| 972ee54b91 | |||
| d09a0b90e4 | |||
| c3cb7a4da9 | |||
| e3889fef29 | |||
| 84500401e7 | |||
| 28293c6340 | |||
| 39b3aed5f3 | |||
| ba90dffa18 | |||
| 69df9b6b95 | |||
| 760c3df90c | |||
| 60f287bb40 | |||
| f52af51a20 | |||
| f2d569a1b0 | |||
| db777fa64b |
+19
-9
@@ -1,11 +1,21 @@
|
|||||||
## v0.1.2 (2026-03-29)
|
## v0.1.8 (2026-04-18)
|
||||||
|
|
||||||
### 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))
|
|
||||||
|
|
||||||
### Bug Fixes
|
### 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 |
|
| 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 |
|
| [68614c9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/68614c9) | fix(windows): keep required numpy submodules in build cleanup | alexei.dolgolyov |
|
||||||
| [cad6e8a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/cad6e8a) | feat: redesign media browser UI | 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 |
|
||||||
| [c9ee41a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c9ee41a) | feat: add media folder management from WebUI | 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>
|
</details>
|
||||||
|
|||||||
+25
-4
@@ -23,6 +23,17 @@ detect_version() {
|
|||||||
|
|
||||||
VERSION_CLEAN="${VERSION#v}"
|
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)
|
# Stamp version into pyproject.toml (single source of truth)
|
||||||
sed -i "s/^version = .*/version = \"${VERSION_CLEAN}\"/" pyproject.toml
|
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
|
find "$sp_dir" -name "*.pyi" -delete 2>/dev/null || true
|
||||||
rm -rf "$sp_dir"/{pip,setuptools,pkg_resources,_distutils_hack}* 2>/dev/null || true
|
rm -rf "$sp_dir"/{pip,setuptools,pkg_resources,_distutils_hack}* 2>/dev/null || true
|
||||||
|
|
||||||
# Trim numpy if present
|
# Trim numpy if present.
|
||||||
for mod in polynomial linalg ma lib distutils f2py typing _pyinstaller; do
|
# 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
|
rm -rf "$sp_dir/numpy/$mod" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -98,6 +112,13 @@ cleanup_site_packages() {
|
|||||||
# Strip debug symbols from native extensions
|
# Strip debug symbols from native extensions
|
||||||
find "$sp_dir" -name "*.$ext_suffix" -exec strip --strip-debug {} \; 2>/dev/null || true
|
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
|
# NOTE: do NOT strip .py source files. A previous version of this function
|
||||||
find "$sp_dir" -name "*.py" ! -name "__init__.py" -delete 2>/dev/null || true
|
# 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
@@ -21,7 +21,7 @@ echo "Creating virtualenv..."
|
|||||||
python3 -m venv "${DIST_DIR}/venv"
|
python3 -m venv "${DIST_DIR}/venv"
|
||||||
source "${DIST_DIR}/venv/bin/activate"
|
source "${DIST_DIR}/venv/bin/activate"
|
||||||
pip install --quiet --upgrade pip
|
pip install --quiet --upgrade pip
|
||||||
pip install --quiet ".[visualizer]"
|
pip install --quiet "."
|
||||||
|
|
||||||
# Remove the installed package (app source is on PYTHONPATH via launcher)
|
# Remove the installed package (app source is on PYTHONPATH via launcher)
|
||||||
rm -rf "${DIST_DIR}"/venv/lib/python*/site-packages/media_server*
|
rm -rf "${DIST_DIR}"/venv/lib/python*/site-packages/media_server*
|
||||||
|
|||||||
+57
-7
@@ -19,10 +19,15 @@ BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-win-x64"
|
|||||||
|
|
||||||
clean_dist "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
clean_dist "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
||||||
|
|
||||||
# --- Download embedded Python ---
|
# --- Download embedded Python (cache-friendly) ---
|
||||||
|
mkdir -p build
|
||||||
|
if [ ! -f build/python-embed.zip ]; then
|
||||||
echo "Downloading embedded Python ${PYTHON_VERSION}..."
|
echo "Downloading embedded Python ${PYTHON_VERSION}..."
|
||||||
curl -sL "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip" \
|
curl -sL "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip" \
|
||||||
-o build/python-embed.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"
|
unzip -qo build/python-embed.zip -d "${DIST_DIR}/python"
|
||||||
|
|
||||||
# Patch ._pth to enable site-packages and app source
|
# Patch ._pth to enable site-packages and app source
|
||||||
@@ -35,9 +40,18 @@ echo '..\app' >> "$PTH_FILE"
|
|||||||
echo "Downloading Windows wheels..."
|
echo "Downloading Windows wheels..."
|
||||||
|
|
||||||
# Core dependencies
|
# 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=(
|
CORE_DEPS=(
|
||||||
"fastapi>=0.109.0"
|
"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>=2.0"
|
||||||
"pydantic-settings>=2.0"
|
"pydantic-settings>=2.0"
|
||||||
"pyyaml>=6.0"
|
"pyyaml>=6.0"
|
||||||
@@ -46,30 +60,50 @@ CORE_DEPS=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Windows-specific dependencies
|
# 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=(
|
WIN_DEPS=(
|
||||||
"winsdk>=1.0.0b10"
|
"winsdk>=1.0.0b10"
|
||||||
"pywin32>=306"
|
"pywin32>=306"
|
||||||
"comtypes>=1.2.0"
|
"comtypes>=1.2.0"
|
||||||
"pycaw>=20230407"
|
"pycaw>=20230407"
|
||||||
"screen-brightness-control>=0.20.0"
|
"screen-brightness-control>=0.20.0"
|
||||||
|
"wmi>=1.5.1"
|
||||||
"monitorcontrol>=3.0.0"
|
"monitorcontrol>=3.0.0"
|
||||||
"pystray>=0.19.0"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Visualizer dependencies
|
# Visualizer dependencies
|
||||||
VIS_DEPS=(
|
VIS_DEPS=(
|
||||||
"soundcard>=0.4.0"
|
"soundcard>=0.4.0"
|
||||||
"numpy>=1.24.0,<2.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).
|
||||||
for dep in "${ALL_DEPS[@]}"; do
|
# 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" \
|
pip download --quiet --no-cache-dir --dest "$WHEEL_DIR" \
|
||||||
|
--platform win_amd64 --python-version "${PYTHON_SHORT}" \
|
||||||
|
--implementation cp --only-binary :all: \
|
||||||
|
"${CROSS_DEPS[@]}"
|
||||||
|
|
||||||
|
# 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}" \
|
--platform win_amd64 --python-version "${PYTHON_SHORT}" \
|
||||||
--implementation cp --only-binary :all: \
|
--implementation cp --only-binary :all: \
|
||||||
"$dep" 2>/dev/null || \
|
"$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: \
|
--only-binary :all: \
|
||||||
"$dep"
|
"$dep"
|
||||||
done
|
done
|
||||||
@@ -85,6 +119,22 @@ for whl in "$WHEEL_DIR"/*.whl; do
|
|||||||
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
||||||
done
|
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"
|
cleanup_site_packages "$SITE_PACKAGES" "pyd" "dll"
|
||||||
verify_frontend
|
verify_frontend
|
||||||
copy_app_files "$DIST_DIR"
|
copy_app_files "$DIST_DIR"
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ Section "!Core (required)" SecCore
|
|||||||
|
|
||||||
SetOutPath "$INSTDIR"
|
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
|
; Copy entire distribution
|
||||||
File /r "dist\media-server\*.*"
|
File /r "dist\media-server\*.*"
|
||||||
|
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ async def set_visualizer_device(
|
|||||||
@router.websocket("/ws")
|
@router.websocket("/ws")
|
||||||
async def websocket_endpoint(
|
async def websocket_endpoint(
|
||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
token: str = Query(..., description="API authentication token"),
|
token: str | None = Query(None, description="API authentication token"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""WebSocket endpoint for real-time media status updates.
|
"""WebSocket endpoint for real-time media status updates.
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,25 @@ def _load_numpy():
|
|||||||
global _np
|
global _np
|
||||||
if _np is None:
|
if _np is None:
|
||||||
try:
|
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
|
import numpy as np
|
||||||
_np = np
|
_np = np
|
||||||
except ImportError:
|
except Exception as e:
|
||||||
logger.info("numpy not installed - audio visualizer unavailable")
|
logger.warning("numpy unavailable - audio visualizer disabled: %s", e)
|
||||||
return _np
|
return _np
|
||||||
|
|
||||||
|
|
||||||
@@ -28,8 +43,8 @@ def _load_soundcard():
|
|||||||
try:
|
try:
|
||||||
import soundcard as sc
|
import soundcard as sc
|
||||||
_sc = sc
|
_sc = sc
|
||||||
except ImportError:
|
except Exception as e:
|
||||||
logger.info("soundcard not installed - audio visualizer unavailable")
|
logger.warning("soundcard unavailable - audio visualizer disabled: %s", e)
|
||||||
return _sc
|
return _sc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class ConnectionManager:
|
|||||||
self._active_connections: set[WebSocket] = set()
|
self._active_connections: set[WebSocket] = set()
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._last_status: dict[str, Any] | None = None
|
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._broadcast_task: asyncio.Task | None = None
|
||||||
self._poll_interval: float = 0.5 # Internal poll interval for change detection
|
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
|
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
|
# Send current status immediately upon connection
|
||||||
if self._last_status:
|
status = self._last_status
|
||||||
|
if not status and self._get_status_func:
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.debug("Failed to send initial status: %s", e)
|
logger.debug("Failed to send initial status: %s", e)
|
||||||
|
|
||||||
@@ -251,6 +260,7 @@ class ConnectionManager:
|
|||||||
if self._running:
|
if self._running:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._get_status_func = get_status_func
|
||||||
self._running = True
|
self._running = True
|
||||||
self._broadcast_task = asyncio.create_task(
|
self._broadcast_task = asyncio.create_task(
|
||||||
self._status_monitor_loop(get_status_func)
|
self._status_monitor_loop(get_status_func)
|
||||||
|
|||||||
@@ -133,6 +133,23 @@ Object.assign(window, {
|
|||||||
// Initialization (DOMContentLoaded)
|
// 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 () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
// Cache DOM references
|
// Cache DOM references
|
||||||
cacheDom();
|
cacheDom();
|
||||||
|
|||||||
@@ -485,7 +485,20 @@ export async function loadAudioDevices() {
|
|||||||
});
|
});
|
||||||
_audioDeviceIconSelect.setValue(select.value, false);
|
_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);
|
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) {
|
} catch (e) {
|
||||||
section.style.display = 'none';
|
section.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
WS_MAX_RECONNECT_ATTEMPTS, WS_PING_INTERVAL_MS,
|
WS_MAX_RECONNECT_ATTEMPTS, WS_PING_INTERVAL_MS,
|
||||||
authRequired, showUpdateBanner,
|
authRequired, showUpdateBanner,
|
||||||
} from './core.js';
|
} 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 { loadScripts, loadScriptsTable, displayQuickAccess } from './scripts.js';
|
||||||
import { loadCallbacksTable } from './callbacks.js';
|
import { loadCallbacksTable } from './callbacks.js';
|
||||||
import { loadHeaderLinks, loadLinksTable } from './links.js';
|
import { loadHeaderLinks, loadLinksTable } from './links.js';
|
||||||
@@ -81,9 +81,6 @@ export function connectWebSocket(token) {
|
|||||||
loadLinksTable();
|
loadLinksTable();
|
||||||
loadHeaderLinks();
|
loadHeaderLinks();
|
||||||
loadAudioDevices();
|
loadAudioDevices();
|
||||||
if (visualizerEnabled && visualizerAvailable) {
|
|
||||||
newWs.send(JSON.stringify({ type: 'enable_visualizer' }));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
newWs.onmessage = (event) => {
|
newWs.onmessage = (event) => {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "media-server-frontend",
|
"name": "media-server-frontend",
|
||||||
"version": "0.1.2",
|
"version": "0.1.8",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "media-server-frontend",
|
"name": "media-server-frontend",
|
||||||
"version": "0.1.2",
|
"version": "0.1.8",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.27.4"
|
"esbuild": "^0.27.4"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "media-server-frontend",
|
"name": "media-server-frontend",
|
||||||
"version": "0.1.2",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Frontend build tooling for media server WebUI",
|
"description": "Frontend build tooling for media server WebUI",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+4
-5
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "media-server"
|
name = "media-server"
|
||||||
version = "0.1.2"
|
version = "0.1.8"
|
||||||
description = "REST API server for controlling system-wide media playback"
|
description = "REST API server for controlling system-wide media playback"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
@@ -32,6 +32,8 @@ dependencies = [
|
|||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"mutagen>=1.47.0",
|
"mutagen>=1.47.0",
|
||||||
"pillow>=10.0.0",
|
"pillow>=10.0.0",
|
||||||
|
"soundcard>=0.4.0",
|
||||||
|
"numpy>=1.24.0,<2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -41,13 +43,10 @@ windows = [
|
|||||||
"comtypes>=1.2.0",
|
"comtypes>=1.2.0",
|
||||||
"pycaw>=20230407",
|
"pycaw>=20230407",
|
||||||
"screen-brightness-control>=0.20.0",
|
"screen-brightness-control>=0.20.0",
|
||||||
|
"wmi>=1.5.1",
|
||||||
"monitorcontrol>=3.0.0",
|
"monitorcontrol>=3.0.0",
|
||||||
"pystray>=0.19.0",
|
"pystray>=0.19.0",
|
||||||
]
|
]
|
||||||
visualizer = [
|
|
||||||
"soundcard>=0.4.0",
|
|
||||||
"numpy>=1.24.0,<2.0",
|
|
||||||
]
|
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=7.0",
|
"pytest>=7.0",
|
||||||
"pytest-asyncio>=0.21",
|
"pytest-asyncio>=0.21",
|
||||||
|
|||||||
Reference in New Issue
Block a user