@omelhorsite/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +112 -130
  2. package/dist/index.js +4800 -610
  3. package/dist/types/client.d.ts +60 -3
  4. package/dist/types/http.d.ts +444 -19
  5. package/dist/types/index.d.ts +5 -1
  6. package/dist/types/internal/attachments.d.ts +17 -0
  7. package/dist/types/internal/helpers.d.ts +9 -0
  8. package/dist/types/listing.d.ts +90 -0
  9. package/dist/types/resources/account.d.ts +71 -5
  10. package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
  11. package/dist/types/resources/admin/chests.d.ts +51 -0
  12. package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
  13. package/dist/types/resources/admin/identities.d.ts +99 -0
  14. package/dist/types/resources/admin/index.d.ts +77 -0
  15. package/dist/types/resources/admin/jobs.d.ts +97 -0
  16. package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
  17. package/dist/types/resources/admin/notepads.d.ts +49 -0
  18. package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
  19. package/dist/types/resources/admin/quotas.d.ts +114 -0
  20. package/dist/types/resources/admin/shortLinks.d.ts +201 -0
  21. package/dist/types/resources/admin/types.d.ts +222 -0
  22. package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
  23. package/dist/types/resources/admin.d.ts +1841 -0
  24. package/dist/types/resources/auth/index.d.ts +39 -0
  25. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  26. package/dist/types/resources/auth/sessions.d.ts +844 -0
  27. package/dist/types/resources/chests.d.ts +54 -3
  28. package/dist/types/resources/content/analysis.d.ts +62 -0
  29. package/dist/types/resources/content/blogs.d.ts +471 -0
  30. package/dist/types/resources/content/feedbacks.d.ts +263 -0
  31. package/dist/types/resources/content/index.d.ts +52 -0
  32. package/dist/types/resources/content/intel/articles.d.ts +230 -0
  33. package/dist/types/resources/content/intel/config.d.ts +135 -0
  34. package/dist/types/resources/content/intel/index.d.ts +53 -0
  35. package/dist/types/resources/content/intel/items.d.ts +91 -0
  36. package/dist/types/resources/content/intel/reports.d.ts +108 -0
  37. package/dist/types/resources/content/intel/scripts.d.ts +152 -0
  38. package/dist/types/resources/content/intel/sources.d.ts +236 -0
  39. package/dist/types/resources/content/intel/stats.d.ts +105 -0
  40. package/dist/types/resources/content/intel/types.d.ts +86 -0
  41. package/dist/types/resources/content/jokes.d.ts +93 -0
  42. package/dist/types/resources/content/notifications.d.ts +147 -0
  43. package/dist/types/resources/content/serviceUsages.d.ts +73 -0
  44. package/dist/types/resources/content/servicesStatus.d.ts +194 -0
  45. package/dist/types/resources/content/siteConfig.d.ts +43 -0
  46. package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
  47. package/dist/types/resources/content.d.ts +2775 -0
  48. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  49. package/dist/types/resources/forms.d.ts +176 -35
  50. package/dist/types/resources/index.d.ts +19 -8
  51. package/dist/types/resources/ipLookup.d.ts +20 -4
  52. package/dist/types/resources/jobs.d.ts +69 -25
  53. package/dist/types/resources/library/annotations.d.ts +162 -0
  54. package/dist/types/resources/library/books.d.ts +534 -0
  55. package/dist/types/resources/library/chat.d.ts +259 -0
  56. package/dist/types/resources/library/index.d.ts +39 -0
  57. package/dist/types/resources/library/shelves.d.ts +237 -0
  58. package/dist/types/resources/library/types.d.ts +120 -0
  59. package/dist/types/resources/library.d.ts +1433 -0
  60. package/dist/types/resources/linkTrees.d.ts +142 -30
  61. package/dist/types/resources/media.d.ts +351 -0
  62. package/dist/types/resources/movies/addons.d.ts +388 -0
  63. package/dist/types/resources/movies/collections.d.ts +352 -0
  64. package/dist/types/resources/movies/index.d.ts +29 -0
  65. package/dist/types/resources/movies/types.d.ts +50 -0
  66. package/dist/types/resources/movies/watchProgress.d.ts +277 -0
  67. package/dist/types/resources/movies.d.ts +1160 -0
  68. package/dist/types/resources/music/artists.d.ts +1068 -0
  69. package/dist/types/resources/music/imports.d.ts +941 -0
  70. package/dist/types/resources/music/index.d.ts +61 -0
  71. package/dist/types/resources/music/playlists.d.ts +1027 -0
  72. package/dist/types/resources/music/social.d.ts +1132 -0
  73. package/dist/types/resources/music/songs.d.ts +1167 -0
  74. package/dist/types/resources/notepads.d.ts +4 -1
  75. package/dist/types/resources/quotas.d.ts +7 -1
  76. package/dist/types/resources/realtime.d.ts +855 -0
  77. package/dist/types/resources/shortLinks.d.ts +49 -5
  78. package/dist/types/resources/social/groupChats.d.ts +458 -0
  79. package/dist/types/resources/social/index.d.ts +19 -0
  80. package/dist/types/resources/social/messages.d.ts +424 -0
  81. package/dist/types/resources/social/relationships.d.ts +322 -0
  82. package/dist/types/resources/social/types.d.ts +37 -0
  83. package/dist/types/resources/social.d.ts +1348 -0
  84. package/dist/types/resources/storage/upload.d.ts +158 -11
  85. package/dist/types/resources/storage.d.ts +111 -27
  86. package/dist/types/resources/tickets.d.ts +101 -13
  87. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  88. package/dist/types/resources/tools/captions.d.ts +448 -21
  89. package/dist/types/resources/tools/downloader.d.ts +21 -0
  90. package/dist/types/resources/tools/index.d.ts +57 -15
  91. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  92. package/dist/types/resources/tools/transcription.d.ts +35 -13
  93. package/dist/types/resources/tools/upscale.d.ts +23 -3
  94. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  95. package/dist/types/types.d.ts +249 -17
  96. package/package.json +2 -1
