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

155 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.