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.
8.3 KiB
8.3 KiB
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_playerentities, 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.jsonkeepscodeowners: []because hassfest validates handles against GitHub. - Min HA version.
2024.10.0(declared inhacs.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
- Plan first. For anything bigger than a typo, use
planner/architectagents (or sketch a TodoWrite plan) before editing files. - Edit minimally. Match the project's style — explicit type hints,
from __future__ import annotations, frozen dataclasses for state. - 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.
- Parse check:
- Review before commit. Spawn
python-reviewer(orcode-reviewer) after any non-trivial change. Two-round reviews are the norm here — the first pass usually surfaces hidden issues. - 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.ClientSessionand never close it. Do not add_ensure_sessionor_owns_sessionflags 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_typemust be inALLOWED_IMAGE_TYPES(seeconst.py). Adding a new image type means updating the constant. - API key never goes into URLs. Image fetches use the
X-Emby-Tokenheader viaEmbyApiClient.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_idis derived frominstance_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_datamust raiseConfigEntryAuthFailedso HA triggers the reauth flow. Do not re-introduce a custom subclass ofUpdateFailedfor 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)
/embyprefix. Most Emby Server deployments require/emby/in front of API paths. The client probes both/emby/System/Infoand/System/Infoontest_connection()and pins the working prefix inself._prefix. Don't hardcode the prefix into endpoint constants.- General commands. Emby's
/Sessions/{id}/Commandendpoint expects{"Name": "<command>", "Arguments": {...}}, NOT/Command/{commandName}?.... Arguments must be string-typed values, so_send_commandstringifies everything. - WebSocket
ForceKeepAlive. Emby will close the WS connection if you don't echo backKeepAlivetoForceKeepAlive. Already handled in_handle_message; don't strip that path. - Non-admin API keys.
GET /Usersreturns 401/403 for non-admin tokens.EmbyApiClient.get_usersfalls back to/Users/Public. - Ticks. Emby uses 100ns ticks (
TICKS_PER_SECOND = 10_000_000). Useround()notint()when converting seconds → ticks to avoid off-by-one on resume positions. NumberSelectorreturns float. Alwaysint(...)before passing to the API layer (port, scan_interval, etc.).PlaySessionIdvsSessionId. WS playback events carry both;SessionIdis the per-client session,PlaySessionIdis per-stream and can collide across devices. The coordinator only keys offSessionId.
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_datakeeps 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_typevalidated againstALLOWED_IMAGE_TYPES.- Diagnostics redact API key and hash session/device/user IDs.
verify_sslhonoured for HTTPS; defaults to verifying.
Files NOT to edit casually
hacs.json— bumping thehomeassistantfloor is a breaking change for installed users; coordinate with a release.manifest.jsonversion— must be bumped together withRELEASE_NOTES.mdand a Git tag.RELEASE_NOTES.mdv0.x.0 sections — append new sections; don't rewrite history.
Release flow
- Land all changes on a feature branch.
- Bump
manifest.jsonversion + add a section toRELEASE_NOTES.md(newest first). - Open a PR on Gitea; wait for review.
- After merge, tag
vX.Y.Zon 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
/embyprefix. - Reading API responses without
isinstancechecks — Emby sometimes returnsnullor unexpected shapes for fields likeRunTimeTicks. Use the_safe_inthelper incoordinator.pyfor any numeric field coming off the wire. - Reusing a fixed
DEVICE_IDacross 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 inmedia_player.py; don't reintroduce the URL-with-key shortcut "for performance". except Exception: pass— always at least_LOGGER.debug(...)so failures are diagnosable.