Files
haos-hacs-emby-media-player/CLAUDE.md
T
alexei.dolgolyov 6ae0ed1787
Release / release (push) Successful in 2s
chore: release v0.2.0
Production-readiness pass: security hardening, performance improvements,
new services (send_message, set_repeat, refresh_library), diagnostics,
reauth flow, image proxy, per-instance device IDs, exponential WS
reconnect backoff, ID validation, stale device cleanup, and supporting
integration plumbing. Three rounds of independent code review applied.

See RELEASE_NOTES.md for the full changelog.
2026-05-26 13:16:36 +03:00

8.3 KiB
Raw Blame History

Claude Code Project Notes

Operator notes for working on this repo with Claude Code (or any other LLM coding agent). Keep this file accurate; future agents read it as ground truth.

Project at a glance

  • What this is. A Home Assistant custom integration that exposes Emby Server clients as media_player entities, with WebSocket real-time updates and a REST polling safety net.
  • Layout. All code lives under custom_components/emby_player/. HACS metadata at the repo root (hacs.json, RELEASE_NOTES.md, README.md).
  • Distribution. HACS custom repo, hosted on a private Gitea instance (git.dolgolyov-family.by). No GitHub mirror — manifest.json keeps codeowners: [] because hassfest validates handles against GitHub.
  • Min HA version. 2024.10.0 (declared in hacs.json); the Python is expected to be 3.12+ as shipped by HAOS.
  • Quality scale. Targets silver.

Module map

File Role
__init__.py Entry setup/unload, runtime data dataclass, hub device, manifest-version lookup, async_remove_config_entry_device
api.py REST client. Owns no aiohttp session. Validates IDs and image types before URL build. Pins working /emby vs "" prefix on connect.
websocket.py WS client. API key in headers (never URL). Exponential backoff + jitter. Detaches async callbacks via task.
coordinator.py DataUpdateCoordinator subclass. Frozen dataclasses. WS events update sessions in place. REST poll merges via last_seen. _safe_int for malformed payloads.
media_player.py The entity class. Image proxy via async_get_media_image. Stale device prune. Enqueue → Emby command map.
config_flow.py User + reauth flows. Uses getattr fallbacks for HA-version-shifting helpers.
browse_media.py Media browser; wraps all errors as BrowseError.
services.py / services.yaml send_message, set_repeat, refresh_library.
diagnostics.py Redacted entry + sessions dump; session IDs hashed.
const.py All constants, including EMBY_ID_PATTERN, ALLOWED_IMAGE_TYPES, timeouts, backoff bounds.
manifest.json HA integration metadata + ssdp / zeroconf hints.
strings.json / translations/en.json UI strings (config flow, services, reauth).

