@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.
- package/dist/index.js +4939 -552
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +4 -1
- package/dist/types/resources/account.d.ts +66 -3
- package/dist/types/resources/admin.d.ts +1837 -0
- package/dist/types/resources/auth/index.d.ts +39 -0
- package/dist/types/resources/auth/passkeys.d.ts +652 -0
- package/dist/types/resources/auth/sessions.d.ts +847 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content.d.ts +2970 -0
- package/dist/types/resources/dynamicQrs.d.ts +39 -3
- package/dist/types/resources/forms.d.ts +176 -35
- package/dist/types/resources/index.d.ts +19 -8
- package/dist/types/resources/ipLookup.d.ts +20 -4
- package/dist/types/resources/jobs.d.ts +62 -21
- package/dist/types/resources/library.d.ts +1435 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies.d.ts +1186 -0
- package/dist/types/resources/music/artists.d.ts +1066 -0
- package/dist/types/resources/music/imports.d.ts +940 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1026 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1183 -0
- package/dist/types/resources/notepads.d.ts +4 -1
- package/dist/types/resources/quotas.d.ts +7 -1
- package/dist/types/resources/realtime.d.ts +855 -0
- package/dist/types/resources/shortLinks.d.ts +45 -4
- package/dist/types/resources/social.d.ts +1330 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +88 -22
- package/dist/types/resources/tickets.d.ts +82 -3
- package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
- package/dist/types/resources/tools/captions.d.ts +448 -21
- package/dist/types/resources/tools/downloader.d.ts +21 -0
- package/dist/types/resources/tools/index.d.ts +57 -15
- package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
- package/dist/types/resources/tools/transcription.d.ts +35 -13
- package/dist/types/resources/tools/upscale.d.ts +23 -3
- package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
- package/dist/types/types.d.ts +249 -17
- 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
|
+
}
|