Roadmap: Core Architecture (Fork)
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
Historical design record. It was written while the code still carried its old names:
jukeboxin this document is today’slauschkiste. What is implemented is described in Feature status.Fork context: diverging from upstream because the direction they’re pursuing isn’t the direction we want. Building on
future3/developas a base — swap pieces incrementally, keep something runnable after every step, ditch what turns out to be dead weight along the way rather than carrying it forward.
Fork goals, roughly in the order we’re tackling them:
-
Core architecture (this doc) — first, because everything else sits on top of it.
-
Advanced plugin system — first slice landed (backend-swap only, see below); the deeper “hook into playback/library/API layer” redesign is still being rethought from scratch, not adopted from upstream’s
future3/draft-entrypoint-pluginsdraft. See “Relationship to the other tracks” below.Status: first slice done. Goal was to decouple the jukebox from the Raspberry Pi: the default system is now a player + web UI that runs on any Linux box using its normal audio output, with everything Pi/mpd-specific opt-in.
player.backendconfig (default:local_audio, decodes via PyAV + outputs via sounddevice/PortAudio – no mpd, no external process) picks the active player backend;jukebox.player.plugindispatches to it viaimportlib.import_module, mirroring howjukebox.rfid.readeralready loaded a hardware reader module by name. Thempdbackend,rpi-gpio(GPIO-attached RFID readers), and each bundled RFID reader module now live behindpyproject.tomlextras instead of being unconditional core dependencies –uv sync --extra <name>to add one. Seepackages/jukebox/src/jukebox/player/backends/local_audio.pyandplayer/plugin.py.PlayerCoordinator(player/coordinator.py) already supported multiple named backends before this (register_backend()), so no coordinator changes were needed – this slice is entirely about selecting and installing a backend, not the dispatch mechanism itself.Not done, deliberately deferred: the FastAPI-router-per-plugin guiding principle below, third-party/out-of-tree plugins, and a VLC backend (dispatch table is extensible, just not built).
local_audioalso doesn’t implement MPD-style tag/album browsing (list_albumsetc.) – folder-triggered playback (the actual RFID use case) only.Guiding principle for the deeper redesign (agreed, not yet implemented): FastAPI becomes the one contract for everything, including plugins (backend and frontend) – not just the browser-facing bridge. A plugin registers a FastAPI router; that’s the uniform way anything (webapp, CLI, another plugin) calls into it, replacing today’s
(package, plugin, method)string addressing with a typed, documented (OpenAPI) surface. Compromise to keep this compatible with the “high performance” goal: in-process calls (e.g. an RFID card action reaching the player) invoke the router’s handler function directly, skipping HTTP/ASGI serialization – only genuinely external or out-of-process callers (browser, external/ out-of-process plugins) pay for the full HTTP round trip.Status: the webapp-facing half is done. Every
player.ctrl/misc/cardsmethod with a real implementation has a typed REST route under/api/v1/{player,settings,cards}/*(seejukebox.api.fastapi_server’sregister_player_routes/register_settings_routes/register_cards_routes) – handlers call straight into the registered object viajukebox.registry.get(), matching the “skip serialization” compromise above without needing the FastAPI-router-per-plugin machinery this principle originally envisioned (there’s no plugin system generating these yet, they’re hand-written per method). The webapp talks REST only, and the genericPOST /api/v1/rpcendpoint (plusjukebox.api.dispatch, which existed only to serve it) is removed. New functionality gets a typed route, not a generic call.Not affected: RFID card actions /
card_removal_action(jukebox.utils.bind_rpc_command/decode_and_call_rpc_command, seedocumentation/builders/rpc-commands.md) still resolve(package, plugin, method)in-process via@plugs.tag/registry.call(). That’s a config format for card actions, not an HTTP API – replacing it belongs to the plugin-system redesign. Untagging methods breaks it (tried and reverted once, see git history).Status (branch
core-plugin-contract): core and plugin contract implemented, all seven steps of core-and-plugins.md. Earlier plan for reference:Core and plugin contract – core is what makes sense on any machine (player, library, cards, settings, system info, volume, timers, jingle, input devices); platform-specific parts and integrations become opt-in plugins (bundled or external:
raspberry-pifor power/GPIO/ battery,mpd, RFID reader drivers, MQTT, card sync), both built on one contract: an@actiondeclaration generating REST route, card action and in-process call,requires-based start order, a stated threading model, andcards.yamlmigration. Draft for review: core-and-plugins.md. -
Packaging/install overhaul — install logic entirely in Python, one package + subpackages, CLI drives system setup instead of ~20 bash scripts. Done: see packaging-and-setup.md (wheels,
install.sh,jukebox setup,jukebox plugin,jukebox update). -
New name — Lauschkiste (done).
Current state (inherited from future3/develop)
Section titled “Current state (inherited from future3/develop)”- Backend core: ZMQ REP RPC server (
src/jukebox/jukebox/rpc/server.py) — single-threaded blockingrecv()loop, callsjukebox.plugs.call()synchronously per request. Also used inproc (inproc://JukeBoxRpcServer) so internal components (e.g. GPIO button handlers) call through the same path as external clients. - Browser-facing layer: Tornado (
src/jukebox/jukebox/api/server.py) bridges HTTP + WebSocket to that RPC. Already reasonably shaped — health check, RPC passthrough, library upload/CRUD, events over WebSocket via anEventBrokerfed by ZMQ SUB — but:- RPC calls run through a
ThreadPoolExecutor(max_workers=1), which serializes all RPC traffic through the browser API. Same shape as the bottleneck diagnosed in a previous session (single REP socket + unbounded MPD timeout wedges all web requests) — just moved one layer up. - Library endpoints get their own executor, also sized 1.
- No FastAPI/uvicorn anywhere yet — Tornado is the actual HTTP stack today.
- RPC calls run through a
- CLI:
run_rpc_tool.pytalks to the ZMQ REP server directly, bypassing Tornado entirely. - Plugin dispatch:
jukebox.plugs.call(package, plugin, method, args, kwargs, as_thread=...)— synchronous, with an opt-in “run in a separate thread” escape hatch used ad hoc per call site.
FastAPI + uvicorn as the one middleware layer between browser (HTTP + WebSocket), CLI, and internal
plugin calls. Drop the ZMQ REP hop where it’s pure indirection; decide separately whether ZMQ pub/sub is
still the right tool for the event/broadcast side once Tornado (which needed the ZMQStream bridge) is
gone.
Why incremental, not big-bang
Section titled “Why incremental, not big-bang”Agreed approach: build on future3/develop, swap parts one at a time, always have something that runs.
Expect to need cleanup passes between steps rather than one clean rewrite.
Proposed steps
Section titled “Proposed steps”-
Inventory concurrency needs first. Which plugin calls are safe to run in parallel vs. must stay serialized against shared hardware state (audio playback, GPIO, MPD)? This decides whether “high performance” comes from more executor workers, from making
plugs.callasync-aware, or both — the real design question here, not just a mechanical framework swap.Finding:
jukebox.plugs.call()already serializes every dispatched call behind one module-levelthreading.RLock(_lock_moduleinsrc/jukebox/jukebox/plugs.py), independent of which plugin/component is being called. So theThreadPoolExecutor(max_workers=1)sizing in the Tornado bridge isn’t the only serialization point — even with more HTTP-layer concurrency, all plugin calls still queue up behind this single lock today. Real parallelism (e.g. a library scan running while playback controls stay responsive) requires replacing this one-lock-for-everything model with something per-component/per-resource — not yet designed, tracked as an open decision below. -
Stand up FastAPI alongside Tornado, reimplementing health/RPC/events first, still backed by the existing ZMQ REP client. Proves the swap without touching core dispatch.
Status: done.
src/jukebox/jukebox/api/fastapi_server.py(FastApiServer,create_app) reimplements health/RPC/events on FastAPI + uvicorn, running independently of and side by side withjukebox.api.server.ApiServer— nothing wires it into the daemon yet.EventBrokerand the events subscription-command parsing (parse_subscription_command) were generalized out of their Tornado-specific bits so both bridges share the same code. The RPC executor here uses 4 workers instead of Tornado’s 1, sinceplugs.call()’s own lock (see finding above) already provides the real serialization — more HTTP-layer workers just avoid queuing unrelated requests (e.g./health) behind a slow plugin call. Tests:test/api/test_fastapi_server.py. -
Port the library endpoints (upload, folders, entries, refresh — the streaming/multipart-heavy ones) to FastAPI.
Status: done.
create_app()infastapi_server.pynow also servesPUT /api/v1/library/files,POST /api/v1/library/folders,GET/DELETE /api/v1/library/entries, andPOST /api/v1/library/refresh, backed by the same transport-neutraljukebox.library.MusicLibrary/LibraryErrorthe Tornado bridge uses, so behavior (status codes, errorcode/messageshape, path/type validation) matches exactly. Uploads stream via Starlette’srequest.stream()straight intoUploadSession.write()(no full-body buffering, same as the Tornado version), executed through a dedicated single-worker libraryThreadPoolExecutorkept separate from the RPC executor. Added a_read_limited_body()helper so the RPC and library-JSON endpoints enforce the same 1 MiB streaming body cap (MAX_MESSAGE_SIZE) Tornado’sstream_request_bodyhandlers had – easy to miss on FastAPI sinceawait request.body()alone doesn’t cap size. Tests ported 1:1 fromtest/api/test_server.py’s library test class intotest/api/test_fastapi_server.py. -
Port the WebSocket event broker to FastAPI’s WebSocket support; decide fate of the ZMQ pub/sub hop underneath (keep it, or replace with in-process asyncio queues now that everything’s one process).
Status: done, but see the reversed decision below. Was already implemented as part of step 2 (
_handle_events_websocketinfastapi_server.py); what was left was the decision and one real gap, both closed now:ZMQ pub/sub stays.Reversed, see “Simplify away ZMQ and nginx” below. Original reasoning was thatjukebox.publishingis a system-wide bus, not Tornado-specific, so nothing to replace. Still true, but doesn’t mean it needs ZMQ specifically – for a single-process, single-Pi app with a handful of local browser clients, ZMQ solves a distributed-systems problem (many independent processes, high throughput) that doesn’t exist here anymore now that dispatch is in-process.FastApiServerbridges it viazmq.asynciotoday; slated to become a plainasyncio.Queuebroadcast instead.- Gap found and fixed: cross-origin WebSocket rejection. Tornado’s
WebSocketHandlerrejects cross-origin handshakes by default; Starlette/FastAPI don’t do this at all. Without a fix, any page in a victim’s browser could open a WebSocket to this API and read every published topic (classic cross-site WebSocket hijacking) – worse than a theoretical gap, sinceapi.bind_addressdefaults to loopback but nginx proxies it out to the LAN regardless. Added_is_same_origin()(same-origin check against theHostheader, mirroring Tornado’s defaultcheck_origin) tofastapi_server.py. Tests:test_events_websocket_rejects_cross_origin_handshake/..._allows_same_origin_and_no_origin_header.
-
Reassess the ZMQ REP/REQ layer once FastAPI fully replaces Tornado — does it still earn its keep, or can CLI and webapp both call the same in-process FastAPI app / plugin dispatch directly and drop a hop?
Status: done. Both remaining ZMQ consumers are gone: the interactive Python RPC CLI (
run_rpc_tool.py) and, once it turned out there was a second one, the C CLI client (src/cli_client/pbc.c) – neither was migrated, both were removed outright (“we’ll find another solution for that later,” not designed yet). With no consumers left,jukebox.rpc.server.RpcServer,jukebox.rpc.client.RpcClient, and thepyzmqdependency were deleted too. (process_request, later moved tojukebox.api.dispatch, served the FastAPI/api/v1/rpchandler until that was removed too.) There is currently no CLI/RPC tool at all; whatever replaces it will presumably be built against the FastAPI HTTP endpoint from the start, per the “everything through FastAPI” principle (see “Advanced plugin system” above).jukebox.daemon’s main thread now blocks onself.api_server.join()instead ofself.rpc_server.run()–FastApiServer(athreading.Thread) is the only long-running server left, so joining it is what keeps the process alive untilexit_gracefully()terminates it. -
Remove the Tornado dependency, and check what else assumed it: webapp nginx config (
resources/default-settings/nginx.default),installation/routines/setup_jukebox_webapp.sh, ports referenced in config defaults.Status: partially done. The Tornado HTTP bridge is gone:
jukebox.api.serverdeleted,jukebox.daemonnow startsFastApiServerinstead ofApiServer, on the exact sameapi.bind_address/api.portconfig keys and default port (5556) – so nginx, the webapp’s Vite dev proxy, anddocker-compose.ymlneeded zero changes. Verified end-to-end (not just unit tests): started the real component-wired daemon pieces plusFastApiServerin a throwaway config dir, hit/api/v1/healthand/api/v1/rpcover real HTTP, got a correct RPC response, shut down cleanly.Update: the
tornadopackage is gone now too. It was still a dependency becausejukebox.publishing.server.PublishServer(the core pub/sub proxy, not the browser bridge – the actual internal message bus everything uses) importedzmq.eventloop.ioloop.IOLoop, which unconditionally doesfrom tornado.ioloop import IOLoopunder the hood (verified by uninstalling tornado and watchingPublishServerfail to import). ReplacingPublishServerwith the in-processEventBus(see “Simplify away ZMQ and nginx” below) removed that import entirely –tornadois no longer inpyproject.toml. -
Measure before/after. “High performance” needs a number, not a vibe — concurrent library scans, cover-art fetches, and RPC calls during active playback are the realistic stress cases.
Simplify away ZMQ and nginx
Section titled “Simplify away ZMQ and nginx”Decision (discussed, not yet implemented): both are inherited complexity from the upstream project’s original design, not something this fork chose, and neither pulls its weight for a single-process app on a single Pi serving a handful of local browser clients over a home LAN:
- ZMQ (
pyzmq) solves distributed-systems problems – many independent processes, potentially high throughput – that no longer exist here. Plugin dispatch is already in-process (see “Old plugin system removed” below), the FastAPI RPC handler already callsprocess_requestdirectly rather than through ZMQ REP, and the pub/sub side (status updates, a handful of events per second even with several browser tabs open) is comfortably within what a plainasyncio.Queuebroadcast handles. Keeping ZMQ only makes sense if this needs to become a genuinely distributed system later (components running as separate processes/containers, possibly relevant if the later plugin-system redesign goes that direction) – not a given, not worth carrying the complexity for speculatively. - nginx exists to reverse-proxy
/api/to the (loopback-bound) FastAPI process and serve the built webapp’s static files. FastAPI/uvicorn can serve static files directly (StaticFiles) and bind wherever needed – nginx’s real strengths (TLS termination, high-concurrency static serving, buffering under load) aren’t relevant at Pi-jukebox scale.
ZMQ pub/sub: done
Section titled “ZMQ pub/sub: done”jukebox.publishing no longer uses ZMQ at all. Replaced with jukebox.publishing.bus.EventBus: a
plain thread-safe last-value-cache + subscriber registry (lock + dict + set, no reactor loop, no
background thread). publish() is called synchronously from whatever thread published (RFID
reader thread, timer threads, the daemon thread, …) and dispatches directly to registered
callbacks – for the FastAPI bridge, that callback (EventBroker.publish) is itself already
thread-safe via asyncio.run_coroutine_threadsafe (it had to be, to deliver ZMQ-sourced events
into the event loop before; now it does the same job one hop shorter).
jukebox.publishing.get_publisher().send/resend/close_server – the call-site API every component
uses – is unchanged, so no component code needed to change. Publisher is no longer thread-local
(the old “one instance per thread, ZMQ sockets aren’t thread-safe” rule doesn’t apply to a
lock-protected dict): one shared instance is enough now.
Turned out to also fully drop the tornado dependency as a side effect: it was only needed
because jukebox.publishing.server.PublishServer imported zmq.eventloop.ioloop.IOLoop, which
unconditionally pulled in tornado.ioloop. Deleting that whole reactor-based server (see above)
removed the last thing requiring it – verified with uv sync after removing it from
pyproject.toml and running the full test suite.
run_publicity_sniffer.py (the one external-process consumer, previously a raw ZMQ TCP
subscriber) now connects to /api/v1/events as a plain WebSocket client (websockets package,
added to [project.dependencies]) – verified against a real running FastApiServer +
components.publishing, not just unit-tested. New tests: test/common/test_bus.py, including
one that reproduces and verifies the fix for the original code’s “log handler republishes through
the bus it’s logging an error for” recursion hazard (see common/loggingext.py’s PubStreamHandler)
– EventBus.publish() caps that at one extra level via a thread-local re-entrancy depth guard
instead of the old code’s thread-identity/recursion-counter check.
Also removed the now-meaningless publishing.tcp_port config key and the 5558 port references
that went with it (jukebox.default.yaml, docker-compose.yml, both Dockerfiles’ EXPOSE).
nginx: done
Section titled “nginx: done”FastAPI now serves the webapp’s static build, /logs, and a couple of fallback pages directly
(jukebox.api.webapp_static.register_webapp_routes, mounted last in create_app() so it never
shadows /api/v1/*). Deliberately ported nginx’s actual behavior rather than inventing new
behavior – e.g. still no SPA deep-link fallback to index.html for unknown paths, since
nginx.default’s try_files $uri $uri/ =404 didn’t do that either.
index.html(/and/index.html) served withCache-Control: no-store; falls back to a “bundle is missing” page if the build hasn’t been installed, matching the old@buildwebuierror page.build/static/*mounted via Starlette’sStaticFiles; any other root-level build file (favicon, manifest, locales/*, …) served through a catch-all with directory-traversal protection (resolves the candidate path and checks it’s still insidebuild_dir– tested explicitly, seetest_catch_all_rejects_path_traversal_outside_build_dir)./logsand/logs/<file>: a small directory listing + flat file server overshared/logs(nginx’sautoindex on), filename-only path validation (no traversal via/logs/../secret).- Unknown paths get a generic 404 page (nginx’s
error_page 404 = /404.html).
Bind address changed from 127.0.0.1 to 0.0.0.0 (api.bind_address in
jukebox.default.yaml): nginx used to be the only thing reachable from the LAN, reverse-proxying
to a loopback-only browser bridge. There’s no separate reverse proxy anymore, so the FastAPI server
itself needs to be reachable directly – this is the one meaningful behavior change from the
nginx-based setup, not just a refactor, so it’s called out explicitly here rather than buried in a
commit message.
Installer changes: installation/routines/setup_jukebox_webapp.sh no longer installs/configures
nginx, just downloads the webapp bundle and verifies the build directory exists.
prepare_dependencies.sh no longer adds nginx/removes apache2.
installation/routines/setup_kiosk_mode.sh’s Chromium kiosk URL changed from http://localhost
(port 80, nginx) to http://localhost:5556 (FastAPI). Deleted
resources/default-settings/nginx.default, resources/html/404.html,
resources/html/runbuildui.html (content now inlined in webapp_static.py; nothing else
referenced them). Verified end-to-end: started a real FastApiServer against this repo’s actual
src/webapp/build and shared/logs, hit /, /favicon.ico, /logs, an unknown path (404), and
/api/v1/health over real HTTP.
Not verified here (no real Pi/systemd/apt environment available): the installer script changes
themselves. Same caveat as the earlier uv installer migration – worth a smoke test before
relying on them.
ZMQ REP/REQ, run_rpc_tool.py, and the C CLI client: done (removed, not migrated)
Section titled “ZMQ REP/REQ, run_rpc_tool.py, and the C CLI client: done (removed, not migrated)”ZeroMQ is gone from the entire project now – the last two consumers were removed outright rather than migrated onto FastAPI, since neither is needed right now and “we’ll find another solution for that later”:
run_rpc_tool.py(interactive Python RPC CLI) and its wrappertools/run_rpc_tool.sh.src/cli_client/pbc.c(a separate C CLI client) – found while removingrun_rpc_tool.py: earlier notes here claimed the Python CLI was the only remaining ZMQ consumer; that was wrong, this C client was a second one, missed on the first pass.
With both gone, jukebox.rpc.server.RpcServer, jukebox.rpc.client.RpcClient, ci/installation/ zmq_smoke.py, and the pyzmq dependency were deleted too (verified nothing else imports zmq
anywhere in src/ or test/). Also cleaned up what existed only to support ZMQ: the
libzmq5/python3-zmq apt packages (packages-core.txt, both Dockerfiles), the dedicated
run_raspbian_armv6_zmq CI job that smoke-tested ZMQ on armv6, libczmq-dev from the main CI
workflow’s apt install, and the rpc.tcp_port config key. The HTTP /api/v1/rpc endpoint and its
process_request dispatcher were removed later, once the webapp was fully on typed REST routes.
There is currently no RPC/CLI tool of any kind. Whatever replaces it should be built against the FastAPI HTTP endpoint from the start, consistent with the “everything through FastAPI” principle (see “Advanced plugin system” above) – not scoped or designed yet.
Dev tooling migrated to uv + bam
Section titled “Dev tooling migrated to uv + bam”Orthogonal to the architecture work above, but done alongside it: Python dev tooling moved from
pip + requirements-dev.txt + flake8 + hand-rolled run_*.sh wrapper scripts to
uv (package manager, pyproject.toml) +
bam (content-hash-cached task runner, bam.yaml) + ruff +
pyright, following the conventions of /home/stefan/Projekte/dev/python-uv-workspace-template
(a Copier template for fresh CLI projects – not run directly here since this is an existing
multi-language monorepo, not a fresh scaffold, but its tool choices and pyproject.toml/bam.yaml
shape were adopted as-is).
run_pytest.sh / run_flake8.sh / run_docgeneration.sh / run_markdownlint.sh / run_jukebox.sh
are gone, replaced by bam lint / bam test / bam docs / bam markdownlint / uv run python src/jukebox/run_jukebox.py. ruff format (~70 files would change) and pyright (178 pre-existing
errors in basic mode) are wired up as bam tasks but deliberately non-blocking (|| true) since the
codebase has never been run through either – same “no formatting baseline commit yet” situation
upstream already flagged in roadmap-plugins-and-packaging.md for their own (not-merged-here) work.
Update: requirements.txt / requirements-excluded.txt are gone too now. The real Pi installer
(migrate_to_cli/installation/routines/setup_jukebox_core.sh) and both Dockerfiles were migrated to uv sync
against pyproject.toml directly (bootstrapping uv itself via the official install script if not
already present). One gotcha caught by actually building docker/Dockerfile.lauschkiste and importing
zmq inside the built image: PyZMQ must keep coming from the python3-zmq apt package (via
--system-site-packages, using the system libzmq) rather than a PyPI wheel – uv sync --no-install-package pyzmq on all three call sites keeps that true, otherwise every install would
silently reintroduce the exact “draft-enabled PyZMQ shadowing the system package” problem the
installer already has one-time cleanup logic for. (PyZMQ has since been removed entirely.)
armv7 verified under QEMU (no real Pi hardware yet): docker/armv7/lauschkiste.Dockerfile (rebased
from buster – Python 3.7, below requires-python – to trixie) builds and jukebox run serves
the API on armv7l. The installer’s _jukebox_core_install_python_requirements was run in the
ci/ci-debian.Dockerfile base (Raspbian trixie repos, packages-core.txt) on armv7l as user pi,
followed by the service’s ExecStart in a minimal systemd-like environment. That caught three
real bugs, now fixed: cffi (via sounddevice) has no armv7l wheel and failed to build without
libffi-dev (added, plus build-essential since evdev never has wheels); a failed uv sync did
not abort the installer (now exit_on_error); and the service ran uv run, but uv lives in
~/.local/bin, which isn’t on the systemd user manager’s PATH (now calls .venv/bin/jukebox
directly). Source builds under QEMU take ~1-2 min. Still open: the service Requires=mpd.service
and the installer always sets up mpd, although local_audio is the default backend. This closes out the last piece
of Track B (packaging/install) that isn’t a full install/update rewrite – runtime dependency
installation is uv-based everywhere now, the rest of Track B (bundling the webapp as package data,
resolving checkout-relative paths, etc.) is still open.
Old plugin system removed
Section titled “Old plugin system removed”The old dynamic, config-driven plugin system (jukebox.plugs, @plugs.register /
@plugs.initialize / @plugs.finalize / @plugs.atexit decorators, one lock serializing every
call) has been removed entirely, not just deprioritized. Replaced by jukebox.registry: a minimal
explicit call registry with the same (package, plugin, method) addressing (so the webapp’s RPC
call shape didn’t need to change) but no dynamic loading, no decorator magic, and no shared global
lock – jukebox.daemon.run() now wires up each component directly (register() / start()
calls), and each component is responsible for its own thread-safety.
Everything not immediately essential was stripped along with it: gpio, mqtt, volume,
timers, battery_monitor, controls, jingle, hostif, synchronisation are gone from
src/jukebox/components, deleted rather than archived (recoverable from git history if needed).
Only player (MPD) and rfid (reader + card database) survived, rewired onto jukebox.registry.
publishing and misc also survived (framework-adjacent, not really “plugins”). This was a
deliberate “draft without plugins/components” cut, not an oversight – the removed pieces come
back later as newly designed components, not restored as-is.
This directly supersedes the “merge future3/draft-entrypoint-plugins later” framing further up
in this doc: there is no old plugin system left to extend at this point.
Relationship to the other fork tracks
Section titled “Relationship to the other fork tracks”- Plugin system: first slice (backend-swap for player/RFID hardware, config + optional-
dependency selection) is done, see “Advanced plugin system” above. Deliberately not pulling in
upstream’s
future3/draft-entrypoint-pluginsdraft (two-tier core/user-plugin loading, config-toggled entry points) for anything deeper — the goal there is more powerful plugins than that model supports — how deep a plugin can reach into playback, library, and the API layer is a question to design once this core architecture work has settled, not before. Revisit step 5 above (plugin dispatch call sites) with that redesign in mind rather than migrating the old model. - Packaging/installer: mostly orthogonal, but step 6 above (nginx/service file changes) overlaps with
the “ship resources as package data” work already scoped in
documentation/developers/roadmap-plugins-and-packaging.md(Track B) — sequence so the same install routine isn’t edited twice for unrelated reasons. - Naming: deferred, no dependency.
Open decisions
Section titled “Open decisions”Resolved: ZMQ pub/sub for events (replaced by in-process EventBus, see “Simplify away ZMQ and
nginx”), CLI/ZMQ REP (removed entirely, no replacement designed yet, see “ZMQ REP/REQ…” above),
and jukebox.plugs’s single global lock (replaced by jukebox.registry, which has no lock at
all – each component is responsible for its own thread-safety, see “Old plugin system removed”).
Still open:
- Real per-component/per-resource concurrency (e.g. a library scan not blocking playback controls)
isn’t designed –
jukebox.registry.call()has no shared lock anymore, but nothing adds deliberate concurrency control either; components are just trusted to be thread-safe on their own terms today. This is the actual “high performance” work – the FastAPI swap alone didn’t achieve it (see step 1 and step 7 further up). - Does
/api/v1get replaced in place, or versioned to/api/v2at some point? (Currently: no need has come up.)