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