Skip to content

Jellyfin Integration

Choose a tag to compare

@tousled tousled released this 27 Jun 10:27
· 23 commits to develop since this release
e6b40dc

[1.1.0] - 2026-06-26

Added

  • Path mappings, detected libraries, the library-filter preference, and the monitored device now persist
    independently per media-server provider (media_servers.providers[type].playback), instead of one shared block
    used by whichever provider happened to be active. Switching between Emby and Jellyfin now shows each provider's
    own mappings/libraries/device, and switching back restores them exactly as left — the same guarantee already
    shipped for auth in 1.0.5. Includes an automatic, one-time migration of existing installs' config.
  • Added toast feedback for the Media Server provider switch: a confirmation when the switch lands on an
    already-authorized provider (previously silent — the only feedback was a transient "connecting…" state), and a
    distinct message when the switch succeeds but the target provider still needs login.
  • Added a provider-branded Media Server hero mark that surfaces the selected Emby/Jellyfin provider without repeating
    the server URL, user, or readiness details already shown in the configuration panels. The mark stays visible as
    setup context while the provider is pending authorization, but renders muted until the provider is authorized.
  • Updated the Media Server installation screenshots to show provider selection, the Jellyfin hero mark, and the muted
    provider state shown before authorization.
  • Added a compact active-provider badge to Media Paths so the detected-library and route-mapping workflow keeps the
    configured Emby/Jellyfin context visible without overloading the refresh action.
  • Added a shared, provider-neutral find_controlling_session_id policy (media_servers/common/models.py) that
    resolves which client session actually issued a remote Play command, used by both Emby and Jellyfin.
  • Instrumented two previously invisible OPPO startup phases — mounting the network share (mount_oppo_network_share,
    includes the device-list-ready wait) and waiting for the OPPO to actually report active playback
    (wait_for_oppo_playback_active, the QPL telnet poll after launching the file) — into the existing
    PlaybackStartupTimer. Before this, a slow OPPO response in either phase showed up as an unexplained gap between
    the named steps and the printed total in "Playback startup timing summary," with no way to tell from the log
    alone whether the OPPO, the TV/AV output switch, or something else caused it. No behavior change: both are
    optional, timer-only wrapping.
  • Added a first-run modal offering to import a legacy XNOPPO config.json (the predecessor project this app
    replaces) on a fresh install with nothing configured yet, separate from the existing "legacy config already on
    disk" migration modal. The user picks the old config.json file, HCC migrates AV/TV/OPPO/playback settings the
    same way the existing migration pipeline already does, moves the Emby server URL into the new multi-provider
    config shape, and makes a best-effort live login with the file's username/password to obtain a real access token
    — if that login fails (e.g. the Emby server isn't reachable yet), the provider is just left unauthenticated, same
    as any freshly added provider, and the existing sidebar readiness indicator shows it needs attention.

Fixed

  • Fixed the Media Server setup screen treating saved provider data as live success when the active Emby/Jellyfin server
    is unavailable. The form now renders from saved config immediately instead of waiting for slow device/library
    discovery timeouts, failed device discovery is shown explicitly, affected panels no longer stay green, and the
    sidebar setup dot is downgraded while the current provider cannot be reached.
  • Fixed the monitored-device selector showing blank rows when a provider returned devices in a non-canonical shape or
    without display labels. Device options are now normalized defensively and invisible rows are filtered out.
  • Fixed media-server provider switches activating/restarting the runtime listener before the target provider was usable.
    Selecting an unconfigured provider now saves it as a draft only; expired or unreachable targets leave the current
    websocket listener untouched; configured targets are checked before any active-playback confirmation or listener swap.
  • Fixed Jellyfin playback-startup notifications never being sent at all. Jellyfin's Play websocket message has the
    same gap Emby's already-shipped fix (1.0.5) covers — it never identifies the controlling client's own session,
    only the bridge's target session — but the fix was never ported to Jellyfin. Every notification silently no-opped
    with "no active source session is available."
  • Fixed Jellyfin source-client cleanup at playback finish using the bridge target session when Jellyfin's Play
    command only provided Id. HCC now treats Id as the target session, resolves the real controlling client
    session, and sends the same double Stop used for Emby so the Jellyfin app clears its stale playback screen when
    the OPPO is stopped from the remote.
  • Fixed Jellyfin Web/App remote-control screens remaining on a frozen playback view after OPPO remote Stop by applying
    the same best-effort double Stop delivery used for Emby to Jellyfin's source client session.
  • Fixed Jellyfin multi-client cleanup when the same user has the web UI and mobile app open for the same HCC playback.
    HCC now resolves active Jellyfin sessions for that user, excludes its own bridge session, skips only sessions that
    explicitly report a different now-playing item, and sends best-effort double Stop to each stale client instead of
    only the single session that originally issued Play.
  • Fixed JellyfinClient.send_session_message sending Text/Header/TimeoutMs as query-string parameters (Emby's
    shape) instead of the JSON request body Jellyfin's server actually requires, and sending an empty data={} body
    that omitted the Content-Type header entirely, which Jellyfin's server rejects with 415 Unsupported Media Type. Confirmed against a real Jellyfin server's own error responses.
  • Fixed JellyfinClient.get_user_views calling a route that does not exist (/Users/{userId}/Views). The real
    route is GET /UserViews?userId=.... Jellyfin library detection had likely never worked on any real install —
    masked by the ModuleMediaServerSetupService bug below, which silently absorbed the resulting failure.
  • Fixed ModuleMediaServerSetupService.load_devices/load_libraries/load_selectable_folders discarding the
    provider module's actual return value and always returning the pre-call config unchanged. Freshly detected
    libraries, devices, and path mappings were computed correctly and then thrown away before reaching the UI, for
    both Emby and Jellyfin.
  • Fixed MediaServerLibrary reading every library as inactive for any install whose playback.libraries predates
    this value object (every install before this provider-boundary refactor) — those entries use raw
    Id/Name/Active (capitalized) instead of the model's id/name/active. A model validator now normalizes
    the legacy shape instead of silently defaulting active to False.
  • find_controlling_session_id's Jellyfin-side session lookup now narrows server-side via the confirmed
    controllableByUserId query parameter instead of fetching every active session on the server for every single
    Play command.
  • Fixed the existing "legacy config found" migration silently discarding emby_server/user_name/user_password
    from an XNOPPO-era flat config.json instead of migrating them — those keys were already listed as legacy
    (so they got removed) but no step ever moved the server URL anywhere first. TV_KEY/TV_DeviceName (no current
    equivalent — LG pairing now goes through a separate key store) are now dropped instead of lingering as orphaned
    fields.
  • Fixed the Docker image being unable to complete a truly fresh install (no existing config volume at all):
    the multi-stage build refactor dropped config.example.json from the final runtime stage, so
    ensure_config_exists() had nothing to seed config.json from and crashed on every container start.
  • Fixed secrets.json carrying a stale, empty single-provider media_server stub forever. It was seeded by
    default for every install before the multi-provider media_servers.providers.* shape existed, and was only ever
    cleaned up when the public config also had real legacy media_server data — which an XNOPPO-era flat config
    never has. It's no longer seeded for new installs, and existing installs self-heal it on the next config save or
    container restart.
  • Fixed the "include pre-release versions" checkbox on the Diagnostics page never actually persisting — it was a
    local-only UI ref, never read from saved config on load and never saved when toggled, so it silently reset to
    off on every page reload. As a result the version check always ran with pre-releases excluded no matter how the
    checkbox looked, even though the backend filtering logic itself (fixed in 1.1.0-rc.2) was already correct. The
    checkbox now loads its saved state and persists immediately on toggle.
  • Fixed compose.yaml (the file users/NAS deployments actually run) carrying both image: and build:. Several
    Docker GUIs (Portainer's "deploy stack from Git" pointed at a tag, among others) build from the compose file's
    build: section instead of pulling the named image — silently producing a locally-rebuilt image stamped
    0.0.0.dev0 (the SETUPTOOLS_SCM_PRETEND_VERSION build-arg's fallback, since nothing sets HCC_VERSION in that
    flow) instead of the real, correctly-versioned image from the registry. compose.yaml is now pull-only; the
    build: section moved to a local-only, untracked override file for development.

Changed

  • Documented that Jellyfin device and library discovery (GET /Devices, GET /Library/VirtualFolders) requires an
    administrator-level Jellyfin account — both endpoints are elevation-gated server-side. See INSTALL.md /
    INSTALL.en.md's FAQ.
  • README.md/README.en.md and INSTALL.md/INSTALL.en.md updated to reflect Jellyfin as a shipped, supported
    media-server provider rather than a roadmap item.
  • Documented installing/updating via Portainer (or similarly-capable web UIs) in INSTALL.md/INSTALL.en.md's
    Docker Compose section — paste compose.yaml into the stack, then pin the running version via the stack's
    HCC_VERSION environment variable.
  • Fixed INSTALL.md/INSTALL.en.md presenting Emby as the only supported media server in the requirements section
    — it never reflected Jellyfin support there, even though Jellyfin has been a shipped provider since 1.1.0-rc.1.
  • All backend routes moved from /api/* to /api/v1/*, ahead of splitting web/api_app.py's 800+ lines into one
    router per domain. The frontend (api/index.js's base URL, plus the few hardcoded now-playing image/config-bootstrap
    paths) moved with it; the dev proxy in vite.config.js still matches on the /api prefix unchanged.