Compare commits
2 Commits
6120625fa9
...
v0.2.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 770bba7e60 | |||
| d1f621f0b4 |
+15
-8
@@ -1,14 +1,20 @@
|
||||
## v0.2.3 (2026-05-01)
|
||||
## v0.2.4 (2026-05-15)
|
||||
|
||||
### UI / Player
|
||||
### Features
|
||||
|
||||
- Square the vinyl stage (`1:0.85` → `1:1`) and pin the tonearm to `height: 36%` instead of `aspect-ratio: 1` so its vertical span tracks the stage on resize. Refines the geometry shipped in v0.2.2. ([d27484a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d27484a))
|
||||
- Brighten the tonearm SVG: lighter pivot/arm gradient stops, thicker stroke widths, stronger cartridge highlight. ([d27484a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d27484a))
|
||||
- Tilt the sleeve `-2deg` so it reads as resting on the disc rather than rectilinearly composed. ([d27484a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d27484a))
|
||||
- **Displays — DDC/CI picture controls:** Monitor cards now expose contrast (slider, same editorial copper treatment as brightness) plus a new "PICTURE TUNING" section with input source, color preset, and picture mode pickers built on the IconSelect widget — HDMI/DisplayPort/DVI/VGA/USB-C glyphs for inputs, thermometer for color temperatures, per-mode icons (movie reel, gamepad, ball, etc.) for picture modes. Backend probes DDC/CI capabilities per monitor at enumeration time and exposes `*_supported` flags so the UI hides rows the hardware doesn't advertise. New endpoints: `POST /api/display/{contrast,input_source,color_preset,picture_mode}/{id}`. Picture mode uses raw VCP `0xDC` with MCCS-spec labels and vendor-friendly fallbacks. 14 new i18n keys per locale (en/ru). ([57fdeb7](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/57fdeb7))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Displays:** keep the primary-display star visible on long monitor names. Move `overflow: hidden` + ellipsis off the parent flex container onto a new inner span, and add `flex-shrink: 0` to the badge so the favourite indicator no longer gets clipped when the model name truncates. ([372e4eb](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/372e4eb))
|
||||
- **Displays — verify DDC/CI writes before reporting success:** DDC/CI writes are fire-and-forget at the protocol level — a successful send does not mean the monitor honored the value. A new `_verify_after_set` helper polls readback after every write and reports `{success: false}` when the monitor silently dropped it (common on LG ultrawides for VCP codes whose registers exist but whose feature isn't really implemented in firmware). Wired into `set_contrast`, `set_input_source`, `set_color_preset`, `set_picture_mode`; input source uses a longer settle window since switching can briefly disrupt the DDC/CI link. Picture mode (VCP `0xDC`) additionally requires the capability string to declare supported codes under `cmds[0xDC]` — without that declaration we treat the feature as unsupported even when reads succeed (the LG case where reads return a stuck value and every write is silently ignored). ([d1f621f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d1f621f))
|
||||
|
||||
---
|
||||
|
||||
### Development / Internal
|
||||
|
||||
#### Chores
|
||||
|
||||
- **`restart-server.ps1` — installer vs dev launches:** Previously only killed processes named `media-server`, silently missing the installer-bundled process (which runs as plain `python.exe` via `media-server.bat`). The script now kills whatever currently owns the listen port regardless of process name, adds `-Mode auto|dev|installer` with auto-detection based on whether the installer launcher exists in `%LOCALAPPDATA%\Media Server`, verifies the port is listening after start, and merges registry PATH so newly-installed dev tools are visible. ([6120625](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/6120625))
|
||||
|
||||
---
|
||||
|
||||
@@ -17,7 +23,8 @@
|
||||
|
||||
| Hash | Message | Author |
|
||||
|------|---------|--------|
|
||||
| [d27484a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d27484a) | ui(player): square vinyl stage, brighter tonearm, tilted sleeve | alexei.dolgolyov |
|
||||
| [372e4eb](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/372e4eb) | fix(displays): keep primary-display star visible on long monitor names | alexei.dolgolyov |
|
||||
| [57fdeb7](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/57fdeb7) | feat(displays): expose DDC/CI contrast, input source, color preset, picture mode | alexei.dolgolyov |
|
||||
| [6120625](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/6120625) | chore(scripts): harden restart-server.ps1 against installer vs dev launches | alexei.dolgolyov |
|
||||
| [d1f621f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d1f621f) | fix(displays): verify DDC/CI writes and trust capability string for picture mode | alexei.dolgolyov |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -267,20 +267,27 @@ def _probe_static_open(mon, mc, monitor_id: int) -> dict:
|
||||
except Exception as e:
|
||||
logger.debug("Monitor %d: color_preset unsupported: %s", monitor_id, e)
|
||||
|
||||
# Picture / scene mode (VCP 0xDC) — not exposed by monitorcontrol's
|
||||
# high-level API, so probe via raw VCP transport.
|
||||
try:
|
||||
mon.vcp.get_vcp_feature(PICTURE_MODE_VCP)
|
||||
static["picture_mode_supported"] = True
|
||||
cmds = caps.get("cmds") or {}
|
||||
declared = cmds.get(PICTURE_MODE_VCP)
|
||||
codes = sorted(declared) if declared else sorted(PICTURE_MODE_LABELS.keys())
|
||||
static["available_picture_modes"] = [
|
||||
{"code": c, "label": PICTURE_MODE_LABELS.get(c, f"Mode {c}")}
|
||||
for c in codes
|
||||
]
|
||||
except Exception as e:
|
||||
logger.debug("Monitor %d: picture_mode unsupported: %s", monitor_id, e)
|
||||
# Picture / scene mode (VCP 0xDC). Trickier than color preset because
|
||||
# many monitors (LG ultrawides included) respond to READS but silently
|
||||
# drop every WRITE - they implement the register but not the feature.
|
||||
# The capability string is the most reliable signal: a monitor that
|
||||
# really implements picture mode declares its supported codes under
|
||||
# cmds[0xDC]. If 0xDC isn't declared, treat the feature as unsupported
|
||||
# to avoid exposing a non-functional select.
|
||||
cmds = caps.get("cmds") or {}
|
||||
declared = cmds.get(PICTURE_MODE_VCP)
|
||||
if declared:
|
||||
try:
|
||||
mon.vcp.get_vcp_feature(PICTURE_MODE_VCP)
|
||||
static["picture_mode_supported"] = True
|
||||
static["available_picture_modes"] = [
|
||||
{"code": c, "label": PICTURE_MODE_LABELS.get(c, f"Mode {c}")}
|
||||
for c in sorted(declared)
|
||||
]
|
||||
except Exception as e:
|
||||
logger.debug("Monitor %d: picture_mode declared but unreadable: %s", monitor_id, e)
|
||||
else:
|
||||
logger.debug("Monitor %d: picture_mode (VCP 0xDC) not declared in capability string", monitor_id)
|
||||
|
||||
return static
|
||||
|
||||
@@ -498,6 +505,31 @@ def set_power(monitor_id: int, on: bool) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _verify_after_set(getter, expected, *, retries: int = 3, delay: float = 0.1) -> bool:
|
||||
"""Poll a DDC/CI getter to confirm the monitor actually applied a write.
|
||||
|
||||
DDC/CI writes are fire-and-forget at the protocol level: a successful
|
||||
send does not mean the monitor honored the value. Many monitors silently
|
||||
drop writes for codes their firmware doesn't really implement (LG's
|
||||
ColorPreset / Picture Mode are common offenders). Without this check the
|
||||
API would report `success: true` while the monitor sat unchanged.
|
||||
|
||||
Compares both raw and `.value` forms so enum/int mismatches don't flag a
|
||||
spurious failure.
|
||||
"""
|
||||
expected_int = getattr(expected, "value", expected)
|
||||
for _ in range(retries):
|
||||
time.sleep(delay)
|
||||
try:
|
||||
actual = getter()
|
||||
except Exception:
|
||||
continue
|
||||
actual_int = getattr(actual, "value", actual)
|
||||
if actual == expected or actual_int == expected_int:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def set_contrast(monitor_id: int, value: int) -> bool:
|
||||
"""Set contrast for a specific monitor (0-100) via DDC/CI."""
|
||||
mc = _load_monitorcontrol()
|
||||
@@ -511,6 +543,9 @@ def set_contrast(monitor_id: int, value: int) -> bool:
|
||||
return False
|
||||
with ddc_monitors[monitor_id] as monitor:
|
||||
monitor.set_contrast(value)
|
||||
if not _verify_after_set(monitor.get_contrast, value):
|
||||
logger.warning("Monitor %d: contrast %d not applied", monitor_id, value)
|
||||
return False
|
||||
_invalidate_cache()
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -536,6 +571,11 @@ def set_input_source(monitor_id: int, source: str) -> bool:
|
||||
return False
|
||||
with ddc_monitors[monitor_id] as monitor:
|
||||
monitor.set_input_source(target)
|
||||
# Source switches can briefly disrupt the DDC/CI link; allow a
|
||||
# longer settle window before declaring failure.
|
||||
if not _verify_after_set(monitor.get_input_source, target, retries=5, delay=0.2):
|
||||
logger.warning("Monitor %d: input source %s not applied", monitor_id, source)
|
||||
return False
|
||||
_invalidate_cache()
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -561,6 +601,12 @@ def set_color_preset(monitor_id: int, preset: str) -> bool:
|
||||
return False
|
||||
with ddc_monitors[monitor_id] as monitor:
|
||||
monitor.set_color_preset(target)
|
||||
if not _verify_after_set(monitor.get_color_preset, target):
|
||||
logger.warning(
|
||||
"Monitor %d: color preset %s not applied (monitor silently rejected)",
|
||||
monitor_id, preset,
|
||||
)
|
||||
return False
|
||||
_invalidate_cache()
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -584,6 +630,16 @@ def set_picture_mode(monitor_id: int, code: int) -> bool:
|
||||
return False
|
||||
with ddc_monitors[monitor_id] as monitor:
|
||||
monitor.vcp.set_vcp_feature(PICTURE_MODE_VCP, code)
|
||||
# Raw VCP read returns (current, maximum) — only compare current.
|
||||
def _read_picture_mode():
|
||||
current, _ = monitor.vcp.get_vcp_feature(PICTURE_MODE_VCP)
|
||||
return current
|
||||
if not _verify_after_set(_read_picture_mode, code):
|
||||
logger.warning(
|
||||
"Monitor %d: picture mode code %d not applied (monitor silently rejected)",
|
||||
monitor_id, code,
|
||||
)
|
||||
return False
|
||||
_invalidate_cache()
|
||||
return True
|
||||
except Exception as e:
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "media-server-frontend",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "media-server-frontend",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.27.4"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "media-server-frontend",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"private": true,
|
||||
"description": "Frontend build tooling for media server WebUI",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "media-server"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
description = "REST API server for controlling system-wide media playback"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
|
||||
Reference in New Issue
Block a user