@@ -0,0 +1,1027 @@
1
+ /**
2
+ * The `music.playlists` namespace: playlists, the join rows inside them, the
3
+ * generated shelves (mixes and radios) and the play history.
4
+ *
5
+ * Five endpoint families live here because they are one screen's worth of API:
6
+ * a playlist, the queue you build from it, the shelves the server builds for
7
+ * you, and the history that feeds those shelves. They are exposed as one entry
8
+ * class with three sub-namespaces hanging off it
9
+ * ({@link MusicPlaylistsNamespace.songs}, `.mixes`, `.radios`, `.plays`), and
10
+ * every sub-namespace is also exported on its own so a host that prefers
11
+ * `oms.music.mixes` can mount it there instead.
12
+ *
13
+ * ## Four things that have already cost bugs
14
+ *
15
+ * 1. **Every id here is an INTEGER.** Playlists, playlist songs, songs,
16
+ * artists and play events all kept auto-increment primary keys, so they
17
+ * arrive as JSON numbers while the `user_id` right next to them is a
18
+ * string. {@link MusicPlaylistsNamespace.reorder} is the one place where
19
+ * that distinction is load-bearing rather than cosmetic - see its docs.
20
+ * 2. **System playlists are half read-only.** A playlist whose `source_kind`
21
+ * is present and not `"manual"` is maintained by an external sync
22
+ * (Spotify today). Renaming it, re-arting it and reordering it are refused
23
+ * with `401`; adding, removing, hiding and copying are NOT. That split is
24
+ * newer than `docs/api-music.md`, which still says every structural edit is
25
+ * refused. Test with {@link isSystemPlaylist}.
26
+ * 3. **Removing a song from a synced playlist does not delete anything.** The
27
+ * sync would just put it back, so the row is marked `hidden` instead and
28
+ * the API still answers `204`. The row keeps coming back in listings for
29
+ * the OWNER (so it can be un-hidden) and disappears for everyone else. See
30
+ * {@link PlaylistSongsNamespace.remove}.
31
+ * 4. **`/music_radios/*` is throttled at 60 requests per minute**, and the
32
+ * bucket is keyed by the `Authorization` header - or by the client IP when
33
+ * there is none. A cookie-authenticated web client therefore shares one
34
+ * budget with every other visitor behind the same address. See
35
+ * {@link MusicRadiosNamespace}.
36
+ */
37
+ import { Resource } from "../../http";
38
+ import type { ListParams } from "../../listing";
39
+ import type { FileInput, Id, NativeFile, Paginated, RequestOptions, Timestamp } from "../../types";
40
+ /**
41
+ * Primary key of a playlist. A NUMBER: `playlists` never moved to the opaque
42
+ * string ids the account-side tables use.
43
+ */
44
+ export type MusicPlaylistId = number;
45
+ /**
46
+ * Primary key of a `playlist_songs` join ROW, which is not the id of the song
47
+ * on it. `DELETE /playlist_songs/:id` wants this one; every method here that
48
+ * takes a `rowId` means this and says so.
49
+ */
50
+ export type MusicPlaylistSongId = number;
51
+ /** Primary key of a play event. A number, like everything else in this file. */
52
+ export type PlayEventId = number;
53
+ /** Visibility of a playlist. There is no `public`: the widest setting is `friends`. */
54
+ export type PlaylistVisibility = "private" | "friends";
55
+ /**
56
+ * Where a playlist came from. `"manual"` (or `null` on very old rows) is a
57
+ * playlist the user built; anything else marks it as maintained by a sync and
58
+ * makes {@link isSystemPlaylist} true. The column is a free string, so treat
59
+ * unknown values as system rather than as a bug.
60
+ */
61
+ export type PlaylistSourceKind = "manual" | "imported" | "spotify_sync" | (string & {});
62
+ /**
63
+ * Which side created a join row. `"sync"` rows belong to the external sync and
64
+ * are hidden rather than deleted; `"manual"` rows are the user's own additions
65
+ * and are deleted for real.
66
+ */
67
+ export type PlaylistSongOrigin = "sync" | "manual";
68
+ /**
69
+ * Positions at or above this floor are the manual block of a synced playlist.
70
+ *
71
+ * The sync numbers its own rows from 1 upwards and never reaches here, so a
72
+ * user's additions to a synced playlist are parked above the floor and the
73
+ * next sync run can renumber its own rows without colliding with them.
74
+ * Mirrors `PlaylistSong::MANUAL_BLOCK_FLOOR`.
75
+ */
76
+ export declare const PLAYLIST_MANUAL_BLOCK_FLOOR = 100000;
77
+ /**
78
+ * Most songs `POST /playlists` will seed from `song_ids` in one call. Mirrors
79
+ * `PlaylistsController::SEED_CAP`; the server takes the first 500 and drops
80
+ * the rest in silence, so the SDK raises instead.
81
+ */
82
+ export declare const PLAYLIST_SEED_CAP = 500;
83
+ /**
84
+ * Requests per minute allowed against `/music_radios/*`. See
85
+ * {@link MusicRadiosNamespace} for what the bucket is keyed on, which matters
86
+ * more than the number.
87
+ */
88
+ export declare const MUSIC_RADIO_RATE_LIMIT_PER_MINUTE = 60;
89
+ /**
90
+ * Window in which a repeat play of the same song is swallowed rather than
91
+ * recorded. Mirrors `PlayEvent::DEDUPE_WINDOW`.
92
+ */
93
+ export declare const PLAY_EVENT_DEDUPE_WINDOW_MS = 30000;
94
+ /**
95
+ * Client labels the backend will store on a play event.
96
+ *
97
+ * A value outside this list does NOT fail the request: the controller records
98
+ * the play with a `null` source rather than losing real listening history over
99
+ * a typo. So a misspelt label is invisible until somebody audits by origin and
100
+ * finds a pile of unlabelled rows.
101
+ */
102
+ export declare const PLAY_EVENT_SOURCES: readonly ["oms-ios", "oms-desktop", "web"];
103
+ /** One of {@link PLAY_EVENT_SOURCES}. */
104
+ export type PlayEventSource = (typeof PLAY_EVENT_SOURCES)[number];
105
+ /** Windows `GET /play_events/top` accepts. Anything else is a `400`. */
106
+ export declare const PLAY_EVENT_TOP_WINDOWS: readonly ["7d", "30d", "90d", "all"];
107
+ /** One of {@link PLAY_EVENT_TOP_WINDOWS}. */
108
+ export type PlayEventWindow = (typeof PLAY_EVENT_TOP_WINDOWS)[number];
109
+ /** Ceiling on `limit` for both `/play_events/recent` and `/play_events/top`. */
110
+ export declare const PLAY_EVENT_MAX_LIMIT = 100;
111
+ /** Kinds of generated mix the server currently produces. */
112
+ export declare const MIX_KINDS: readonly ["top_artist", "this_is", "monthly_rewind", "year_mix", "repeat_rewind", "time_capsule", "discoveries"];
113
+ /**
114
+ * Kind of a generated mix. Widened with `string & {}` on purpose: the
115
+ * generator gains kinds faster than any client is redeployed, and a mix whose
116
+ * kind is unknown still renders perfectly well from its title and its songs.
117
+ */
118
+ export type MixKind = (typeof MIX_KINDS)[number] | (string & {});
119
+ /**
120
+ * The compact `ArtistBlueprint` view, as it is embedded in mixes and in the
121
+ * album/artist rows of the history endpoints.
122
+ *
123
+ * Blueprinter views ADD to the base rather than replace it, so `compact`
124
+ * carries `id`, `created_at` and `updated_at` too. The `*_fs_node_id` keys are
125
+ * a deliberate duplicate of the `*_media_id` ones, kept for the old web
126
+ * frontend; read the `_media_id` form in new code.
127
+ */
128
+ export interface MusicArtistPayload {
129
+ readonly id: number;
130
+ readonly created_at: Timestamp;
131
+ readonly updated_at: Timestamp;
132
+ readonly name: string;
133
+ readonly slug: string;
134
+ readonly image_media_id: string | null;
135
+ readonly compressed_image_media_id: string | null;
136
+ /** @deprecated Legacy twin of `image_media_id`, same value. */
137
+ readonly image_fs_node_id?: string | null;
138
+ /** @deprecated Legacy twin of `compressed_image_media_id`, same value. */
139
+ readonly compressed_image_fs_node_id?: string | null;
140
+ }
141
+ /** One artist credit on a song, as `SongArtistBlueprint` renders it. */
142
+ export interface MusicSongArtistPayload {
143
+ readonly id: number;
144
+ readonly created_at: Timestamp;
145
+ readonly updated_at: Timestamp;
146
+ readonly song_id: number;
147
+ readonly artist_id: number;
148
+ /** 0 for the lead credit; featured artists follow. */
149
+ readonly position: number;
150
+ readonly role: string | null;
151
+ readonly name: string | null;
152
+ readonly slug: string | null;
153
+ readonly image_media_id: string | null;
154
+ readonly compressed_image_media_id: string | null;
155
+ /** Cached Deezer picture. This, not the uploads, is what usually renders. */
156
+ readonly picture: string | null;
157
+ readonly picture_medium: string | null;
158
+ readonly external_image_url: string | null;
159
+ }
160
+ /**
161
+ * A song, as it is embedded in playlist rows, mixes, radios and history.
162
+ *
163
+ * This is a deliberate SUBSET of what `SongBlueprint` sends: the fields every
164
+ * consumer in this file needs, and no more. The full song type belongs to the
165
+ * songs namespace, and duplicating thirty audio-metadata columns in two places
166
+ * is how the two copies drift apart. Extra keys ARE on the wire - cast when
167
+ * you need `isrc`, the codec fields or the stem media ids.
168
+ */
169
+ export interface MusicSongPayload {
170
+ readonly id: number;
171
+ readonly created_at: Timestamp;
172
+ readonly updated_at: Timestamp;
173
+ readonly title: string;
174
+ readonly album: string | null;
175
+ /** Seconds. `null` or `0` on old uploads and on failed probes. */
176
+ readonly duration: number | null;
177
+ readonly year: number | null;
178
+ readonly user_id: Id;
179
+ readonly artists: readonly MusicSongArtistPayload[];
180
+ readonly audio_media_id: string | null;
181
+ readonly compressed_audio_media_id: string | null;
182
+ readonly artwork_media_id: string | null;
183
+ readonly compressed_artwork_media_id: string | null;
184
+ }
185
+ /**
186
+ * A playlist.
187
+ *
188
+ * `visibility` and `owned` are newer than `docs/api-music.md` and than the
189
+ * `Playlist` type in `oms-music/src/domain/playlist.ts`; both are on the wire
190
+ * today. `owned` is computed against the ASKING user, which is what makes a
191
+ * followed playlist distinguishable from your own in one listing.
192
+ */
193
+ export interface MusicPlaylist {
194
+ readonly id: MusicPlaylistId;
195
+ readonly created_at: Timestamp;
196
+ readonly updated_at: Timestamp;
197
+ readonly name: string;
198
+ /** Owner. A string id, unlike every other id in this file. */
199
+ readonly user_id: Id;
200
+ readonly visibility: PlaylistVisibility;
201
+ /**
202
+ * False for a playlist you FOLLOW rather than own. Everything that writes
203
+ * refuses on those, so this is the flag a UI hides its edit affordances on.
204
+ */
205
+ readonly owned: boolean;
206
+ readonly source_kind: PlaylistSourceKind | null;
207
+ /** e.g. `"spotify"`. Used verbatim in the refusal message on a system playlist. */
208
+ readonly source_provider: string | null;
209
+ readonly source_url: string | null;
210
+ /** `"liked"` marks the mirror of the provider's liked-tracks list. */
211
+ readonly source_external_id: string | null;
212
+ readonly synced_at: Timestamp | null;
213
+ /** Attachment id of the cover, as a string, or `null`. */
214
+ readonly artwork_media_id: string | null;
215
+ /** @deprecated Legacy twin of `artwork_media_id`, same value. */
216
+ readonly artwork_fs_node_id?: string | null;
217
+ }
218
+ /** One song's membership of one playlist. */
219
+ export interface MusicPlaylistSong {
220
+ /** The JOIN ROW id. Not the song id. This is what `remove` takes. */
221
+ readonly id: MusicPlaylistSongId;
222
+ readonly created_at: Timestamp;
223
+ readonly updated_at: Timestamp;
224
+ readonly playlist_id: MusicPlaylistId;
225
+ readonly song_id: number;
226
+ /**
227
+ * Sort key inside the playlist. Not dense and not stable: appends take
228
+ * `max + 1`, seeds start at 1, {@link MusicPlaylistsNamespace.reorder}
229
+ * renumbers from 0, and manual rows on a synced playlist start at
230
+ * {@link PLAYLIST_MANUAL_BLOCK_FLOOR}. Order by it; never do arithmetic
231
+ * with it.
232
+ */
233
+ readonly position: number;
234
+ readonly origin: PlaylistSongOrigin;
235
+ /**
236
+ * A `sync` row the owner removed. It stays so the next sync run finds it and
237
+ * leaves it alone. Only the OWNER ever sees a hidden row: the listing filters
238
+ * `hidden = FALSE OR playlists.user_id = <you>`, so a follower's page count
239
+ * and the owner's differ for the same playlist.
240
+ */
241
+ readonly hidden: boolean;
242
+ /** The full song. Present on every row of every view. */
243
+ readonly song: MusicSongPayload;
244
+ }
245
+ /** A generated shelf, without its songs. */
246
+ export interface MusicMixSummary {
247
+ /**
248
+ * Identity of the shelf, e.g. `"mix:top_artist:1:ab12cd34"`. It contains
249
+ * colons, so it MUST be percent-encoded into the path -
250
+ * {@link MusicMixesNamespace.get} does that for you.
251
+ */
252
+ readonly slug: string;
253
+ readonly kind: MixKind;
254
+ /** English fallback. Render {@link title_key} instead where you have i18n. */
255
+ readonly title: string;
256
+ /** English fallback. Render {@link description_key} instead. */
257
+ readonly description: string;
258
+ readonly title_key: string;
259
+ readonly title_params: Readonly<Record<string, string | number>>;
260
+ readonly description_key: string;
261
+ readonly description_params: Readonly<Record<string, string | number>>;
262
+ /** What the mix was built around: an artist name, a year, or `null`. */
263
+ readonly seed: string | number | null;
264
+ /**
265
+ * Resolved at render time rather than cached, so a refreshed picture shows up
266
+ * without waiting out the 24h cache. Only `top_artist` and `this_is` mixes
267
+ * carry one.
268
+ */
269
+ readonly artist: MusicArtistPayload | null;
270
+ /** Tailwind gradient classes for the card. Clients are free to ignore it. */
271
+ readonly gradient: string | null;
272
+ }
273
+ /** A generated shelf with its songs, in mix order. */
274
+ export interface MusicMix extends MusicMixSummary {
275
+ readonly songs: readonly MusicSongPayload[];
276
+ }
277
+ /** An artist or song radio. */
278
+ export interface MusicRadio {
279
+ /** `"radio:artist:<hash>"` or `"radio:song:<song id>"`. */
280
+ readonly slug: string;
281
+ readonly kind: "artist" | "song";
282
+ /**
283
+ * Pre-baked and PORTUGUESE, unlike a mix, which ships i18n keys. There is no
284
+ * `title_key` here to render instead, so a non-Portuguese UI either shows
285
+ * Portuguese or builds its own title from `seed`.
286
+ */
287
+ readonly title: string;
288
+ readonly description: string;
289
+ /** Seed artist name, or the seed song's title for a song radio. */
290
+ readonly seed: string | number;
291
+ readonly gradient: string | null;
292
+ /**
293
+ * Around 40 tracks, all from the caller's own library. For a song radio the
294
+ * seed song is guaranteed to be `songs[0]`.
295
+ */
296
+ readonly songs: readonly MusicSongPayload[];
297
+ }
298
+ /** A recorded play. */
299
+ export interface MusicPlayEvent {
300
+ readonly id: PlayEventId;
301
+ readonly created_at: Timestamp;
302
+ readonly updated_at: Timestamp;
303
+ readonly user_id: Id;
304
+ readonly song_id: number;
305
+ readonly played_at: Timestamp;
306
+ /** `null` when the client sent nothing, or sent a label off the whitelist. */
307
+ readonly source: PlayEventSource | null;
308
+ /** Seconds actually listened, clamped server-side. `null` when not reported. */
309
+ readonly listened_s: number | null;
310
+ /** The full song. `POST /play_events` renders the `:extended` view. */
311
+ readonly song: MusicSongPayload;
312
+ }
313
+ /** What the server answers when a play was inside the dedupe window. */
314
+ export interface PlayEventDeduped {
315
+ readonly deduped: true;
316
+ }
317
+ /** Either a stored event, or the marker saying the play was swallowed. */
318
+ export type RecordPlayResult = MusicPlayEvent | PlayEventDeduped;
319
+ /** One row of `GET /play_events/recent?group_by=song`. */
320
+ export interface RecentSongPlay {
321
+ readonly song: MusicSongPayload;
322
+ readonly last_played_at: Timestamp;
323
+ }
324
+ /** One row of `GET /play_events/recent?group_by=album`. */
325
+ export interface RecentAlbumPlay {
326
+ readonly album: string | null;
327
+ /**
328
+ * Lead artist of the album, compact view, or `null`.
329
+ *
330
+ * `oms-music/src/api/endpoints/playEvents.ts` types this as
331
+ * `Artist | string | null` and calls the string case "legacy rows". No such
332
+ * case exists in the Rails this was checked against: the album grouping joins
333
+ * `song_artists` and renders a blueprint or `nil`, never a bare name.
334
+ */
335
+ readonly artist: MusicArtistPayload | null;
336
+ readonly artwork_media_id: string | null;
337
+ /** @deprecated Legacy twin of `artwork_media_id`, same value. */
338
+ readonly artwork_fs_node_id?: string | null;
339
+ readonly last_played_at: Timestamp;
340
+ }
341
+ /** One row of `GET /play_events/top?scope=song`. */
342
+ export interface TopSongRow {
343
+ readonly song: MusicSongPayload;
344
+ readonly play_count: number;
345
+ }
346
+ /** One row of `GET /play_events/top?scope=album`. */
347
+ export interface TopAlbumRow {
348
+ readonly album: string | null;
349
+ readonly artist: MusicArtistPayload | null;
350
+ readonly artwork_media_id: string | null;
351
+ /** @deprecated Legacy twin of `artwork_media_id`, same value. */
352
+ readonly artwork_fs_node_id?: string | null;
353
+ readonly play_count: number;
354
+ }
355
+ /** One row of `GET /play_events/top?scope=artist`. */
356
+ export interface TopArtistRow {
357
+ readonly artist: MusicArtistPayload;
358
+ readonly play_count: number;
359
+ }
360
+ /** Filter columns of `GET /playlists`. */
361
+ export declare const PLAYLIST_FILTER_COLUMNS: readonly ["id", "name", "created_at", "updated_at"];
362
+ /** Filters for {@link MusicPlaylistsNamespace.list}. */
363
+ export interface ListPlaylistsParams extends ListParams<(typeof PLAYLIST_FILTER_COLUMNS)[number]> {
364
+ /**
365
+ * Partial, accent-insensitive match on the name, sent as `search[name]`.
366
+ *
367
+ * There is no filter for the owner and none for the visibility: the
368
+ * controller's allowlist is `id`, `name`, `created_at`, `updated_at` and
369
+ * nothing else, and an unrecognised filter key is a `400`, not a wider
370
+ * result. Use the `owned` flag on each row to tell yours from the ones you
371
+ * follow.
372
+ */
373
+ readonly name?: string;
374
+ /** Exact ids, sent as `exact_search[id][]`, which the backend turns into `IN`. */
375
+ readonly ids?: readonly MusicPlaylistId[];
376
+ }
377
+ /** Arguments for {@link MusicPlaylistsNamespace.create}. */
378
+ export interface CreatePlaylistInput {
379
+ readonly name: string;
380
+ /** Defaults to `"private"` server-side. */
381
+ readonly visibility?: PlaylistVisibility;
382
+ /**
383
+ * Reuses an EXISTING attachment as the cover - typically a song's
384
+ * `artwork_media_id`. No bytes are copied and the blob counts once against
385
+ * the music quota. To upload a new image instead, create the playlist and
386
+ * then call {@link MusicPlaylistsNamespace.uploadArtwork}.
387
+ */
388
+ readonly artworkMediaId?: string;
389
+ /**
390
+ * Songs to seed the playlist with, in order. This is what "save this radio
391
+ * as a playlist" uses.
392
+ *
393
+ * Ids you cannot see are dropped in SILENCE and the playlist is still
394
+ * created, so compare `song_ids.length` against what you read back if that
395
+ * matters. Positions start at 1. At most {@link PLAYLIST_SEED_CAP} ids; the
396
+ * server truncates, the SDK throws.
397
+ */
398
+ readonly songIds?: readonly number[];
399
+ }
400
+ /** Fields {@link MusicPlaylistsNamespace.update} can change. */
401
+ export interface UpdatePlaylistInput {
402
+ /** Refused with `401` on a system playlist: the sync owns the name. */
403
+ readonly name?: string;
404
+ /** Always editable, system playlist or not: visibility belongs to the owner. */
405
+ readonly visibility?: PlaylistVisibility;
406
+ /**
407
+ * Point the cover at an existing attachment, or pass `null` to purge the
408
+ * current one. Refused with `401` on a system playlist.
409
+ *
410
+ * Omitting the key and passing `null` are different requests: the server
411
+ * tests `params.key?`, so an absent key leaves the artwork alone and an
412
+ * explicit `null` deletes it.
413
+ */
414
+ readonly artworkMediaId?: string | null;
415
+ }
416
+ /** Filter columns of `GET /playlist_songs`. */
417
+ export declare const PLAYLIST_SONG_FILTER_COLUMNS: readonly ["id", "playlist_id", "song_id", "position", "origin", "hidden", "created_at", "updated_at"];
418
+ /** Filters for {@link PlaylistSongsNamespace.list}. */
419
+ export interface ListPlaylistSongsParams extends ListParams<(typeof PLAYLIST_SONG_FILTER_COLUMNS)[number]> {
420
+ readonly playlistId?: MusicPlaylistId;
421
+ readonly songId?: number;
422
+ readonly ids?: readonly MusicPlaylistSongId[];
423
+ readonly origin?: PlaylistSongOrigin;
424
+ /**
425
+ * Narrow to hidden or to visible rows. Note that only the owner ever sees a
426
+ * hidden row at all, so `hidden: true` is an empty page for a follower.
427
+ */
428
+ readonly hidden?: boolean;
429
+ }
430
+ /** Arguments for {@link PlayEventsNamespace.record}. */
431
+ export interface RecordPlayInput {
432
+ readonly songId: number;
433
+ /**
434
+ * Which client is reporting. Off-whitelist values are stored as `null`
435
+ * rather than rejected, so a typo costs you the label and nothing else.
436
+ * See {@link PLAY_EVENT_SOURCES}.
437
+ */
438
+ readonly source?: PlayEventSource;
439
+ /**
440
+ * Seconds actually listened. Clamped server-side to `0 .. 3x` the track
441
+ * duration, or to 24 hours when the duration is unknown, so a runaway
442
+ * accumulator is capped rather than refused.
443
+ */
444
+ readonly listenedSeconds?: number;
445
+ }
446
+ /** Arguments for the `/play_events/recent` reads. */
447
+ export interface RecentPlaysParams {
448
+ /** 1 to {@link PLAY_EVENT_MAX_LIMIT}. Defaults to 24 server-side. */
449
+ readonly limit?: number;
450
+ }
451
+ /** Arguments for the `/play_events/top` reads. */
452
+ export interface TopPlaysParams {
453
+ /** 1 to {@link PLAY_EVENT_MAX_LIMIT}. Defaults to 10 server-side. */
454
+ readonly limit?: number;
455
+ /** Defaults to `"all"`. Anything outside {@link PLAY_EVENT_TOP_WINDOWS} is a `400`. */
456
+ readonly since?: PlayEventWindow;
457
+ }
458
+ /** Arguments for {@link PlayEventsNamespace.topSongs}. */
459
+ export interface TopSongsParams extends TopPlaysParams {
460
+ /**
461
+ * Narrow to one artist by NAME - the backend canonicalises it and looks it
462
+ * up in your own roster. A name that matches nothing yields an empty array
463
+ * rather than a `404`, so an empty result does not tell you which of the two
464
+ * happened. Only `scope=song` honours this.
465
+ */
466
+ readonly artist?: string;
467
+ }
468
+ /**
469
+ * True for a playlist an external sync owns. Pure function, no request.
470
+ *
471
+ * The rule is `source_kind` present and not `"manual"`, which means an
472
+ * unrecognised value counts as system - deliberately, since the safe reading of
473
+ * an unknown provider is "something else is writing to this".
474
+ *
475
+ * What it costs you: `update` (name and artwork only), `reorder` and
476
+ * `uploadArtwork` all answer `401`. Adding, removing, hiding and copying still
477
+ * work, and changing the visibility still works.
478
+ */
479
+ export declare function isSystemPlaylist(playlist: Pick<MusicPlaylist, "source_kind">): boolean;
480
+ /**
481
+ * True for the playlist a provider's liked-tracks sync writes into. Pure
482
+ * function, no request. It is a system playlist with the reserved
483
+ * `source_external_id` of `"liked"`, and hosts usually give it their own
484
+ * artwork rather than whatever the sync attached.
485
+ */
486
+ export declare function isLikedMirror(playlist: Pick<MusicPlaylist, "source_kind" | "source_external_id">): boolean;
487
+ /**
488
+ * Narrows the answer of {@link PlayEventsNamespace.record}.
489
+ *
490
+ * The endpoint answers with two different shapes on two different statuses
491
+ * (`200 {"deduped":true}` and `201 <event>`), and the SDK cannot see the status
492
+ * from the parsed body, so the shape is the test.
493
+ */
494
+ export declare function playWasDeduped(result: RecordPlayResult): result is PlayEventDeduped;
495
+ /**
496
+ * The `music.playlists` namespace.
497
+ *
498
+ * Mount it wherever you like; the sub-namespaces are reachable through it and
499
+ * are also exported separately if you would rather mount them at their own
500
+ * paths.
501
+ */
502
+ export declare class MusicPlaylistsNamespace extends Resource {
503
+ /** The join rows: what is inside a playlist, and in what order. */
504
+ readonly songs: PlaylistSongsNamespace;
505
+ /** Generated shelves, refreshed daily. */
506
+ readonly mixes: MusicMixesNamespace;
507
+ /** Artist and song radios. Throttled harder than everything else here. */
508
+ readonly radios: MusicRadiosNamespace;
509
+ /** Play history and the aggregates built on it. */
510
+ readonly plays: PlayEventsNamespace;
511
+ constructor(http: ConstructorParameters<typeof Resource>[0]);
512
+ /**
513
+ * `GET /playlists` - the caller's library: playlists they own, plus the ones
514
+ * they follow while those stay visible.
515
+ *
516
+ * "While those stay visible" is doing real work. A followed playlist leaves
517
+ * the listing the moment its owner sets it back to `private` or the friendship
518
+ * ends, without any event and without the follow being deleted; re-friending
519
+ * brings it back. So a client that caches this list has to treat a
520
+ * disappearance as normal rather than as a deletion.
521
+ *
522
+ * This is NARROWER than what {@link get} will open: a friend's `friends`
523
+ * playlist you have not followed is readable by id but never enumerated here.
524
+ *
525
+ * The default order is `created_at:desc` rather than the server's, which is
526
+ * unspecified. Offset pagination over an unordered query in Postgres can
527
+ * repeat a row on one page and skip it on the next, so the SDK always sends
528
+ * an order. Pass `order` to choose another one.
529
+ *
530
+ * Filters are `name` and `ids` and nothing else - see
531
+ * {@link ListPlaylistsParams.name} for why. Indexes carry an `ETag`, so a
532
+ * conditional GET can come back `304`; the transport does not fabricate a
533
+ * body for that, and no method here sends `If-None-Match` of its own.
534
+ *
535
+ * Ceiling: the general authenticated 600/min.
536
+ */
537
+ list(params?: ListPlaylistsParams, options?: RequestOptions): Promise<Paginated<MusicPlaylist>>;
538
+ /**
539
+ * `GET /playlists/:id` - one playlist.
540
+ *
541
+ * Wider than {@link list}: this opens any playlist you own AND any playlist a
542
+ * friend has set to `friends`, followed or not. That is what makes a shared
543
+ * link work before the recipient has followed anything.
544
+ *
545
+ * @throws {OmsApiError} 404 `"Resource not found"` when the playlist does not
546
+ * exist OR is not visible to you. The two are deliberately indistinguishable.
547
+ */
548
+ get(id: MusicPlaylistId, options?: RequestOptions): Promise<MusicPlaylist>;
549
+ /**
550
+ * `POST /playlists` - creates a playlist, optionally seeded with songs.
551
+ *
552
+ * The seeding branch is the interesting one, and it fails soft on the server:
553
+ * ids you cannot see are dropped without a word, duplicates are collapsed, and
554
+ * the playlist is created regardless. It is done in one transaction AFTER the
555
+ * playlist is saved, so a failure there leaves an empty playlist behind rather
556
+ * than nothing.
557
+ *
558
+ * Not retried on a lost answer (the transport's default for a `POST`), because
559
+ * a replay mints a second playlist. A `429` is still retried, and safely so:
560
+ * this backend refuses before it writes.
561
+ *
562
+ * Ceiling: the general authenticated 600/min.
563
+ *
564
+ * @throws {TypeError} when more than {@link PLAYLIST_SEED_CAP} song ids are
565
+ * passed, or one of them is not an integer. The server would silently keep
566
+ * the first 500 and drop the rest.
567
+ * @throws {OmsApiError} 400 when the name is blank or `visibility` is not one
568
+ * of {@link PlaylistVisibility}, and when `artworkMediaId` does not resolve
569
+ * (`"Invalid artwork media id"`).
570
+ */
571
+ create(input: CreatePlaylistInput, options?: RequestOptions): Promise<MusicPlaylist>;
572
+ /**
573
+ * `PATCH /playlists/:id` - renames, re-visibilities or re-arts a playlist.
574
+ *
575
+ * The system-playlist guard here is per FIELD, not per request: `visibility`
576
+ * is the owner's and goes through on a synced playlist, while a `name` or an
577
+ * artwork change on the same playlist is refused. Sending all three at once
578
+ * therefore fails as a whole - split the call if you want the visibility
579
+ * change to land anyway.
580
+ *
581
+ * `artworkMediaId: null` purges the current cover; omitting the key leaves it
582
+ * alone. See {@link UpdatePlaylistInput.artworkMediaId}.
583
+ *
584
+ * Ceiling: the general authenticated 600/min.
585
+ *
586
+ * @throws {OmsApiError} 404 `"Resource not found"` when the playlist is not
587
+ * visible to you; 401 when it is visible but not yours, and 401 again with
588
+ * an explanatory sentence when it is a system playlist and the change is
589
+ * one the sync owns; 400 on an invalid `artworkMediaId` or visibility.
590
+ */
591
+ update(id: MusicPlaylistId, input: UpdatePlaylistInput, options?: RequestOptions): Promise<MusicPlaylist>;
592
+ /**
593
+ * `DELETE /playlists/:id` - destroys the playlist, its join rows, its follows
594
+ * and its artwork blob. `204`, so this resolves to `undefined`.
595
+ *
596
+ * The songs themselves are untouched - a playlist owns memberships, not audio.
597
+ * Deleting a SYSTEM playlist is allowed, and it is the only way to opt out of
598
+ * a sync from this side; the provider will recreate it on the next run unless
599
+ * the sync itself is switched off.
600
+ *
601
+ * @throws {OmsApiError} 404 when not visible, 401 when visible but not yours.
602
+ */
603
+ delete(id: MusicPlaylistId, options?: RequestOptions): Promise<void>;
604
+ /**
605
+ * `POST /playlists/:id/copy` - forks a playlist into an editable one of your
606
+ * own. `201` with the new playlist.
607
+ *
608
+ * This is the escape hatch from a system playlist, and its whole purpose:
609
+ * the copy has `source_kind: "manual"`, so everything that was refused on the
610
+ * original works on it. The artwork is re-attached to the SAME blob, which
611
+ * costs no bytes and counts once against the music quota, and the songs are
612
+ * renumbered densely from 1.
613
+ *
614
+ * Two things it will not do. It only copies a playlist you OWN - a friend's
615
+ * playlist that {@link get} opens happily answers `401 "not yours"` here,
616
+ * which `docs/api-music.md` does not mention. And the new name is built
617
+ * server-side as `"<name> (cópia)"`, in Portuguese, whatever the client's
618
+ * locale; rename it afterwards with {@link update} if that matters.
619
+ *
620
+ * Hidden rows are left behind: a copy is what you SEE, not what the sync
621
+ * knows about.
622
+ *
623
+ * @throws {OmsApiError} 404 `"playlist not found"` (lower case here, unlike
624
+ * the generic `"Resource not found"` elsewhere), 401 `"not yours"`.
625
+ */
626
+ copy(id: MusicPlaylistId, options?: RequestOptions): Promise<MusicPlaylist>;
627
+ /**
628
+ * `POST /playlists/:id/reorder` - rewrites the order of the playlist.
629
+ *
630
+ * Read this before calling it. The endpoint is not "move song X to slot N";
631
+ * it is "here is the complete order", and the implementation is one line that
632
+ * makes three things true at once:
633
+ *
634
+ * ```ruby
635
+ * new_position = @song_ids.index(ps.song_id)
636
+ * ps.update(position: new_position) if new_position
637
+ * ```
638
+ *
639
+ * 1. **Positions become the INDEX in your array, so they start at 0.** Every
640
+ * other path numbers from 1 (seeding) or from `max + 1` (appending). After
641
+ * a reorder the playlist is densely numbered `0..n-1`.
642
+ * 2. **A row whose song you left out keeps its OLD position.** It is not
643
+ * moved to the end and not removed - it stays wherever it was and now
644
+ * interleaves with, or collides with, the renumbered rows. So send the
645
+ * complete order, always. Ordering by `position` after a partial reorder
646
+ * gives an arrangement nobody asked for.
647
+ * 3. **The ids are matched with `Array#index`, which is `==` on Integers.**
648
+ * A string id matches nothing, so `["12","5"]` moves NOTHING and still
649
+ * answers `200`. This is the silent failure this method exists to prevent:
650
+ * it coerces to numbers and throws on anything that is not an integer.
651
+ *
652
+ * The response body is the reorderer's internal output (raw ActiveRecord JSON
653
+ * of the join rows, not the blueprint) and is not part of the contract, so
654
+ * this resolves to `undefined`. Refetch with
655
+ * {@link PlaylistSongsNamespace.list} if you need the new state.
656
+ *
657
+ * Ceiling: the general authenticated 600/min.
658
+ *
659
+ * @throws {TypeError} on an empty array (the service raises `ArgumentError`
660
+ * for it, which is a 500, not a 400) or on an id that is not an integer.
661
+ * @throws {OmsApiError} 404 when the playlist is not visible, 401 `"not yours"`
662
+ * when it is visible but somebody else's - following it is not enough - and
663
+ * 401 with the "make a copy first" sentence on a system playlist.
664
+ */
665
+ reorder(id: MusicPlaylistId, songIds: readonly number[], options?: RequestOptions): Promise<void>;
666
+ /**
667
+ * `POST /playlists/:id/upload_artwork` - uploads a new cover. Multipart, in
668
+ * the field `artwork`. `200` with the updated playlist.
669
+ *
670
+ * Works in all three runtimes, and the file is what differs between them:
671
+ * pass a `FileInput` (`{ data: Blob | Uint8Array | ReadableStream, filename }`)
672
+ * in the browser, in Bun or in a Worker, and pass the `{ uri, name, type }`
673
+ * descriptor your picker returned on React Native. The RN descriptor is
674
+ * appended to the `FormData` untouched so the native layer can stream it off
675
+ * disk; on any other runtime that object would go out as the literal text
676
+ * `"[object Object]"`, so `buildFormData` throws there instead of uploading
677
+ * a 200 with no file in it.
678
+ *
679
+ * The bytes go through the music quota funnel, and replacing a cover
680
+ * `purge_later`s the previous blob rather than leaking it. Note that this is
681
+ * the only artwork path that spends quota: pointing `artworkMediaId` at an
682
+ * existing attachment reuses the blob and costs nothing.
683
+ *
684
+ * Uploads are capped at roughly 100 MB by the CDN in front of the API, well
685
+ * above anything an image crop produces; the app sends a JPEG of about 2 MB.
686
+ *
687
+ * Ceiling: the general authenticated 600/min.
688
+ *
689
+ * @throws {OmsApiError} 400 `"Music storage quota exceeded"` when the account
690
+ * is out of music storage; 404 when the playlist is not visible; 401 when
691
+ * it is not yours or is a system playlist.
692
+ */
693
+ uploadArtwork(id: MusicPlaylistId, artwork: FileInput | NativeFile, options?: RequestOptions): Promise<MusicPlaylist>;
694
+ }
695
+ /**
696
+ * The `playlist_songs` join rows: membership, order, and the semi-sync dance
697
+ * around removing a song from a playlist somebody else maintains.
698
+ *
699
+ * Every method here addresses a ROW, never a song. The two ids are both plain
700
+ * numbers and swapping them addresses a real, different row, so a mix-up
701
+ * silently operates on the wrong record instead of erroring.
702
+ */
703
+ export declare class PlaylistSongsNamespace extends Resource {
704
+ /**
705
+ * `GET /playlist_songs` - the rows of a playlist, or every playlist a song is
706
+ * in, with the full song embedded on each row.
707
+ *
708
+ * Defaults to `position:asc` and a page of 100, which is how a playlist screen
709
+ * reads itself. Filtering by `songId` instead is the membership check the
710
+ * "add to playlist" dialogue needs: one request tells you which playlists
711
+ * already contain the song.
712
+ *
713
+ * The scope is `Playlist.viewable_by`, so this also lists a friend's `friends`
714
+ * playlist. What a follower does NOT see is the hidden rows: the query is
715
+ * `hidden = FALSE OR playlists.user_id = <you>`, so the same playlist has a
716
+ * different length depending on who is asking. Do not use a row count from
717
+ * here as the owner's row count.
718
+ *
719
+ * Every row carries a fully preloaded song (artists, artwork, audio and stem
720
+ * media ids), so a page of 100 is a large payload. Ask for the page size you
721
+ * will actually render.
722
+ *
723
+ * Ceiling: the general authenticated 600/min.
724
+ */
725
+ list(params?: ListPlaylistSongsParams, options?: RequestOptions): Promise<Paginated<MusicPlaylistSong>>;
726
+ /**
727
+ * `POST /playlist_songs` - appends a song to a playlist. `201` with the row.
728
+ *
729
+ * The position is assigned server-side and you cannot choose it: it is
730
+ * `max(position) + 1` normally, and on a SYSTEM playlist it is lifted to at
731
+ * least {@link PLAYLIST_MANUAL_BLOCK_FLOOR} with `origin: "manual"`, so the
732
+ * addition sits in a block the sync never renumbers.
733
+ *
734
+ * Adding to a system playlist WORKS. `docs/api-music.md` still says this is a
735
+ * `401`, and it was before semi-sync landed; the current controller only
736
+ * requires that you own the playlist. What is still refused on a system
737
+ * playlist is renaming it, re-arting it and reordering it.
738
+ *
739
+ * Not retried on a lost answer: the unique index would turn the replay into a
740
+ * `400`, reporting a failure for a row that was in fact created.
741
+ *
742
+ * Ceiling: the general authenticated 600/min.
743
+ *
744
+ * @throws {OmsApiError} 400 `"Song has already been taken"` when the song is
745
+ * already on the playlist - including when it is there as a HIDDEN row,
746
+ * which is invisible to a follower and the usual cause of a surprising
747
+ * duplicate error; 404 `"Song not found"` when the song is not visible to
748
+ * you; 401 when the playlist is not yours.
749
+ */
750
+ add(playlistId: MusicPlaylistId, songId: number, options?: RequestOptions): Promise<MusicPlaylistSong>;
751
+ /**
752
+ * `DELETE /playlist_songs/:id` - takes a song off a playlist. `204`, so this
753
+ * resolves to `undefined`.
754
+ *
755
+ * `rowId` is the JOIN ROW id, the `id` on a {@link MusicPlaylistSong}, not the
756
+ * song's id. Passing a song id here will usually address some other real row.
757
+ *
758
+ * **It does not always delete.** On a row the sync created (`origin: "sync"`)
759
+ * the server marks it `hidden` and keeps it, because a deleted row would just
760
+ * be recreated on the next sync run. The status is `204` either way, so the
761
+ * response cannot tell you which happened - look at `origin` on the row before
762
+ * you call, and expect a hidden row to keep appearing in the OWNER's listings.
763
+ * {@link unhide} puts it back.
764
+ *
765
+ * Manual rows are deleted for real, on system playlists too.
766
+ *
767
+ * @throws {OmsApiError} 404 when the row is not visible, 401 when the playlist
768
+ * is not yours.
769
+ */
770
+ remove(rowId: MusicPlaylistSongId, options?: RequestOptions): Promise<void>;
771
+ /**
772
+ * `POST /playlist_songs/:id/hide` - hides a row without deleting it. `200`
773
+ * with the updated row.
774
+ *
775
+ * This is what {@link remove} does implicitly to a sync row, made explicit and
776
+ * available for manual rows too. Included even though it is not in the route
777
+ * list this module was commissioned from, because exposing the hiding half of
778
+ * the mechanism without {@link unhide} would leave a caller unable to undo a
779
+ * `remove`.
780
+ *
781
+ * @throws {OmsApiError} 404 when the row is not visible, 401 when the playlist
782
+ * is not yours.
783
+ */
784
+ hide(rowId: MusicPlaylistSongId, options?: RequestOptions): Promise<MusicPlaylistSong>;
785
+ /**
786
+ * `POST /playlist_songs/:id/unhide` - puts a hidden row back. `200` with the
787
+ * updated row.
788
+ *
789
+ * The only way to undo a {@link remove} that turned into a hide. It needs the
790
+ * row id, and only the OWNER can see a hidden row to get one - a follower's
791
+ * listing does not contain it.
792
+ *
793
+ * @throws {OmsApiError} 404 when the row is not visible, 401 when the playlist
794
+ * is not yours.
795
+ */
796
+ unhide(rowId: MusicPlaylistSongId, options?: RequestOptions): Promise<MusicPlaylistSong>;
797
+ }
798
+ /**
799
+ * Generated mix shelves: "This is X", the monthly rewind, the year mix, the
800
+ * time capsule, the discoveries.
801
+ *
802
+ * The whole set is generated per user and cached server-side for 24 hours, so
803
+ * the first call after the cache expires is the slow one and everything after
804
+ * it is cheap. The set ROTATES: slugs are content-addressed, so yesterday's
805
+ * slug is simply gone today, which is why {@link get} answers `404` far more
806
+ * often than a missing-record `404` normally would.
807
+ *
808
+ * Neither endpoint is CRUD-shaped: there is no list DSL, no paging and no
809
+ * ETag. {@link list} always returns the complete set.
810
+ */
811
+ export declare class MusicMixesNamespace extends Resource {
812
+ /**
813
+ * `GET /music_mixes` - every shelf currently generated for the caller,
814
+ * without their songs.
815
+ *
816
+ * The song ids are stripped from this view on purpose, so the payload stays
817
+ * small; fetch one shelf with {@link get} when the user opens it.
818
+ *
819
+ * Titles come twice over. `title` and `description` are an English fallback;
820
+ * `title_key`/`title_params` and their description twins are the i18n
821
+ * template the UI should actually render, so the shelf follows the app
822
+ * language instead of being permanently one language. The embedded `artist`
823
+ * is resolved at render time rather than cached with the shelf, so a picture
824
+ * that lands today shows up today.
825
+ *
826
+ * Ceiling: the general authenticated 600/min.
827
+ */
828
+ list(options?: RequestOptions): Promise<MusicMixSummary[]>;
829
+ /**
830
+ * `GET /music_mixes/:slug` - one shelf with its songs, in mix order.
831
+ *
832
+ * The slug contains colons (`mix:top_artist:1:ab12cd34`). This encodes it for
833
+ * you; the route accepts any segment without a slash.
834
+ *
835
+ * A `404` here is ORDINARY, not an error to report. The shelves rotate as the
836
+ * user's listening changes and the cache turns over every 24 hours, so a slug
837
+ * captured yesterday, deep-linked, or held in a stale list is simply not in
838
+ * today's set. Refetch {@link list} and let the user pick again.
839
+ *
840
+ * The songs are filtered to the caller's own library, so a shelf can come back
841
+ * with fewer songs than it was generated with if tracks were deleted in the
842
+ * meantime.
843
+ *
844
+ * Ceiling: the general authenticated 600/min.
845
+ *
846
+ * @throws {OmsApiError} 404 `"Mix not found"`.
847
+ */
848
+ get(slug: string, options?: RequestOptions): Promise<MusicMix>;
849
+ }
850
+ /**
851
+ * Artist and song radios: about 40 tracks built by intersecting Last.fm
852
+ * similar-artist data with what the caller actually owns.
853
+ *
854
+ * **This is the throttled family.** `/music_radios/*` sits behind
855
+ * rack-attack's `external_proxy/by_session` rule at
856
+ * {@link MUSIC_RADIO_RATE_LIMIT_PER_MINUTE} requests per minute, shared with
857
+ * `/lyrics`, `/artists/*` and `/artist_metadata/*`. The bucket key is the
858
+ * `Authorization` HEADER when there is one and the client IP when there is not,
859
+ * which has a consequence worth planning around: a cookie-authenticated web
860
+ * client sends no `Authorization`, so every visitor behind one address shares a
861
+ * single 60/min budget. Token clients get a bucket per token.
862
+ *
863
+ * Over the limit the API answers `429` with a `Retry-After`, which the transport
864
+ * honours by sleeping and retrying - a rate-limited call can therefore take most
865
+ * of a minute. Pass `retry: false` on anything with a user waiting.
866
+ *
867
+ * A built radio is cached per user for 7 days, so the cost is paid once.
868
+ */
869
+ export declare class MusicRadiosNamespace extends Resource {
870
+ /**
871
+ * `GET /music_radios/artist/:artist` - a radio seeded on one artist.
872
+ *
873
+ * Takes the artist's SLUG or their name: the backend canonicalises what you
874
+ * send and tries `canonical_name` first, then `slug`. `docs/api-music.md` says
875
+ * slug only, which understates it. Either way the lookup is against the
876
+ * caller's own roster and never creates an artist, so an artist you do not
877
+ * have is a `404` rather than an empty radio.
878
+ *
879
+ * Roughly 30% of the tracks come from the seed artist and the rest from
880
+ * similar artists you own, shuffled. The mix is drawn with `RANDOM()` on the
881
+ * first build and then frozen for 7 days, so calling twice gives the same
882
+ * radio, not a reshuffle.
883
+ *
884
+ * Ceiling: {@link MUSIC_RADIO_RATE_LIMIT_PER_MINUTE} per minute. A cold build
885
+ * runs several queries over the whole library; the mobile app allows 60
886
+ * seconds for it, and passing `timeoutMs` is reasonable here.
887
+ *
888
+ * @throws {OmsApiError} 404 `"Could not build radio for <artist>"` - the same
889
+ * answer for "no such artist in your library" and for "nothing similar to
890
+ * play". 429 when the minute's budget is spent.
891
+ */
892
+ forArtist(artistSlugOrName: string, options?: RequestOptions): Promise<MusicRadio>;
893
+ /**
894
+ * `GET /music_radios/song/:id` - a radio seeded on one song.
895
+ *
896
+ * Built by taking the song's lead artist's radio and re-titling it, so a song
897
+ * with no artist credit cannot produce one. The seed song is guaranteed to be
898
+ * `songs[0]` - it is unshifted in if the shuffle did not already include it -
899
+ * which is what lets a client start playback at index 0 and have the user hear
900
+ * the track they tapped.
901
+ *
902
+ * Ceiling: {@link MUSIC_RADIO_RATE_LIMIT_PER_MINUTE} per minute.
903
+ *
904
+ * @throws {OmsApiError} 404 `"Could not build radio for song <id>"` when the
905
+ * song is not yours, has no artist, or its artist yields nothing to play.
906
+ * 429 when the minute's budget is spent.
907
+ */
908
+ forSong(songId: number, options?: RequestOptions): Promise<MusicRadio>;
909
+ }
910
+ /**
911
+ * Play history: recording what was listened to, and the aggregates built on it.
912
+ *
913
+ * None of these are CRUD-shaped. They take plain query parameters (`limit`,
914
+ * `group_by`, `scope`, `since`, `artist`), not the list DSL, so `modifiers[page]`
915
+ * and `search[...]` are ignored rather than rejected here, there is no ETag, and
916
+ * `limit` is the only way to bound a result. There is no way to page past it:
917
+ * {@link PLAY_EVENT_MAX_LIMIT} rows is all the history these endpoints will give.
918
+ *
919
+ * Everything is scoped to the caller's own events. There is no route to read
920
+ * anyone else's.
921
+ */
922
+ export declare class PlayEventsNamespace extends Resource {
923
+ /**
924
+ * `POST /play_events` - records that a song was played.
925
+ *
926
+ * Two answers, two shapes, and this is the one thing to get right: a play of
927
+ * the same song within {@link PLAY_EVENT_DEDUPE_WINDOW_MS} of the last one is
928
+ * SWALLOWED, and the server answers `200 {"deduped": true}` instead of `201`
929
+ * with an event. The transport parses both into the same promise, so narrow
930
+ * with {@link playWasDeduped} before touching `.id`. The dedupe exists because
931
+ * scrubbing, a rewind, and a double-mounted player component all look like
932
+ * fresh plays otherwise.
933
+ *
934
+ * That window also makes this the one write in this file that is SAFE to
935
+ * retry: a duplicate arriving inside 30 seconds is absorbed rather than
936
+ * doubled. The transport still will not replay a `POST` by default, so pass
937
+ * `retry: {}` if a lost answer on a flaky connection should be tried again.
938
+ *
939
+ * Hosts usually treat this as fire-and-forget - the app does, with no error
940
+ * UI - because a lost play event is worth less than an error toast during
941
+ * playback.
942
+ *
943
+ * `listenedSeconds` is clamped server-side rather than validated: `0` is the
944
+ * floor and three times the track's duration is the ceiling, or 24 hours when
945
+ * the duration is unknown or zero, so a runaway accumulator is capped instead
946
+ * of refused.
947
+ *
948
+ * Ceiling: the general authenticated 600/min. A client that posts one event
949
+ * per track is nowhere near it; one that posts on every seek is not.
950
+ *
951
+ * @throws {OmsApiError} 400 when `songId` is missing (`ParameterMissing`), 404
952
+ * `"Song not found"` when the song is not visible to you.
953
+ */
954
+ record(input: RecordPlayInput, options?: RequestOptions): Promise<RecordPlayResult>;
955
+ /**
956
+ * `GET /play_events/recent?group_by=song` - recently played songs, newest
957
+ * first, each song appearing at most once with the timestamp of its latest
958
+ * play.
959
+ *
960
+ * Collapsed, not raw history: ten plays of one song are one row. There is no
961
+ * endpoint that returns the raw event stream.
962
+ *
963
+ * `limit` defaults to 24 and is capped at {@link PLAY_EVENT_MAX_LIMIT}. A value
964
+ * the server cannot read as a positive integer falls back to the default
965
+ * rather than failing, so a bad limit is invisible.
966
+ *
967
+ * Ceiling: the general authenticated 600/min.
968
+ */
969
+ recentSongs(params?: RecentPlaysParams, options?: RequestOptions): Promise<RecentSongPlay[]>;
970
+ /**
971
+ * `GET /play_events/recent?group_by=album` - recently played albums.
972
+ *
973
+ * Grouped by album name AND lead artist, so two albums with the same title by
974
+ * different artists stay apart. Songs with no album are excluded entirely -
975
+ * the query filters `album NOT IN (NULL, '')` - so a library of loose singles
976
+ * produces an empty shelf here while {@link recentSongs} is full.
977
+ *
978
+ * Ceiling: the general authenticated 600/min.
979
+ */
980
+ recentAlbums(params?: RecentPlaysParams, options?: RequestOptions): Promise<RecentAlbumPlay[]>;
981
+ /**
982
+ * `GET /play_events/top?scope=song` - most played songs, with their counts.
983
+ *
984
+ * `since` defaults to `"all"`; the windows are fixed strings and anything else
985
+ * is a `400`, so do not build one from a date. `limit` defaults to 10, capped
986
+ * at {@link PLAY_EVENT_MAX_LIMIT}.
987
+ *
988
+ * The `artist` filter is the "popular tracks by this artist" query and only
989
+ * exists on this scope. It matches on the canonicalised NAME against the
990
+ * caller's own roster, and an artist that does not resolve gives an EMPTY
991
+ * ARRAY rather than a `404` - indistinguishable from an artist you own but
992
+ * have never played.
993
+ *
994
+ * Ceiling: the general authenticated 600/min.
995
+ *
996
+ * @throws {OmsApiError} 400 `"Invalid since; must be 7d, 30d, 90d, or all"`.
997
+ */
998
+ topSongs(params?: TopSongsParams, options?: RequestOptions): Promise<TopSongRow[]>;
999
+ /**
1000
+ * `GET /play_events/top?scope=album` - most played albums, with their counts.
1001
+ *
1002
+ * Same album/artist grouping and the same exclusion of songs with no album as
1003
+ * {@link recentAlbums}. The `artist` filter does NOT apply to this scope; it
1004
+ * is ignored rather than rejected, which is why this method does not offer it.
1005
+ *
1006
+ * Ceiling: the general authenticated 600/min.
1007
+ *
1008
+ * @throws {OmsApiError} 400 on an unrecognised `since`.
1009
+ */
1010
+ topAlbums(params?: TopPlaysParams, options?: RequestOptions): Promise<TopAlbumRow[]>;
1011
+ /**
1012
+ * `GET /play_events/top?scope=artist` - most played artists, with their counts.
1013
+ *
1014
+ * Counted on credits whose `role` is `"primary"`, at ANY position - NOT the
1015
+ * stricter lead credit (`role: "primary"` and `position: 0`) that the album
1016
+ * groupings use. A track credited to two primaries therefore counts once for
1017
+ * each of them, and these counts can add up to more than the number of plays.
1018
+ * A featured credit never counts.
1019
+ *
1020
+ * Every row carries a compact artist, never a bare name.
1021
+ *
1022
+ * Ceiling: the general authenticated 600/min.
1023
+ *
1024
+ * @throws {OmsApiError} 400 on an unrecognised `since`.
1025
+ */
1026
+ topArtists(params?: TopPlaysParams, options?: RequestOptions): Promise<TopArtistRow[]>;
1027
+ }