Repository navigation
feat(core): generic accounts, per-account user state and a provider-neutral integration API - #49
Open
tajanthind wants to merge 1 commit into
Open
tajanthind wants to merge 1 commit into
tajanthind wants to merge 1 commit into
Conversation
…eutral integration API Adds the reusable primitives an external server/client integration needs, without implementing any client protocol in core. Accounts and authentication - Account-scoped API tokens (models.ApiToken): 256-bit random, `fnack_` prefixed, stored ONLY as a SHA-256 hash, individually revocable, optional expiry, last-used tracking, prefix kept for display. The guard now accepts `Authorization: Bearer fnack_…` / `X-API-Token` and authenticates the request AS that account (session → token → machine key → auth_provider). Passwords stay scrypt-hashed; sessions unchanged. - Token issue/list/revoke endpoints; revocation is account-scoped. Per-account user state (services/user_state.py, models.py) - First-class, explicitly account-scoped persistence for: playlists and playlist items, favorites/stars (artist|album|track), ratings (1..5), bookmarks, scrobble/play history, and the saved play queue with its cursor. - Every read/write filters by the authenticated account; another account's object is reported as NotFound so existence is never leaked. - References are fnack's INTERNAL artist/album/track ids, validated before persistence. The id columns deliberately carry no FK: hard-deleting a library row must not cascade away or block user state. - Playlist/queue ordering is deterministic: positions are unique per owner, compacted to 0..n-1 after every mutation, and reordering runs as a two-phase move (bulk shift out of the 0..n-1 range, then a single CASE assignment) so no two rows can transiently share a position — including autoflush. - Library churn is handled: state rows keep a snapshot (title/artist/album/ duration/isrc) so they stay readable (`available: false`) when the track is gone, and are re-attached by ISRC when the recording is re-created under a new internal id (provider replacement). Wired into both hard-delete paths (artist delete, discography-sync prune) plus relink after sync. Library queries (services/library_query.py) - Provider-neutral artist/album/track search, filters, whitelisted sorting, direct id lookup and counts; SQL LIMIT/OFFSET + COUNT so an integration never loads the library into memory. Provider identities stay opaque and separately scoped. Integration API (services/integration_api.py) - Versioned JSON boundary at /api/integration/v1 with meta discovery, token exchange, library endpoints and the full user-state surface. It is the integration contract — not the SQLAlchemy models. `GET /meta` and `POST /auth/token` are the only public endpoints; everything else requires an account identity, and no endpoint accepts an account id from the caller. The machine API key does not authenticate here (it carries no account). Migration - The nine new tables are created additively and idempotently at startup (services/schema_migrations.py), so existing installations keep every library row and account. Terminology - Core carries no client/product-specific vocabulary; legacy client-specific wording in plugin docstrings was generalised. Tests (tests/architecture/test_user_state_and_integration_api.py) - two accounts with fully isolated state (+ cross-account refusal for every surface), playlist CRUD/insert/reorder/compaction and uniqueness, favorites, ratings, bookmarks, scrobble history, saved queue, hard-deletion of a referenced track + ISRC re-link, provider identity changes leaving state intact, provider-neutrality of the new services, SQL-paginated library queries, HTTP end-to-end (token auth, session parity, machine-key refusal, revocation) and the migration on an existing database. - Verified: architecture suite 28/28, smoke test passes, and a real-boot E2E against the full app.py (throwaway config tree) exercised 27 checks of the boundary end to end.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the generic core infrastructure an external media server needs, without implementing any client protocol in core.
fnack core: accounts + library + generic user state + stable ids
External server (yours): its own client protocol
Clients: connect to your server, never to core
1. Accounts & authentication
ApiTokenmodel: 256-bit randomfnack_…tokens, stored only as SHA-256 hashes (prefix kept for display), individually revocable, optional expiry,last_used_ataudit.Authorization: Bearer fnack_…/X-API-Tokenand authenticates the request as that account. Precedence: session → account token → machine key → auth_provider. Passwords stay scrypt-hashed; sessions unchanged.2. Per-account user state (
services/user_state.py,models.py)New account-scoped tables:
playlists,playlist_items,favorites,ratings,bookmarks,scrobbles,play_queues,play_queue_entries(+api_tokens).current_index/position_ms/changed_by.3. Stable references (no provider coupling)
external_ids are never primary references and never required to resolve saved items.4. Concurrency & correctness
0..n-1; reordering is a two-phase move (bulk shift, then a singleCASEassignment) so no two rows can transiently collide — including SQLAlchemy autoflush.PUT …/itemsrefuses anything but the exact item set (no silent reordering).available: false) when the referenced track is hard-deleted; re-linked by ISRC when the recording is re-created under a new id. Hooked into both hard-delete paths (artist delete, sync prune) + relink after sync.5. Library query APIs (
services/library_query.py)Provider-neutral artist/album/track search, filters, whitelisted sorting, SQL pagination (
LIMIT/OFFSET+COUNT,has_more, limit ≤ 500), direct id lookup, counts. No loading the library into Python; provider identities stay opaque/separately scoped.6. Core API boundary (
services/integration_api.py)Versioned JSON API at
/api/integration/v1— the integration contract (not the ORM models):metadiscovery, token exchange/管理,/me, library endpoints, and the full user-state surface. OnlyGET /metaandPOST /auth/tokenare public; everything else requires an account identity, and no endpoint accepts an account id from the caller. The machine API key does not authenticate here (no account ⇒ can't honour isolation).7. Migration
Nine new tables are created additively and idempotently at startup (
services/schema_migrations.py); existing library rows and accounts are untouched (asserted by a test that drops the new tables from a populated DB and runs the migration).8. Docs
docs/integration-api.md(full reference: auth, endpoints, ordering/isolation/deletion semantics, curl examples) + an architecture section. Core docs contain no client/product-specific vocabulary.Tests —
tests/architecture/test_user_state_and_integration_api.pyTwo users with fully isolated state and cross-account refusal on every surface (service + HTTP) · playlist CRUD/insert/reorder/compaction/uniqueness · favorites · ratings · bookmarks · scrobble history · saved queue · hard-deletion of a referenced track + ISRC re-link · provider change leaving state intact + provider-neutrality of the new services · SQL paginated library queries · token hashing/revocation/session parity/machine-key refusal · migration on an existing install · no-client-terminology guard.
Verification
app.py(throwaway config tree, never the live container): 27/27 checks — setup → token exchange → library pagination → playlist ordering → favorites/ratings/bookmarks/scrobbles/queue → cross-account isolation → machine-key refusal → token revocation.Not in scope (per request)
No server protocol implementation, no client-specific logic, no Arpeggi/AudioMuse modelling, no version bump, no tag, no merge, and no changes to the external server.