Workflow expectations

  1. Plan first. For anything bigger than a typo, use planner / architect agents (or sketch a TodoWrite plan) before editing files.
  2. Edit minimally. Match the project's style — explicit type hints, from __future__ import annotations, frozen dataclasses for state.
  3. Verify locally.
    • Parse check: python -c "import ast, pathlib; [ast.parse(p.read_text(encoding='utf-8'), filename=str(p)) for p in pathlib.Path('custom_components/emby_player').glob('*.py')]"
    • JSON validation for manifest.json, strings.json, translations/*.json, hacs.json.
  4. Review before commit. Spawn python-reviewer (or code-reviewer) after any non-trivial change. Two-round reviews are the norm here — the first pass usually surfaces hidden issues.
  5. Never commit without explicit user approval. This rule has been broken before — don't.

Conventions / invariants

  • Aiohttp session is owned by HA. API and WebSocket clients accept an injected aiohttp.ClientSession and never close it. Do not add _ensure_session or _owns_session flags back.
  • IDs are validated. Anything that lands in a URL path (session_id, item_id, user_id, parent_id) must go through _validate_emby_id (regex ^[A-Za-z0-9_-]{1,128}$). Don't bypass.
  • Image type whitelist. image_type must be in ALLOWED_IMAGE_TYPES (see const.py). Adding a new image type means updating the constant.
  • API key never goes into URLs. Image fetches use the X-Emby-Token header via EmbyApiClient.fetch_image. The HA media-player image proxy (async_get_media_image / async_get_browse_image) is the public path.
  • WebSocket auth via headers. Same reason: no query-string leakage to proxy logs.
  • Device identity is per-config-entry. device_id is derived from instance_id.async_get(hass) + entry.entry_id. Multiple HA installs on one Emby server must not collide.
  • Frozen dataclasses for coordinator state. Use dataclasses.replace(...) to produce a new session; never mutate.
  • Auth failure routing. Authentication errors during _async_update_data must raise ConfigEntryAuthFailed so HA triggers the reauth flow. Do not re-introduce a custom subclass of UpdateFailed for auth.
  • No raw print / no emoji in code or comments. Use _LOGGER.
  • Comments explain WHY only. Identifier names should carry the WHAT.

Emby API gotchas (historical, still worth remembering)

  • /emby prefix. Most Emby Server deployments require /emby/ in front of API paths. The client probes both /emby/System/Info and /System/Info on test_connection() and pins the working prefix in self._prefix. Don't hardcode the prefix into endpoint constants.
  • General commands. Emby's /Sessions/{id}/Command endpoint expects {"Name": "<command>", "Arguments": {...}}, NOT /Command/{commandName}?.... Arguments must be string-typed values, so _send_command stringifies everything.
  • WebSocket ForceKeepAlive. Emby will close the WS connection if you don't echo back KeepAlive to ForceKeepAlive. Already handled in _handle_message; don't strip that path.
  • Non-admin API keys. GET /Users returns 401/403 for non-admin tokens. EmbyApiClient.get_users falls back to /Users/Public.
  • Ticks. Emby uses 100ns ticks (TICKS_PER_SECOND = 10_000_000). Use round() not int() when converting seconds → ticks to avoid off-by-one on resume positions.
  • NumberSelector returns float. Always int(...) before passing to the API layer (port, scan_interval, etc.).
  • PlaySessionId vs SessionId. WS playback events carry both; SessionId is the per-client session, PlaySessionId is per-stream and can collide across devices. The coordinator only keys off SessionId.

Performance budget

  • WS connected. Trust WS for live updates. REST poll runs every 5 min (DEFAULT_SCAN_INTERVAL_WS = 300) as a safety net; the merge logic in _async_update_data keeps WS-current sessions intact.
  • WS down. Polling falls back to the user-configured scan_interval (default 10 s; range 560 s).
  • Image fetches. Run on a separate 30 s timeout (IMAGE_FETCH_TIMEOUT_SECONDS), independent of the 15 s REST timeout.

Security checklist (anything that touches the API key or user URLs)

  • API key only travels in headers (X-Emby-Token) or HA's own redacted config entry storage — never URLs.
  • All IDs flowing into REST paths validated.
  • image_type validated against ALLOWED_IMAGE_TYPES.
  • Diagnostics redact API key and hash session/device/user IDs.
  • verify_ssl honoured for HTTPS; defaults to verifying.

Files NOT to edit casually

  • hacs.json — bumping the homeassistant floor is a breaking change for installed users; coordinate with a release.
  • manifest.json version — must be bumped together with RELEASE_NOTES.md and a Git tag.
  • RELEASE_NOTES.md v0.x.0 sections — append new sections; don't rewrite history.

Release flow

  1. Land all changes on a feature branch.
  2. Bump manifest.json version + add a section to RELEASE_NOTES.md (newest first).
  3. Open a PR on Gitea; wait for review.
  4. After merge, tag vX.Y.Z on the default branch — the Gitea release workflow under .gitea/ cuts the release.

Anti-patterns we've already paid for

  • Bypassing constants and hardcoding endpoint paths — see history of the missing /emby prefix.
  • Reading API responses without isinstance checks — Emby sometimes returns null or unexpected shapes for fields like RunTimeTicks. Use the _safe_int helper in coordinator.py for any numeric field coming off the wire.
  • Reusing a fixed DEVICE_ID across HA instances — Emby will evict whichever instance last registered, silently breaking the other.
  • Including the API key in URLs returned to the browser via media_image_url. The fix is the image proxy in media_player.py; don't reintroduce the URL-with-key shortcut "for performance".
  • except Exception: pass — always at least _LOGGER.debug(...) so failures are diagnosable.