Missing-file lifecycle end to end, UPnP stall recovery, Android browse parity, Flutter client removed #126
-2
@@ -11,8 +11,6 @@ import javax.inject.Singleton
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-through accessor for the admin cross-user requests queue.
|
* Read-through accessor for the admin cross-user requests queue.
|
||||||
* Mirrors `flutter_client/lib/admin/admin_providers.dart`'s
|
|
||||||
* AdminRequestsController.
|
|
||||||
*
|
*
|
||||||
* No Room caching — admin actions are infrequent and don't benefit
|
* No Room caching — admin actions are infrequent and don't benefit
|
||||||
* from offline scrollback. `approve` and `reject` fire direct REST
|
* from offline scrollback. `approve` and `reject` fire direct REST
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import java.io.IOException
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps server error codes (and common transport failures) to
|
* Maps server error codes (and common transport failures) to
|
||||||
* friendly, sentence-case copy. Mirrors
|
* friendly, sentence-case copy.
|
||||||
* `flutter_client/assets/error-copy.json` + `error_copy.dart`.
|
|
||||||
*
|
*
|
||||||
* Server errors are `{"error":{"code":"...","message":"..."}}`.
|
* Server errors are `{"error":{"code":"...","message":"..."}}`.
|
||||||
* [fromThrowable] pulls the code out of a Retrofit [HttpException]'s
|
* [fromThrowable] pulls the code out of a Retrofit [HttpException]'s
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import retrofit2.http.POST
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/admin/invites`. Mirrors
|
* Retrofit interface for `/api/admin/invites`.
|
||||||
* `flutter_client/lib/api/endpoints/admin_invites.dart`.
|
|
||||||
*
|
*
|
||||||
* Server TTL is hardcoded at 24h; the only configurable field is the
|
* Server TTL is hardcoded at 24h; the only configurable field is the
|
||||||
* optional `note` on create.
|
* optional `note` on create.
|
||||||
|
|||||||
+1
-2
@@ -6,8 +6,7 @@ import retrofit2.http.POST
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/admin/quarantine`. Mirrors
|
* Retrofit interface for `/api/admin/quarantine`.
|
||||||
* `flutter_client/lib/api/endpoints/admin_quarantine.dart`.
|
|
||||||
*
|
*
|
||||||
* Three resolution endpoints:
|
* Three resolution endpoints:
|
||||||
* - `resolve` → admin reviewed, no action taken (clears flags).
|
* - `resolve` → admin reviewed, no action taken (clears flags).
|
||||||
|
|||||||
+1
-2
@@ -6,8 +6,7 @@ import retrofit2.http.POST
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/admin/requests`. Mirrors
|
* Retrofit interface for `/api/admin/requests`.
|
||||||
* `flutter_client/lib/api/endpoints/admin_requests.dart`.
|
|
||||||
*
|
*
|
||||||
* Server returns the same `requestView` shape as the user-side
|
* Server returns the same `requestView` shape as the user-side
|
||||||
* `/api/requests`, so RequestWire is reused. Different listing scope —
|
* `/api/requests`, so RequestWire is reused. Different listing scope —
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import retrofit2.http.PUT
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/admin/users`. Mirrors
|
* Retrofit interface for `/api/admin/users`.
|
||||||
* `flutter_client/lib/api/endpoints/admin_users.dart`.
|
|
||||||
*
|
*
|
||||||
* Note: the PUT-auto-approve body field is `auto_approve`, NOT
|
* Note: the PUT-auto-approve body field is `auto_approve`, NOT
|
||||||
* `auto_approve_requests` — the request shape differs from the
|
* `auto_approve_requests` — the request shape differs from the
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import retrofit2.http.Body
|
|||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/auth`. Mirrors
|
* Retrofit interface for `/api/auth`.
|
||||||
* `flutter_client/lib/api/endpoints/auth.dart`.
|
|
||||||
*
|
*
|
||||||
* The actual session-cookie capture happens in
|
* The actual session-cookie capture happens in
|
||||||
* [com.fabledsword.minstrel.api.AuthCookieInterceptor]; we don't
|
* [com.fabledsword.minstrel.api.AuthCookieInterceptor]; we don't
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import retrofit2.http.Query
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for Discover / Lidarr search / request creation.
|
* Retrofit interface for Discover / Lidarr search / request creation.
|
||||||
* Mirrors `flutter_client/lib/api/endpoints/discover.dart`.
|
|
||||||
*
|
*
|
||||||
* `/api/lidarr/search` has a 60s LRU on the server so quick re-types
|
* `/api/lidarr/search` has a 60s LRU on the server so quick re-types
|
||||||
* of the same query are cheap.
|
* of the same query are cheap.
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import retrofit2.http.Body
|
|||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `POST /api/events`. Mirrors the relevant
|
* Retrofit interface for `POST /api/events`. All four
|
||||||
* slice of `flutter_client/lib/api/endpoints/events.dart`. All four
|
|
||||||
* variants share the same URL — the discriminator is in the request
|
* variants share the same URL — the discriminator is in the request
|
||||||
* body's `type` field. Server contract is best-effort per spec;
|
* body's `type` field. Server contract is best-effort per spec;
|
||||||
* callers (the live path in PlayEventsReporter) swallow errors and
|
* callers (the live path in PlayEventsReporter) swallow errors and
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import retrofit2.http.GET
|
|||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/me/history`. Mirrors the relevant
|
* Retrofit interface for `/api/me/history` — history only. The profile,
|
||||||
* subset of `flutter_client/lib/api/endpoints/me.dart` (only
|
* timezone and quarantine endpoints on `/api/me` live with their own
|
||||||
* `history()`; profile / timezone / quarantine endpoints land with
|
* features rather than here.
|
||||||
* their respective phases).
|
|
||||||
*/
|
*/
|
||||||
interface HistoryApi {
|
interface HistoryApi {
|
||||||
@GET("api/me/history")
|
@GET("api/me/history")
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import com.fabledsword.minstrel.models.wire.HomeIndexWire
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for the Home discovery endpoint. Mirrors
|
* Retrofit interface for the Home discovery endpoint. Only the ID-only
|
||||||
* `flutter_client/lib/api/endpoints/home.dart` — just the ID-only
|
* `/api/home/index` variant is used. The server also serves a heavier
|
||||||
* `/api/home/index` variant. The Flutter port has a heavier
|
* `/api/home` (full embedded payload); we don't use it because
|
||||||
* `/api/home` (full embedded payload) too; we don't use it because
|
|
||||||
* the per-item hydration path (sync controller → Room → Flow) is
|
* the per-item hydration path (sync controller → Room → Flow) is
|
||||||
* the only one the native client needs.
|
* the only one the native client needs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import retrofit2.http.Query
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for the server's native `/api/...` library surface.
|
* Retrofit interface for the server's native `/api/...` library surface.
|
||||||
* Mirrors `flutter_client/lib/api/endpoints/library.dart` 1:1.
|
|
||||||
*
|
*
|
||||||
* Notes on shapes:
|
* Notes on shapes:
|
||||||
* - `GET /api/artists/{id}` returns ArtistDetailWire (ArtistRef fields
|
* - `GET /api/artists/{id}` returns ArtistDetailWire (ArtistRef fields
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import retrofit2.http.POST
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/likes`. Mirrors
|
* Retrofit interface for `/api/likes`.
|
||||||
* `flutter_client/lib/api/endpoints/likes.dart`.
|
|
||||||
*
|
*
|
||||||
* Path segment `kind` is one of "artists" | "albums" | "tracks"
|
* Path segment `kind` is one of "artists" | "albums" | "tracks"
|
||||||
* (plural, matching the server route). The Repository hides that
|
* (plural, matching the server route). The Repository hides that
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import retrofit2.http.PUT
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for the `/api/me` endpoints — caller-scoped account endpoints.
|
* Retrofit interface for the `/api/me` endpoints — caller-scoped account endpoints.
|
||||||
* Mirrors the relevant slice of `flutter_client/lib/api/endpoints/settings.dart`.
|
|
||||||
*
|
*
|
||||||
* History + timezone + system-playlists-status live under /api/me too
|
* History + timezone + system-playlists-status live under /api/me too
|
||||||
* but are handled by their respective feature repositories; this
|
* but are handled by their respective feature repositories; this
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import retrofit2.http.Path
|
|||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/playlists`. Mirrors
|
* Retrofit interface for `/api/playlists`.
|
||||||
* `flutter_client/lib/api/endpoints/playlists.dart`.
|
|
||||||
*/
|
*/
|
||||||
interface PlaylistsApi {
|
interface PlaylistsApi {
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +53,7 @@ interface PlaylistsApi {
|
|||||||
* the system playlist's tracks in rotation-aware order without
|
* the system playlist's tracks in rotation-aware order without
|
||||||
* rebuilding — used by the Home play-button overlay so taps on For
|
* rebuilding — used by the Home play-button overlay so taps on For
|
||||||
* You / Discover / Today's mix advance rotation rather than picking
|
* You / Discover / Today's mix advance rotation rather than picking
|
||||||
* the stored order. Mirrors `playlists.dart.systemShuffle`.
|
* the stored order.
|
||||||
*/
|
*/
|
||||||
@GET("api/playlists/system/{kind}/shuffle")
|
@GET("api/playlists/system/{kind}/shuffle")
|
||||||
suspend fun systemShuffle(@Path("kind") variant: String): PlaylistDetailWire
|
suspend fun systemShuffle(@Path("kind") variant: String): PlaylistDetailWire
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import retrofit2.http.POST
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/quarantine`. Mirrors the relevant
|
* Retrofit interface for `/api/quarantine`: flag and unflag, plus the
|
||||||
* parts of `flutter_client/lib/api/endpoints/quarantine.dart` (flag
|
* `/api/quarantine/mine` listing.
|
||||||
* and unflag) plus the `/api/quarantine/mine` endpoint from `me.dart`.
|
|
||||||
*
|
*
|
||||||
* Both flag and unflag are user-scoped — callers act on their own
|
* Both flag and unflag are user-scoped — callers act on their own
|
||||||
* quarantine entries. The cross-user admin surface is a separate
|
* quarantine entries. The cross-user admin surface is a separate
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import retrofit2.http.GET
|
|||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `/api/radio`. Mirrors the relevant slice of
|
* Retrofit interface for `/api/radio`. The server picks a fresh shuffle each
|
||||||
* `flutter_client/lib/api/endpoints/radio.dart` (a single GET that
|
|
||||||
* returns the seeded queue). The server picks a fresh shuffle each
|
|
||||||
* invocation — clients call this once per radio start.
|
* invocation — clients call this once per radio start.
|
||||||
*/
|
*/
|
||||||
interface RadioApi {
|
interface RadioApi {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import retrofit2.http.GET
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for the user-side `/api/requests`. Mirrors
|
* Retrofit interface for the user-side `/api/requests`.
|
||||||
* `flutter_client/lib/api/endpoints/requests.dart`.
|
|
||||||
*
|
*
|
||||||
* Server scopes results to the caller — admins see only their own
|
* Server scopes results to the caller — admins see only their own
|
||||||
* requests through this endpoint. The cross-user admin view lives on
|
* requests through this endpoint. The cross-user admin view lives on
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import retrofit2.http.GET
|
|||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit interface for `GET /api/search`. Mirrors
|
* Retrofit interface for `GET /api/search`. Server returns 400
|
||||||
* `flutter_client/lib/api/endpoints/search.dart`. Server returns 400
|
|
||||||
* on empty/whitespace-only `q` — the caller is responsible for
|
* on empty/whitespace-only `q` — the caller is responsible for
|
||||||
* guarding.
|
* guarding.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton facade over the auth state machine. Mirrors Flutter's
|
* Singleton facade over the auth state machine.
|
||||||
* `AuthController` from `auth_provider.dart`.
|
|
||||||
*
|
*
|
||||||
* Cookie persistence is handled by [AuthCookieInterceptor] capturing
|
* Cookie persistence is handled by [AuthCookieInterceptor] capturing
|
||||||
* Set-Cookie on the login response; the user identity itself
|
* Set-Cookie on the login response; the user identity itself
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import javax.inject.Singleton
|
|||||||
private const val POOL_LIMIT = 100
|
private const val POOL_LIMIT = 100
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Offline play sources over the local audio-cache index. Mirrors
|
* Offline play sources over the local audio-cache index.
|
||||||
* `flutter_client/lib/cache/shuffle_source.dart`.
|
|
||||||
*
|
*
|
||||||
* Both pools are UNIONs over the cache regardless of storage bucket
|
* Both pools are UNIONs over the cache regardless of storage bucket
|
||||||
* (liked AND recently-played both included). The two-bucket split is
|
* (liked AND recently-played both included). The two-bucket split is
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package com.fabledsword.minstrel.cache.audiocache
|
package com.fabledsword.minstrel.cache.audiocache
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults for the 2-bucket audio cache. Matches the Flutter client.
|
* Defaults for the 2-bucket audio cache.
|
||||||
*
|
*
|
||||||
* - `likedCapBytes`: cap for the protected bucket — cached files for
|
* - `likedCapBytes`: cap for the protected bucket — cached files for
|
||||||
* tracks the user has liked. Evicted only after the rolling bucket
|
* tracks the user has liked. Evicted only after the rolling bucket
|
||||||
|
|||||||
+1
-2
@@ -6,8 +6,7 @@ private const val FIVE_GIB_BYTES = 5L * 1024 * 1024 * 1024
|
|||||||
private const val DEFAULT_PREFETCH_WINDOW = 5
|
private const val DEFAULT_PREFETCH_WINDOW = 5
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User-tunable audio cache settings. Mirrors Flutter's `CacheSettings`
|
* User-tunable audio cache settings. Persisted as a JSON
|
||||||
* (cache_settings_provider.dart) field-for-field. Persisted as a JSON
|
|
||||||
* blob on the auth_session single-row table via [AuthStore].
|
* blob on the auth_session single-row table via [AuthStore].
|
||||||
*
|
*
|
||||||
* - [likedCapBytes]: budget for cached files of liked tracks. 0 means
|
* - [likedCapBytes]: budget for cached files of liked tracks. 0 means
|
||||||
|
|||||||
-1
@@ -59,7 +59,6 @@ interface CachedPlaylistDao {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Atomically reconciles the cache against the fresh list response.
|
* Atomically reconciles the cache against the fresh list response.
|
||||||
* Mirrors `flutter_client/lib/playlists/playlists_provider.dart:54` —
|
|
||||||
* `BuildSystemPlaylists` rotates system-playlist UUIDs every
|
* `BuildSystemPlaylists` rotates system-playlist UUIDs every
|
||||||
* rebuild, so upsert alone leaves stale rows whose detail fetch
|
* rebuild, so upsert alone leaves stale rows whose detail fetch
|
||||||
* 404s. Delete any of the user's rows not in [freshOwnedIds] (this
|
* 404s. Delete any of the user's rows not in [freshOwnedIds] (this
|
||||||
|
|||||||
Vendored
+1
-1
@@ -8,7 +8,7 @@ import kotlinx.datetime.Instant
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* One row per fully-downloaded audio file. Mirrors
|
* One row per fully-downloaded audio file. Mirrors
|
||||||
* `flutter_client/lib/cache/db.dart`'s `AudioCacheIndex` Drift table.
|
* the Flutter client's `AudioCacheIndex` Drift table.
|
||||||
*
|
*
|
||||||
* Drives the 2-bucket LRU eviction (Phase 12 AudioCacheEvictionWorker):
|
* Drives the 2-bucket LRU eviction (Phase 12 AudioCacheEvictionWorker):
|
||||||
* - `incidental` files (streamed-and-cached side effect) evict first
|
* - `incidental` files (streamed-and-cached side effect) evict first
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache row for one album. Mirrors `flutter_client/lib/cache/db.dart`'s
|
* Cache row for one album. Mirrors the Flutter client's
|
||||||
* `CachedAlbums` Drift table.
|
* `CachedAlbums` Drift table.
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "cached_albums")
|
@Entity(tableName = "cached_albums")
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache row for one artist. Mirrors `flutter_client/lib/cache/db.dart`'s
|
* Cache row for one artist. Mirrors the Flutter client's
|
||||||
* `CachedArtists` Drift table.
|
* `CachedArtists` Drift table.
|
||||||
*
|
*
|
||||||
* Column names follow Kotlin idiom (camelCase) rather than Drift's
|
* Column names follow Kotlin idiom (camelCase) rather than Drift's
|
||||||
|
|||||||
Vendored
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.datetime.Instant
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-item row driving the Home screen sections. Mirrors
|
* Per-item row driving the Home screen sections. Mirrors
|
||||||
* `flutter_client/lib/cache/db.dart`'s `CachedHomeIndex` Drift table.
|
* the Flutter client's `CachedHomeIndex` Drift table.
|
||||||
*
|
*
|
||||||
* `section` is one of (matching /api/home keys):
|
* `section` is one of (matching /api/home keys):
|
||||||
* - "recently_added_albums"
|
* - "recently_added_albums"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Like membership row. Mirrors `flutter_client/lib/cache/db.dart`'s
|
* Like membership row. Mirrors the Flutter client's
|
||||||
* `CachedLikes` Drift table. Composite primary key — one user may
|
* `CachedLikes` Drift table. Composite primary key — one user may
|
||||||
* independently like a track AND its album AND its artist; rows are
|
* independently like a track AND its album AND its artist; rows are
|
||||||
* disambiguated by the (userId, entityType, entityId) triple.
|
* disambiguated by the (userId, entityType, entityId) triple.
|
||||||
|
|||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.datetime.Instant
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* One row per pending offline-write. Mirrors
|
* One row per pending offline-write. Mirrors
|
||||||
* `flutter_client/lib/cache/db.dart`'s `CachedMutations` Drift table.
|
* the Flutter client's `CachedMutations` Drift table.
|
||||||
*
|
*
|
||||||
* MutationQueue.enqueue() inserts a row when a server-write fails with
|
* MutationQueue.enqueue() inserts a row when a server-write fails with
|
||||||
* an IOException; MutationReplayer.drain() pops and re-attempts each
|
* an IOException; MutationReplayer.drain() pops and re-attempts each
|
||||||
|
|||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.datetime.Instant
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache row for one playlist (user or system). Mirrors
|
* Cache row for one playlist (user or system). Mirrors
|
||||||
* `flutter_client/lib/cache/db.dart`'s `CachedPlaylists` Drift table.
|
* the Flutter client's `CachedPlaylists` Drift table.
|
||||||
*
|
*
|
||||||
* `systemVariant` is null for user playlists and one of
|
* `systemVariant` is null for user playlists and one of
|
||||||
* "for_you" / "songs_like_artist" / "discover" / "todays_mix" / etc.
|
* "for_you" / "songs_like_artist" / "discover" / "todays_mix" / etc.
|
||||||
|
|||||||
Vendored
+1
-1
@@ -4,7 +4,7 @@ import androidx.room.Entity
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ordered membership of tracks within a playlist. Mirrors
|
* Ordered membership of tracks within a playlist. Mirrors
|
||||||
* `flutter_client/lib/cache/db.dart`'s `CachedPlaylistTracks` Drift table.
|
* the Flutter client's `CachedPlaylistTracks` Drift table.
|
||||||
* Composite PK so the same track can only appear once per playlist;
|
* Composite PK so the same track can only appear once per playlist;
|
||||||
* `position` carries the ordering.
|
* `position` carries the ordering.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.datetime.Instant
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The current user's quarantine flag for one track. Mirrors
|
* The current user's quarantine flag for one track. Mirrors
|
||||||
* `flutter_client/lib/cache/db.dart`'s `CachedQuarantineMine` Drift
|
* the Flutter client's `CachedQuarantineMine` Drift
|
||||||
* table.
|
* table.
|
||||||
*
|
*
|
||||||
* The flat denormalized track/album/artist columns let the Quarantine
|
* The flat denormalized track/album/artist columns let the Quarantine
|
||||||
|
|||||||
Vendored
+1
-1
@@ -8,7 +8,7 @@ import kotlinx.datetime.Instant
|
|||||||
/**
|
/**
|
||||||
* Single-row snapshot of the last playback session — queue (as JSON),
|
* Single-row snapshot of the last playback session — queue (as JSON),
|
||||||
* current index, position, and source tag. Mirrors
|
* current index, position, and source tag. Mirrors
|
||||||
* `flutter_client/lib/cache/db.dart`'s `CachedResumeState` Drift table.
|
* the Flutter client's `CachedResumeState` Drift table.
|
||||||
*
|
*
|
||||||
* Lets a torn-down session (the player's idle/dismissed teardown)
|
* Lets a torn-down session (the player's idle/dismissed teardown)
|
||||||
* resume on next launch; without it the headset / lock-screen play
|
* resume on next launch; without it the headset / lock-screen play
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache row for one track. Mirrors `flutter_client/lib/cache/db.dart`'s
|
* Cache row for one track. Mirrors the Flutter client's
|
||||||
* `CachedTracks` Drift table.
|
* `CachedTracks` Drift table.
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "cached_tracks")
|
@Entity(tableName = "cached_tracks")
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ private const val BACKOFF_FACTOR = 2
|
|||||||
* ViewModels + the central [LiveEventsDispatcher]) collect filtered
|
* ViewModels + the central [LiveEventsDispatcher]) collect filtered
|
||||||
* subsets of the stream.
|
* subsets of the stream.
|
||||||
*
|
*
|
||||||
* Connection lifecycle mirrors
|
* Connection lifecycle:
|
||||||
* `flutter_client/lib/shared/live_events_provider.dart`:
|
|
||||||
* - Gated on having a session cookie. Subscription opens when the
|
* - Gated on having a session cookie. Subscription opens when the
|
||||||
* cookie transitions to non-null and closes when it transitions
|
* cookie transitions to non-null and closes when it transitions
|
||||||
* back to null (sign-out).
|
* back to null (sign-out).
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import kotlinx.serialization.json.JsonObject
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parsed event from the server's SSE stream. Mirrors
|
* Parsed event from the server's SSE stream. Mirrors
|
||||||
* `flutter_client/lib/shared/live_events_provider.dart`'s `LiveEvent`.
|
* the Flutter client's `LiveEvent`.
|
||||||
*
|
*
|
||||||
* - [kind] is the SSE `event:` field (e.g. "track.liked", "playlist.deleted").
|
* - [kind] is the SSE `event:` field (e.g. "track.liked", "playlist.deleted").
|
||||||
* - [userId] is the actor whose user-scoped state changed (empty for
|
* - [userId] is the actor whose user-scoped state changed (empty for
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps incoming [LiveEvent]s to cross-screen state refreshes. Mirrors
|
* Maps incoming [LiveEvent]s to cross-screen state refreshes. Activated
|
||||||
* `flutter_client/lib/shared/live_events_dispatcher.dart`. Activated
|
|
||||||
* by force-@Inject in MinstrelApplication.
|
* by force-@Inject in MinstrelApplication.
|
||||||
*
|
*
|
||||||
* Scope is deliberately narrow: this dispatcher only touches state
|
* Scope is deliberately narrow: this dispatcher only touches state
|
||||||
|
|||||||
@@ -204,8 +204,7 @@ private const val HOURS_PER_DAY = 24L
|
|||||||
private const val DAYS_PER_WEEK = 7L
|
private const val DAYS_PER_WEEK = 7L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight relative-time formatter mirroring Flutter's
|
* Lightweight relative-time formatter:
|
||||||
* `library_screen.dart`'s `_relativeTime`:
|
|
||||||
*
|
*
|
||||||
* < 1h → "Nm ago"
|
* < 1h → "Nm ago"
|
||||||
* < 24h → "Nh ago"
|
* < 24h → "Nh ago"
|
||||||
|
|||||||
@@ -1172,7 +1172,7 @@ enum class OfflinePoolKind(val label: String) {
|
|||||||
* first / greyed after, and the "building/pending" placeholders are dropped
|
* first / greyed after, and the "building/pending" placeholders are dropped
|
||||||
* (they need the server to generate, so they're meaningless offline).
|
* (they need the server to generate, so they're meaningless offline).
|
||||||
*
|
*
|
||||||
* Diverges from Flutter (`flutter_client/lib/library/home_screen.dart`
|
* Diverges from Flutter (the Flutter client
|
||||||
* `_buildPlaylistsRow`) which only shows the 5 fixed slots and never
|
* `_buildPlaylistsRow`) which only shows the 5 fixed slots and never
|
||||||
* surfaces the secondary kinds on Home. Operator authorized the
|
* surfaces the secondary kinds on Home. Operator authorized the
|
||||||
* divergence on 2026-06-01; web UI catch-up tracked as task #53.
|
* divergence on 2026-06-01; web UI catch-up tracked as task #53.
|
||||||
@@ -1374,7 +1374,7 @@ private const val MOST_PLAYED_COVER_DP = 48
|
|||||||
|
|
||||||
// 3 rows of MOST_PLAYED_TILE_HEIGHT_DP + 2 * 8dp inter-row spacing,
|
// 3 rows of MOST_PLAYED_TILE_HEIGHT_DP + 2 * 8dp inter-row spacing,
|
||||||
// rounded up. Mirrors Flutter (`CompactTrackCard` in
|
// rounded up. Mirrors Flutter (`CompactTrackCard` in
|
||||||
// flutter_client/lib/library/widgets/compact_track_card.dart) which
|
// the Flutter client) which
|
||||||
// uses a horizontal-row card pattern - much denser than the square
|
// uses a horizontal-row card pattern - much denser than the square
|
||||||
// per-track tiles that web uses (operator request 2026-06-01: "in the
|
// per-track tiles that web uses (operator request 2026-06-01: "in the
|
||||||
// flutter iteration the tiles were different and smaller so more of
|
// flutter iteration the tiles were different and smaller so more of
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import com.fabledsword.minstrel.shared.widgets.SkeletonArtistTile
|
|||||||
* Library tab. Seven-tab TabBar (Artists / Albums / Genres / Years /
|
* Library tab. Seven-tab TabBar (Artists / Albums / Genres / Years /
|
||||||
* History / Liked / Hidden), matching the web client's library tab bar.
|
* History / Liked / Hidden), matching the web client's library tab bar.
|
||||||
* Genres and Years arrived with #2467; the rest predate it and mirrored
|
* Genres and Years arrived with #2467; the rest predate it and mirrored
|
||||||
* `flutter_client/lib/library/library_screen.dart`.
|
* the Flutter client.
|
||||||
*
|
*
|
||||||
* Artists + Albums are wired against the existing LibraryViewModel
|
* Artists + Albums are wired against the existing LibraryViewModel
|
||||||
* (cache-first reads of cached_artists / cached_albums). The other
|
* (cache-first reads of cached_artists / cached_albums). The other
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.models
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight reference to one album. Mirrors
|
* Lightweight reference to one album. Mirrors
|
||||||
* `flutter_client/lib/models/album.dart`'s `AlbumRef`.
|
* the Flutter client's `AlbumRef`.
|
||||||
*
|
*
|
||||||
* `coverUrl` and `durationSec` match the server contract (not
|
* `coverUrl` and `durationSec` match the server contract (not
|
||||||
* `cover_art_url` / `duration_ms`). `year` is omitempty server-side so
|
* `cover_art_url` / `duration_ms`). `year` is omitempty server-side so
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.models
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight reference to one artist. Mirrors
|
* Lightweight reference to one artist. Mirrors
|
||||||
* `flutter_client/lib/models/artist.dart`'s `ArtistRef`.
|
* the Flutter client's `ArtistRef`.
|
||||||
*
|
*
|
||||||
* `coverUrl` is the server's field name (NOT cover_art_url). Server emits
|
* `coverUrl` is the server's field name (NOT cover_art_url). Server emits
|
||||||
* empty string when the artist has no representative album cover; UI code
|
* empty string when the artist has no representative album cover; UI code
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ enum class LidarrRequestKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lidarr search hit. Mirrors `flutter_client/lib/models/lidarr.dart`'s
|
* Lidarr search hit. Mirrors the Flutter client's
|
||||||
* `LidarrSearchResult` — `mbid` is the result's own MBID; `artistMbid`
|
* `LidarrSearchResult` — `mbid` is the result's own MBID; `artistMbid`
|
||||||
* and `albumMbid` are filled when the row is an album/track and the
|
* and `albumMbid` are filled when the row is an album/track and the
|
||||||
* UI needs the parent IDs to build the request.
|
* UI needs the parent IDs to build the request.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.models
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain shape for one admin-issued registration invite. Mirrors
|
* Domain shape for one admin-issued registration invite. Mirrors
|
||||||
* `flutter_client/lib/models/invite.dart Invite` and the server's
|
* the Flutter client's `Invite` and the server's
|
||||||
* `inviteResp` from `internal/api/admin_invites.go`.
|
* `inviteResp` from `internal/api/admin_invites.go`.
|
||||||
*
|
*
|
||||||
* `invitedBy` and `redeemedBy` are UUIDs of users (not usernames);
|
* `invitedBy` and `redeemedBy` are UUIDs of users (not usernames);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.models
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Caller's ListenBrainz integration state. Mirrors
|
* Caller's ListenBrainz integration state. Mirrors
|
||||||
* `flutter_client/lib/models/my_profile.dart ListenBrainzStatus`
|
* the Flutter client's `ListenBrainzStatus`
|
||||||
* and the server's `listenBrainzResp`.
|
* and the server's `listenBrainzResp`.
|
||||||
*
|
*
|
||||||
* The token itself is never read back from the server — `tokenSet`
|
* The token itself is never read back from the server — `tokenSet`
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.models
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight reference to one playlist (user or system-generated).
|
* Lightweight reference to one playlist (user or system-generated).
|
||||||
* Mirrors `flutter_client/lib/models/playlist.dart`'s `Playlist`.
|
* Mirrors the Flutter client's `Playlist`.
|
||||||
*
|
*
|
||||||
* `systemVariant` discriminates user vs. system playlists — null for
|
* `systemVariant` discriminates user vs. system playlists — null for
|
||||||
* user-owned, one of "for_you" / "discover" / "songs_like_artist" / etc.
|
* user-owned, one of "for_you" / "discover" / "songs_like_artist" / etc.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ enum class RequestStatus {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* One Lidarr request the user has submitted. Mirrors
|
* One Lidarr request the user has submitted. Mirrors
|
||||||
* `flutter_client/lib/models/admin_request.dart AdminRequest` —
|
* the Flutter client's `AdminRequest` —
|
||||||
* shared between the user-side `/api/requests` view and the admin
|
* shared between the user-side `/api/requests` view and the admin
|
||||||
* cross-user view since the wire shape is identical.
|
* cross-user view since the wire shape is identical.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package com.fabledsword.minstrel.models
|
|||||||
/**
|
/**
|
||||||
* Caller's most recent system_playlist_runs state, driving the Home
|
* Caller's most recent system_playlist_runs state, driving the Home
|
||||||
* placeholder cards for not-yet-generated system playlists. Mirrors
|
* placeholder cards for not-yet-generated system playlists. Mirrors
|
||||||
* `flutter_client/lib/models/system_playlists_status.dart` and the
|
* the server's `systemPlaylistsStatusResp`.
|
||||||
* server's `systemPlaylistsStatusResp`.
|
|
||||||
*
|
*
|
||||||
* Zero values (inFlight=false, both timestamps null) mean the user
|
* Zero values (inFlight=false, both timestamps null) mean the user
|
||||||
* has never had a build attempted — the placeholders read as
|
* has never had a build attempted — the placeholders read as
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight reference to one track. Mirrors
|
* Lightweight reference to one track. Mirrors
|
||||||
* `flutter_client/lib/models/track.dart`'s `TrackRef`.
|
* the Flutter client's `TrackRef`.
|
||||||
*
|
*
|
||||||
* The `Ref` suffix matches the Flutter convention — these types carry
|
* The `Ref` suffix matches the Flutter convention — these types carry
|
||||||
* only the IDs + display fields needed for list rendering + the player
|
* only the IDs + display fields needed for list rendering + the player
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.fabledsword.minstrel.models
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shape returned by `GET /api/client/version`. Mirrors
|
* Wire shape returned by `GET /api/client/version`. Mirrors
|
||||||
* `flutter_client/lib/update/update_info.dart UpdateInfo`.
|
* the Flutter client's `UpdateInfo`.
|
||||||
*
|
*
|
||||||
* `version` is the server-bundled APK version (may have a leading
|
* `version` is the server-bundled APK version (may have a leading
|
||||||
* "v" from the git tag); `apkUrl` is server-relative (e.g.
|
* "v" from the git tag); `apkUrl` is server-relative (e.g.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shape for `AlbumRef`. Mirrors `flutter_client/lib/models/album.dart`.
|
* Wire shape for `AlbumRef`.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AlbumWire(
|
data class AlbumWire(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shape for `ArtistRef`. Mirrors `flutter_client/lib/models/artist.dart`.
|
* Wire shape for `ArtistRef`.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ArtistWire(
|
data class ArtistWire(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
/**
|
/**
|
||||||
* One row of `GET /api/lidarr/search`. Mirrors
|
* One row of `GET /api/lidarr/search`. Mirrors
|
||||||
* `web/src/lib/api/types.ts LidarrSearchResult` /
|
* `web/src/lib/api/types.ts LidarrSearchResult` /
|
||||||
* `flutter_client/lib/models/lidarr.dart LidarrSearchResult`.
|
* the Flutter client's `LidarrSearchResult`.
|
||||||
*
|
*
|
||||||
* `inLibrary` and `requested` let the UI greyout rows the user can't
|
* `inLibrary` and `requested` let the UI greyout rows the user can't
|
||||||
* act on (already imported / already awaiting review). All defaults
|
* act on (already imported / already awaiting review). All defaults
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shapes for `POST /api/events`. The endpoint multiplexes four
|
* Wire shapes for `POST /api/events`. The endpoint multiplexes four
|
||||||
* variants on the `type` discriminator field, mirroring
|
* variants on the `type` discriminator field.
|
||||||
* `flutter_client/lib/api/endpoints/events.dart`.
|
|
||||||
*
|
*
|
||||||
* play_started returns the server-assigned play_event_id (nullable —
|
* play_started returns the server-assigned play_event_id (nullable —
|
||||||
* server may suppress under certain conditions); the other three
|
* server may suppress under certain conditions); the other three
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shape of `GET /api/home/index` — five flat slices of entity-ID
|
* Wire shape of `GET /api/home/index` — five flat slices of entity-ID
|
||||||
* strings, one per Home section. Mirrors
|
* strings, one per Home section. Mirrors the server's
|
||||||
* `flutter_client/lib/models/home_index.dart` (and the server's
|
* `HomeIndexPayload` in `internal/api/types.go`.
|
||||||
* `internal/api/types.go HomeIndexPayload`).
|
|
||||||
*
|
*
|
||||||
* Section name implies entity type; no per-entry type tag is needed:
|
* Section name implies entity type; no per-entry type tag is needed:
|
||||||
* - recentlyAddedAlbums → album
|
* - recentlyAddedAlbums → album
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shape for `GET /api/me` and the return value of
|
* Wire shape for `GET /api/me` and the return value of
|
||||||
* `PUT /api/me/profile`. Mirrors
|
* `PUT /api/me/profile`. Two things the shape assumes:
|
||||||
* `flutter_client/lib/models/my_profile.dart`:
|
|
||||||
* - `display_name` and `email` are nullable; server returns null
|
* - `display_name` and `email` are nullable; server returns null
|
||||||
* when the user hasn't set them yet (registration only requires
|
* when the user hasn't set them yet (registration only requires
|
||||||
* a username).
|
* a username).
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* One row of `GET /api/quarantine/mine`. Mirrors
|
* One row of `GET /api/quarantine/mine`. Mirrors
|
||||||
* `flutter_client/lib/models/quarantine_mine.dart QuarantineMineRow`
|
* the Flutter client's `QuarantineMineRow`
|
||||||
* (web `LidarrQuarantineMineRow`).
|
* (web `LidarrQuarantineMineRow`).
|
||||||
*
|
*
|
||||||
* Reason values: `bad_rip` / `wrong_file` / `wrong_tags` / `duplicate`
|
* Reason values: `bad_rip` / `wrong_file` / `wrong_tags` / `duplicate`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
|||||||
* Wire shape of `requestView` from `internal/api/requests.go` — the
|
* Wire shape of `requestView` from `internal/api/requests.go` — the
|
||||||
* row returned by both `GET /api/requests` (caller's own requests) and
|
* row returned by both `GET /api/requests` (caller's own requests) and
|
||||||
* `GET /api/admin/requests` (cross-user admin view). Mirrors
|
* `GET /api/admin/requests` (cross-user admin view). Mirrors
|
||||||
* `flutter_client/lib/models/admin_request.dart AdminRequest`.
|
* the Flutter client's `AdminRequest`.
|
||||||
*
|
*
|
||||||
* Status values: `pending` / `approved` / `rejected` / `completed` /
|
* Status values: `pending` / `approved` / `rejected` / `completed` /
|
||||||
* `failed`. Kind values: `artist` / `album` / `track`.
|
* `failed`. Kind values: `artist` / `album` / `track`.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire shape for `TrackRef` as the server emits it. Mirrors
|
* Wire shape for `TrackRef` as the server emits it. Mirrors
|
||||||
* `flutter_client/lib/models/track.dart`'s `TrackRef.fromJson`
|
* the Flutter client's `TrackRef.fromJson`
|
||||||
* field-for-field; the keys are snake_case because the server is Go
|
* field-for-field; the keys are snake_case because the server is Go
|
||||||
* (json:"album_id" etc.).
|
* (json:"album_id" etc.).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import javax.inject.Singleton
|
|||||||
* Pre-downloads the next-N tracks in the queue into the shared Media3
|
* Pre-downloads the next-N tracks in the queue into the shared Media3
|
||||||
* [androidx.media3.datasource.cache.SimpleCache] so a skip-forward or
|
* [androidx.media3.datasource.cache.SimpleCache] so a skip-forward or
|
||||||
* natural advance plays from disk instead of waiting on a fresh HTTP
|
* natural advance plays from disk instead of waiting on a fresh HTTP
|
||||||
* connection. Mirrors the Flutter `Prefetcher` (cache/prefetcher.dart):
|
* connection. Behaviour:
|
||||||
* watches the player's current track, walks forward by
|
* watches the player's current track, walks forward by
|
||||||
* [com.fabledsword.minstrel.cache.audiocache.CacheSettings.prefetchWindow]
|
* [com.fabledsword.minstrel.cache.audiocache.CacheSettings.prefetchWindow]
|
||||||
* tracks, and pins each one. Idempotent — `CacheWriter` is a no-op when
|
* tracks, and pins each one. Idempotent — `CacheWriter` is a no-op when
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ private const val DEBOUNCE_MS = 2_000L
|
|||||||
* operator never finds out the track is bad and the next user hits
|
* operator never finds out the track is bad and the next user hits
|
||||||
* the same wall.
|
* the same wall.
|
||||||
*
|
*
|
||||||
* The snackbar text mirrors Flutter's `playback_error_reporter.dart`:
|
* The snackbar text:
|
||||||
* collect [PlayerController.playbackErrorEvents], debounce in a 2s
|
* collect [PlayerController.playbackErrorEvents], debounce in a 2s
|
||||||
* window, emit "Couldn't play 'X' — skipping" for a single error or
|
* window, emit "Couldn't play 'X' — skipping" for a single error or
|
||||||
* "Skipped N unplayable tracks" when a burst lands inside the window.
|
* "Skipped N unplayable tracks" when a burst lands inside the window.
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package com.fabledsword.minstrel.player
|
|||||||
import com.fabledsword.minstrel.models.TrackRef
|
import com.fabledsword.minstrel.models.TrackRef
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cycle on the repeat button: off → all → one → off. Mirrors
|
* Cycle on the repeat button: off → all → one → off. Maps directly to
|
||||||
* `AudioServiceRepeatMode` in flutter_client and maps directly to
|
|
||||||
* the three Media3 `Player.REPEAT_MODE_*` int constants.
|
* the three Media3 `Player.REPEAT_MODE_*` int constants.
|
||||||
*/
|
*/
|
||||||
enum class RepeatMode { OFF, ALL, ONE }
|
enum class RepeatMode { OFF, ALL, ONE }
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import javax.inject.Singleton
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Persists the player's last queue + position to Room so a torn-down
|
* Persists the player's last queue + position to Room so a torn-down
|
||||||
* session can resume on next app launch. Mirrors
|
* session can resume on next app launch.
|
||||||
* `flutter_client/lib/cache/resume_controller.dart`.
|
|
||||||
*
|
*
|
||||||
* Subscribes to [PlayerController.uiState] in init; persists when the
|
* Subscribes to [PlayerController.uiState] in init; persists when the
|
||||||
* (track-id, queueIndex) changes — captures real session transitions
|
* (track-id, queueIndex) changes — captures real session transitions
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ private const val GRADIENT_TWEEN_MS = 600
|
|||||||
* The held color is NOT reset when [coverUrl] changes — it stays on
|
* The held color is NOT reset when [coverUrl] changes — it stays on
|
||||||
* the previous track's dominant until the new palette resolves, so the
|
* the previous track's dominant until the new palette resolves, so the
|
||||||
* gradient tweens old→new directly instead of dipping toward the
|
* gradient tweens old→new directly instead of dipping toward the
|
||||||
* fallback mid-swap. Mirrors `now_playing_screen.dart`'s preload-then-
|
* fallback mid-swap — preload-then-swap, keeping the previous dominant
|
||||||
* swap ("keep the previous dominant"); the cover image swaps smoothly
|
* until the new one resolves. The cover image swaps smoothly
|
||||||
* via `CoverPrefetcher`, which warms the next track's bytes into Coil.
|
* via `CoverPrefetcher`, which warms the next track's bytes into Coil.
|
||||||
*
|
*
|
||||||
* Starts at [Color.Transparent] (cold mount) and resets to it only
|
* Starts at [Color.Transparent] (cold mount) and resets to it only
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ private const val COVER_SIZE_DP = 48
|
|||||||
private const val SCRUBBER_ROW_HEIGHT_DP = 4
|
private const val SCRUBBER_ROW_HEIGHT_DP = 4
|
||||||
|
|
||||||
// Upward flick speed (dp/s) that expands the bar into NowPlaying.
|
// Upward flick speed (dp/s) that expands the bar into NowPlaying.
|
||||||
// Mirrors player_bar.dart's 200 px/s threshold; expressed in dp and
|
// The 200 px/s threshold carries over from the Flutter player bar,
|
||||||
// converted via density so the gesture feels the same across screens.
|
// expressed in dp and converted via density so the gesture feels the
|
||||||
|
// same across screens.
|
||||||
private const val SWIPE_UP_VELOCITY_DP = 200
|
private const val SWIPE_UP_VELOCITY_DP = 200
|
||||||
|
|
||||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@@ -129,7 +130,7 @@ fun MiniPlayer(
|
|||||||
.collectAsStateWithLifecycle(initialValue = false)
|
.collectAsStateWithLifecycle(initialValue = false)
|
||||||
|
|
||||||
// Swipe up anywhere on the bar to expand into the full player —
|
// Swipe up anywhere on the bar to expand into the full player —
|
||||||
// mirrors player_bar.dart. We only act on a clear upward flick
|
// We only act on a clear upward flick
|
||||||
// (negative velocity past the threshold) so a slow tap-with-jitter
|
// (negative velocity past the threshold) so a slow tap-with-jitter
|
||||||
// doesn't accidentally open the screen. The horizontal seek slider
|
// doesn't accidentally open the screen. The horizontal seek slider
|
||||||
// keeps its own gestures; a vertical draggable only claims
|
// keeps its own gestures; a vertical draggable only claims
|
||||||
|
|||||||
@@ -371,9 +371,7 @@ private fun NowPlayingContent(
|
|||||||
TrackHeader(title = track.title, artist = track.artistName, album = track.albumTitle)
|
TrackHeader(title = track.title, artist = track.artistName, album = track.albumTitle)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
// Action row (like, shuffle, repeat, queue, kebab) sits ABOVE the
|
// Action row (like, shuffle, repeat, queue, kebab) sits ABOVE the
|
||||||
// scrubber — Flutter's _SecondaryControls placement
|
// scrubber. Android previously had it below the transport row.
|
||||||
// (now_playing_screen.dart:464). Android previously had it below
|
|
||||||
// the transport row.
|
|
||||||
BottomActionsRow(
|
BottomActionsRow(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
track = track,
|
track = track,
|
||||||
|
|||||||
+2
-3
@@ -88,7 +88,7 @@ class PlaylistsRepository @Inject constructor(
|
|||||||
// Reconcile: BuildSystemPlaylists rotates system-playlist
|
// Reconcile: BuildSystemPlaylists rotates system-playlist
|
||||||
// UUIDs every rebuild, so upsert alone leaves stale rows
|
// UUIDs every rebuild, so upsert alone leaves stale rows
|
||||||
// whose detail fetch 404s ("That playlist no longer
|
// whose detail fetch 404s ("That playlist no longer
|
||||||
// exists"). Mirrors playlists_provider.dart's deleteWhere.
|
// exists").
|
||||||
playlistDao.replaceList(
|
playlistDao.replaceList(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
freshOwnedIds = wire.owned.map { it.id },
|
freshOwnedIds = wire.owned.map { it.id },
|
||||||
@@ -111,8 +111,7 @@ class PlaylistsRepository @Inject constructor(
|
|||||||
api.get(id)
|
api.get(id)
|
||||||
} catch (e: HttpException) {
|
} catch (e: HttpException) {
|
||||||
// Server says this playlist is gone — drop the stale cache
|
// Server says this playlist is gone — drop the stale cache
|
||||||
// row so the list stops showing it. Mirrors
|
// row so the list stops showing it.
|
||||||
// playlists_provider.dart's deleteWhere on detail failure.
|
|
||||||
if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
|
if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||||
playlistDao.deleteByIds(listOf(id))
|
playlistDao.deleteByIds(listOf(id))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
|||||||
/**
|
/**
|
||||||
* One playlist tile. Sized to match `AlbumCard` (176dp wide, 144dp
|
* One playlist tile. Sized to match `AlbumCard` (176dp wide, 144dp
|
||||||
* square cover) so they line up in the Home Playlists carousel.
|
* square cover) so they line up in the Home Playlists carousel.
|
||||||
* Mirrors `flutter_client/lib/playlists/widgets/playlist_card.dart`.
|
|
||||||
*
|
*
|
||||||
* System playlists carry a small "For You" / "Discover" / etc. label
|
* System playlists carry a small "For You" / "Discover" / etc. label
|
||||||
* subtitle under the name (substituting for the artist-name line on
|
* subtitle under the name (substituting for the artist-name line on
|
||||||
|
|||||||
-1
@@ -28,7 +28,6 @@ import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder tile for a system playlist that hasn't generated yet.
|
* Placeholder tile for a system playlist that hasn't generated yet.
|
||||||
* Mirrors `flutter_client/lib/playlists/widgets/playlist_placeholder_card.dart`.
|
|
||||||
* Sized to match [PlaylistCard] so the Home Playlists row stays
|
* Sized to match [PlaylistCard] so the Home Playlists row stays
|
||||||
* visually consistent.
|
* visually consistent.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ private fun CancelConfirmDialog(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun KindAvatar(kind: String) {
|
private fun KindAvatar(kind: String) {
|
||||||
// Flutter mapping: disc-3 for artist, library-big for album,
|
// Flutter mapping: disc-3 for artist, library-big for album,
|
||||||
// music for track. Mirrors lib/requests/requests_screen.dart.
|
// music for track.
|
||||||
val icon = when (kind) {
|
val icon = when (kind) {
|
||||||
"artist" -> Lucide.Disc3
|
"artist" -> Lucide.Disc3
|
||||||
"album" -> Lucide.LibraryBig
|
"album" -> Lucide.LibraryBig
|
||||||
|
|||||||
-3
@@ -15,9 +15,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Labeled horizontal-scroll section used throughout the Home screen.
|
* Labeled horizontal-scroll section used throughout the Home screen.
|
||||||
* Mirrors `flutter_client/lib/library/widgets/horizontal_scroll_row.dart`
|
|
||||||
* — a Fraunces section title at 16dp gutter, then a horizontal LazyRow
|
|
||||||
* with the same gutter and 8dp inter-item spacing.
|
|
||||||
*
|
*
|
||||||
* Pass empty `title` to render a continuation row directly under a
|
* Pass empty `title` to render a continuation row directly under a
|
||||||
* previously-titled one (used by Rediscover when it has both album and
|
* previously-titled one (used by Rediscover when it has both album and
|
||||||
|
|||||||
+1
-4
@@ -24,10 +24,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Always-visible circular play button overlaid on Home tile cover art
|
* Always-visible circular play button overlaid on Home tile cover art
|
||||||
* (AlbumCard / ArtistCard / PlaylistCard). Mirrors
|
* (AlbumCard / ArtistCard / PlaylistCard).
|
||||||
* `flutter_client/lib/library/widgets/play_circle_button.dart`: 44dp
|
|
||||||
* accent-colored disc, parchment Play icon, drop shadow, self-managed
|
|
||||||
* loading spinner.
|
|
||||||
*
|
*
|
||||||
* [onPlay] is a suspend lambda so the caller can await the fetch-and-
|
* [onPlay] is a suspend lambda so the caller can await the fetch-and-
|
||||||
* queue setup. While it runs, the icon is swapped for a spinner and
|
* queue setup. While it runs, the icon is swapped for a spinner and
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ val ShellContentWindowInsets: WindowInsets = WindowInsets(0, 0, 0, 0)
|
|||||||
* its own Scaffold's snackbar host as well; this one is the shell-
|
* its own Scaffold's snackbar host as well; this one is the shell-
|
||||||
* wide catch-all for events that outlive a single screen).
|
* wide catch-all for events that outlive a single screen).
|
||||||
*
|
*
|
||||||
* Mirrors the Flutter `_ShellWithPlayerBar` (lib/shared/routing.dart).
|
|
||||||
*
|
|
||||||
* Top-level routes (Home / Library / Search / Discover / Playlists /
|
* Top-level routes (Home / Library / Search / Discover / Playlists /
|
||||||
* Settings / Admin / detail screens) wrap themselves with this.
|
* Settings / Admin / detail screens) wrap themselves with this.
|
||||||
* Full-screen routes (NowPlaying / Queue / ServerUrl / Login)
|
* Full-screen routes (NowPlaying / Queue / ServerUrl / Login)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Flutter theme's `ColorScheme` role mapping (theme_data.dart):
|
* The `ColorScheme` role mapping:
|
||||||
* primary = accent (forest-teal) — same both modes
|
* primary = accent (forest-teal) — same both modes
|
||||||
* onPrimary = onAction
|
* onPrimary = onAction
|
||||||
* surface = iron
|
* surface = iron
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ package api
|
|||||||
// pgtype.Timestamptz / Date into strings, mirroring the pattern
|
// pgtype.Timestamptz / Date into strings, mirroring the pattern
|
||||||
// playlistRowView already established for /api/playlists.
|
// playlistRowView already established for /api/playlists.
|
||||||
//
|
//
|
||||||
// Field names match flutter_client/lib/cache/sync_controller.dart's
|
// Field names match what the Android client deserialises in
|
||||||
// _*FromJson helpers exactly. Adding a field server-side requires a
|
// models/wire/SyncResponseWire.kt (Sync{Artist,Album,Track}Wire) exactly.
|
||||||
// matching read in the Flutter helper or it'll be silently dropped.
|
// Adding a field server-side requires a matching @SerialName there or it is
|
||||||
|
// silently dropped — kotlinx.serialization ignores unknown keys, so the
|
||||||
|
// failure is a missing value at runtime, not an error at parse time.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import (
|
|||||||
|
|
||||||
// These tests pin the wire-format keys for /api/library/sync upserts.
|
// These tests pin the wire-format keys for /api/library/sync upserts.
|
||||||
// Without them, sqlc model field-name drift or accidental
|
// Without them, sqlc model field-name drift or accidental
|
||||||
// `json.Marshal(rawStruct)` regressions would silently break the
|
// `json.Marshal(rawStruct)` regressions would silently break the Android
|
||||||
// Flutter client (which reads snake_case keys via _*FromJson helpers
|
// client, which reads these snake_case keys via @SerialName in
|
||||||
// in flutter_client/lib/cache/sync_controller.dart).
|
// models/wire/SyncResponseWire.kt. "Silently" is the operative word:
|
||||||
|
// kotlinx.serialization skips unknown keys, so a renamed field arrives as
|
||||||
|
// a default value rather than an error.
|
||||||
|
|
||||||
// validUUID is a deterministic test UUID — not a real value, just
|
// validUUID is a deterministic test UUID — not a real value, just
|
||||||
// something that pgtype.UUID.Valid will accept.
|
// something that pgtype.UUID.Valid will accept.
|
||||||
|
|||||||
Reference in New Issue
Block a user