6ae0ed1787
Release / release (push) Successful in 2s
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.
155 lines
8.3 KiB
Markdown
155 lines
8.3 KiB
Markdown
# 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 5–60 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.
|