@omelhorsite/sdk 0.4.0 → 0.4.2
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/README.md +4 -4
- package/dist/index.js +59 -52
- package/dist/types/auth/device.d.ts +1 -1
- package/dist/types/auth/index.d.ts +2 -2
- package/dist/types/auth/tokens.d.ts +15 -15
- package/dist/types/client.d.ts +10 -10
- package/dist/types/errors.d.ts +12 -15
- package/dist/types/http.d.ts +74 -118
- package/dist/types/index.d.ts +1 -2
- package/dist/types/local/qr.d.ts +1 -1
- package/dist/types/local/wordlist.d.ts +2 -3
- package/dist/types/resources/account.d.ts +14 -17
- package/dist/types/resources/auth/index.d.ts +1 -1
- package/dist/types/resources/auth/passkeys.d.ts +127 -163
- package/dist/types/resources/auth/sessions.d.ts +110 -152
- package/dist/types/resources/chests.d.ts +27 -31
- package/dist/types/resources/dynamicQrs.d.ts +29 -45
- package/dist/types/resources/forms.d.ts +37 -58
- package/dist/types/resources/jobs.d.ts +28 -40
- package/dist/types/resources/media.d.ts +48 -61
- package/dist/types/resources/music/artists.d.ts +179 -245
- package/dist/types/resources/music/imports.d.ts +160 -303
- package/dist/types/resources/music/index.d.ts +8 -7
- package/dist/types/resources/music/playlists.d.ts +77 -110
- package/dist/types/resources/music/social.d.ts +153 -228
- package/dist/types/resources/music/songs.d.ts +160 -206
- package/dist/types/resources/realtime.d.ts +75 -88
- package/dist/types/resources/shortLinks.d.ts +33 -45
- package/dist/types/resources/storage/upload.d.ts +42 -56
- package/dist/types/resources/storage.d.ts +71 -104
- package/dist/types/resources/tools/backgroundRemoval.d.ts +11 -13
- package/dist/types/resources/tools/captions.d.ts +120 -135
- package/dist/types/resources/tools/index.d.ts +4 -0
- package/dist/types/resources/tools/srMachine.d.ts +114 -0
- package/dist/types/resources/tools/upscale.d.ts +12 -16
- package/dist/types/types.d.ts +29 -38
- package/package.json +1 -1
|
@@ -2,31 +2,27 @@
|
|
|
2
2
|
* The `music.imports` namespace: getting audio INTO the library, and keeping a
|
|
3
3
|
* Spotify account mirrored into it.
|
|
4
4
|
*
|
|
5
|
-
* Four
|
|
5
|
+
* Four route families sit behind one namespace because they are the four
|
|
6
6
|
* doors a track can walk through:
|
|
7
7
|
*
|
|
8
8
|
* | Route family | What it is | Who may call it |
|
|
9
9
|
* |---|---|---|
|
|
10
|
-
* | `/song_imports` | one track, downloaded
|
|
10
|
+
* | `/song_imports` | one track, downloaded from a URL or found by search | any signed-in user |
|
|
11
11
|
* | `/playlist_imports/preview` | read a playlist URL without importing it | any signed-in user |
|
|
12
|
-
* | `/spotify_syncs/*` | mirror a Spotify account into playlists |
|
|
13
|
-
* | `/s_r_machine/*` | raw fetch + transcode helpers | **admins only** |
|
|
12
|
+
* | `/spotify_syncs/*` | mirror a Spotify account into playlists | Spotify-enabled accounts only |
|
|
14
13
|
*
|
|
15
14
|
* ## An import is not a `Job`
|
|
16
15
|
*
|
|
17
|
-
* `POST /song_imports` answers a
|
|
18
|
-
*
|
|
19
|
-
* `
|
|
16
|
+
* `POST /song_imports` answers a song import, not a generic job, and there is
|
|
17
|
+
* no `job_id` anywhere on it. So `oms.jobs.get()` will never find it and
|
|
18
|
+
* `watch_token` means nothing here.
|
|
20
19
|
*
|
|
21
|
-
* What this namespace DOES
|
|
22
|
-
* MusicImportsNamespace.wait} and {@link MusicImportsNamespace.watch}
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
25
|
-
* {@link
|
|
26
|
-
*
|
|
27
|
-
* polling engine in this SDK and this is not a second one. The same is true of
|
|
28
|
-
* {@link SpotifySyncNamespace.waitForSync}, which binds `poll` to
|
|
29
|
-
* `GET /spotify_syncs/status`.
|
|
20
|
+
* What this namespace DOES share with `oms.jobs` is the polling loop:
|
|
21
|
+
* {@link MusicImportsNamespace.wait} and {@link MusicImportsNamespace.watch}
|
|
22
|
+
* poll `GET /song_imports/:id` until {@link isSongImportTerminal}, with the
|
|
23
|
+
* same backoff, the same `waitTimeoutMs`, the same `signal` semantics and the
|
|
24
|
+
* same two `OmsTimeoutError` codes. {@link SpotifySyncNamespace.waitForSync}
|
|
25
|
+
* does the same against `GET /spotify_syncs/status`.
|
|
30
26
|
*
|
|
31
27
|
* Three scales, all called "progress", none of them the same number:
|
|
32
28
|
*
|
|
@@ -41,20 +37,17 @@
|
|
|
41
37
|
*
|
|
42
38
|
* Everything here sits under the general authenticated ceiling of 600/min
|
|
43
39
|
* except {@link MusicImportsNamespace.previewPlaylist}, which carries TWO
|
|
44
|
-
* budgets of its own
|
|
45
|
-
* been used to take the site down. Read its doc comment before writing a loop
|
|
46
|
-
* around it.
|
|
40
|
+
* budgets of its own. Read its doc comment before writing a loop around it.
|
|
47
41
|
*
|
|
48
42
|
* ## OAuth tokens cannot reach any of this
|
|
49
43
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* session cookie or a personal token, like the rest of music.
|
|
44
|
+
* An OAuth access token gets `403 {"error":"insufficient_scope"}` on every
|
|
45
|
+
* route here. Imports need a session cookie or a personal token, like the rest
|
|
46
|
+
* of music.
|
|
54
47
|
*/
|
|
55
48
|
import { type ApiClient, Resource } from "../../http";
|
|
56
49
|
import type { ListParams } from "../../listing";
|
|
57
|
-
import type { BaseRecord,
|
|
50
|
+
import type { BaseRecord, Id, Paginated, Progress, RequestOptions, Timestamp, WaitOptions } from "../../types";
|
|
58
51
|
import type { DownloaderPreview } from "../tools/downloader";
|
|
59
52
|
/**
|
|
60
53
|
* Primary key of a song import. An **integer**, like `songs` and `playlists`
|
|
@@ -73,7 +66,7 @@ export type SongImportId = number | string;
|
|
|
73
66
|
* jobs namespace has a dead branch here and a missing one there.
|
|
74
67
|
*/
|
|
75
68
|
export type SongImportState = "pending" | "processing" | "complete" | "failed";
|
|
76
|
-
/** The four states
|
|
69
|
+
/** The four states. */
|
|
77
70
|
export declare const SONG_IMPORT_STATES: Readonly<{
|
|
78
71
|
readonly pending: "pending";
|
|
79
72
|
readonly processing: "processing";
|
|
@@ -81,8 +74,8 @@ export declare const SONG_IMPORT_STATES: Readonly<{
|
|
|
81
74
|
readonly failed: "failed";
|
|
82
75
|
}>;
|
|
83
76
|
/**
|
|
84
|
-
* The states
|
|
85
|
-
*
|
|
77
|
+
* The states that are final, and the ones {@link MusicImportsNamespace.wait}
|
|
78
|
+
* stops on.
|
|
86
79
|
*
|
|
87
80
|
* `"pending"` is deliberately absent even though a row can go BACK to it: see
|
|
88
81
|
* {@link isSongImportTerminal}.
|
|
@@ -91,28 +84,26 @@ export declare const SONG_IMPORT_TERMINAL_STATES: readonly SongImportState[];
|
|
|
91
84
|
/**
|
|
92
85
|
* True once this state can never change again.
|
|
93
86
|
*
|
|
94
|
-
* The state machine is NOT monotonic on the way there.
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
87
|
+
* The state machine is NOT monotonic on the way there. A transient download
|
|
88
|
+
* error puts the row back to `"pending"` with `progress_pct: 0.0` before a
|
|
89
|
+
* retry, so a poller legitimately observes `processing -> pending ->
|
|
90
|
+
* processing -> complete` and a progress bar legitimately goes backwards.
|
|
91
|
+
* Never latch a UI on "it was processing, so it cannot be pending again"; read
|
|
92
|
+
* the state every time.
|
|
100
93
|
*
|
|
101
|
-
* `"failed"`, by contrast, is only ever written when the
|
|
102
|
-
* retry, so it really is the end.
|
|
94
|
+
* `"failed"`, by contrast, is only ever written when the server has decided
|
|
95
|
+
* NOT to retry, so it really is the end.
|
|
103
96
|
*/
|
|
104
97
|
export declare function isSongImportTerminal(state: string): boolean;
|
|
105
98
|
/**
|
|
106
|
-
* One
|
|
99
|
+
* One song import.
|
|
107
100
|
*
|
|
108
|
-
*
|
|
109
|
-
* `ApplicationBlueprint`, so every key below is present on every response;
|
|
110
|
-
* what varies is the value.
|
|
101
|
+
* Every key below is present on every response; what varies is the value.
|
|
111
102
|
*
|
|
112
|
-
* ## Five
|
|
103
|
+
* ## Five fields you can write but never read back
|
|
113
104
|
*
|
|
114
105
|
* `search_artist`, `search_title`, `search_album`, `isrc` and `artwork_url` are
|
|
115
|
-
* all accepted by `POST /song_imports` and NONE of them is
|
|
106
|
+
* all accepted by `POST /song_imports` and NONE of them is echoed back.
|
|
116
107
|
* `artwork_data_b64` likewise (and it is wiped from the row the moment the
|
|
117
108
|
* import settles, deduped or not). So an import created in search mode answers
|
|
118
109
|
* with `source_url: null` and no trace of what was searched for: if the caller
|
|
@@ -141,33 +132,32 @@ export interface SongImport extends Omit<BaseRecord, "id"> {
|
|
|
141
132
|
/** The provider's own id for the track. Half of the dedupe key. */
|
|
142
133
|
readonly source_id: string | null;
|
|
143
134
|
/**
|
|
144
|
-
* `"yt_dlp"` (the
|
|
145
|
-
* `"spotify_sync"` (written by
|
|
146
|
-
*
|
|
147
|
-
* these.
|
|
135
|
+
* `"yt_dlp"` (the default, and what every user-driven import is) or
|
|
136
|
+
* `"spotify_sync"` (written by the Spotify sync, one row per track per run).
|
|
137
|
+
* Never `"upload"` - that is a `Song.source_kind` value, not one of these.
|
|
148
138
|
*/
|
|
149
139
|
readonly source_kind: string;
|
|
150
140
|
readonly override_title: string | null;
|
|
151
141
|
readonly override_artist: string | null;
|
|
152
142
|
readonly override_album: string | null;
|
|
153
|
-
/** Seconds, as a float. A hint the
|
|
143
|
+
/** Seconds, as a float. A hint the downloader uses to pick between candidates. */
|
|
154
144
|
readonly expected_duration_s: number | null;
|
|
155
145
|
/** Requested position in {@link SongImport.playlist_id}. See the note on `create`. */
|
|
156
146
|
readonly position: number | null;
|
|
157
|
-
/** The
|
|
147
|
+
/** The downloader's own request id. Diagnostics only; nothing here takes it. */
|
|
158
148
|
readonly sidecar_request_id: string | null;
|
|
159
149
|
readonly state: SongImportState;
|
|
160
150
|
/**
|
|
161
|
-
* Free text from the
|
|
162
|
-
* whatever
|
|
151
|
+
* Free text from the downloader (`"starting"`, `"complete"`, `"a repetir"`,
|
|
152
|
+
* or whatever it last said). Human-facing, not a state: switch on
|
|
163
153
|
* {@link SongImport.state}.
|
|
164
154
|
*/
|
|
165
155
|
readonly progress_message: string | null;
|
|
166
156
|
/**
|
|
167
157
|
* A FLOAT between 0 and 1, not a percentage. `0.05` means 5%.
|
|
168
158
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
159
|
+
* It is a real number from the moment the row exists. It can go DOWN - a
|
|
160
|
+
* transient retry resets it to `0.0`.
|
|
171
161
|
* Multiply by 100 before showing it, or use {@link songImportProgress}.
|
|
172
162
|
*/
|
|
173
163
|
readonly progress_pct: number;
|
|
@@ -193,22 +183,21 @@ export interface SongImport extends Omit<BaseRecord, "id"> {
|
|
|
193
183
|
* Exactly one of the two modes must be satisfied, and the server checks it in
|
|
194
184
|
* this order:
|
|
195
185
|
*
|
|
196
|
-
* 1. **URL mode** - `sourceUrl` present. It must
|
|
197
|
-
*
|
|
198
|
-
*
|
|
186
|
+
* 1. **URL mode** - `sourceUrl` present. It must be a public http(s) URL:
|
|
187
|
+
* public DNS only, no redirect to a private address. Anything else is
|
|
188
|
+
* `400 "source_url is not allowed"`.
|
|
199
189
|
* 2. **Search mode** - `searchArtist` AND `searchTitle` both non-blank, with no
|
|
200
|
-
* `sourceUrl`. The
|
|
190
|
+
* `sourceUrl`. The server goes and finds the track itself.
|
|
201
191
|
*
|
|
202
192
|
* Neither one satisfied is `400 "source_url or (search_artist + search_title)
|
|
203
193
|
* required"`. One without the other counts as neither: `searchArtist` alone is
|
|
204
194
|
* not search mode.
|
|
205
195
|
*
|
|
206
|
-
* Every other field is optional and every unknown field is dropped in silence
|
|
207
|
-
*
|
|
208
|
-
* successful import that ignored what you asked for.
|
|
196
|
+
* Every other field is optional and every unknown field is dropped in silence,
|
|
197
|
+
* so a typo here is a successful import that ignored what you asked for.
|
|
209
198
|
*/
|
|
210
199
|
export interface CreateSongImportInput {
|
|
211
|
-
/** URL mode. Public http(s) only; see the
|
|
200
|
+
/** URL mode. Public http(s) only; see the note above. */
|
|
212
201
|
readonly sourceUrl?: string;
|
|
213
202
|
/** Search mode. Needs {@link CreateSongImportInput.searchTitle} beside it. */
|
|
214
203
|
readonly searchArtist?: string;
|
|
@@ -228,22 +217,21 @@ export interface CreateSongImportInput {
|
|
|
228
217
|
readonly sourceId?: string;
|
|
229
218
|
/**
|
|
230
219
|
* Defaults server-side to `"yt_dlp"`. Passing `"spotify_sync"` by hand is a
|
|
231
|
-
* bad idea: it
|
|
232
|
-
*
|
|
233
|
-
* playlist-position rewriting the sync does.
|
|
220
|
+
* bad idea: it moves the import onto the slow bulk lane and makes the row
|
|
221
|
+
* eligible for the playlist-position rewriting the sync does.
|
|
234
222
|
*/
|
|
235
223
|
readonly sourceKind?: string;
|
|
236
|
-
/** Tag overrides written onto the finished song instead of what
|
|
224
|
+
/** Tag overrides written onto the finished song instead of what the download carried. */
|
|
237
225
|
readonly overrideTitle?: string;
|
|
238
226
|
readonly overrideArtist?: string;
|
|
239
227
|
readonly overrideAlbum?: string;
|
|
240
|
-
/** Cover to embed.
|
|
228
|
+
/** Cover to embed. Must be a public http(s) URL, exactly like `sourceUrl`. */
|
|
241
229
|
readonly artworkUrl?: string;
|
|
242
230
|
/**
|
|
243
|
-
* Cover as base64, for a picture that has no public URL.
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
231
|
+
* Cover as base64, for a picture that has no public URL. Wiped as soon as
|
|
232
|
+
* the import settles. Keep it small: it is a JSON body, not a multipart
|
|
233
|
+
* upload, and the CDN in front of the API caps a request body at roughly
|
|
234
|
+
* 100 MB.
|
|
247
235
|
*/
|
|
248
236
|
readonly artworkDataB64?: string;
|
|
249
237
|
/** Seconds. A hint, not a constraint. */
|
|
@@ -266,7 +254,7 @@ export interface CreateSongImportInput {
|
|
|
266
254
|
/**
|
|
267
255
|
* Filters for {@link MusicImportsNamespace.list}.
|
|
268
256
|
*
|
|
269
|
-
* The
|
|
257
|
+
* The server allowlists six columns and nothing else. An unrecognised
|
|
270
258
|
* `search[...]` key is a `400` naming it, never a silently wider result.
|
|
271
259
|
*/
|
|
272
260
|
export interface ListSongImportsParams extends ListParams<(typeof SONG_IMPORT_FILTER_COLUMNS)[number]> {
|
|
@@ -280,37 +268,36 @@ export interface ListSongImportsParams extends ListParams<(typeof SONG_IMPORT_FI
|
|
|
280
268
|
export declare const SONG_IMPORT_FILTER_COLUMNS: readonly ["id", "state", "playlist_id", "user_id", "created_at", "updated_at"];
|
|
281
269
|
/**
|
|
282
270
|
* What `POST /playlist_imports/preview` answers: the SAME `DownloaderPreview`
|
|
283
|
-
* shape `oms.tools.downloader.preview()` returns, because both
|
|
284
|
-
*
|
|
271
|
+
* shape `oms.tools.downloader.preview()` returns, because both read metadata
|
|
272
|
+
* the same way.
|
|
285
273
|
*
|
|
286
274
|
* Check `kind` first: a `"playlist"` carries `count` and `tracks` and none of
|
|
287
275
|
* the track fields, a `"track"` carries the track fields and neither of those.
|
|
288
|
-
* Note
|
|
289
|
-
*
|
|
276
|
+
* Note that `formats` is absent here even on a `"track"` - unlike the
|
|
277
|
+
* downloader's own preview.
|
|
290
278
|
*/
|
|
291
279
|
export type PlaylistImportPreview = DownloaderPreview;
|
|
292
280
|
/**
|
|
293
281
|
* Sustained ceiling on `POST /playlist_imports/preview`: 60 an hour, keyed by
|
|
294
|
-
* user id.
|
|
282
|
+
* user id.
|
|
295
283
|
*/
|
|
296
284
|
export declare const PLAYLIST_IMPORT_PREVIEW_HOURLY_LIMIT = 60;
|
|
297
285
|
/**
|
|
298
|
-
* Burst ceiling on the same route: 20 a minute, from
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* `/
|
|
286
|
+
* Burst ceiling on the same route: 20 a minute, from a bucket SHARED with
|
|
287
|
+
* every other expensive tool (`/upscales`, `/vocal_separations`,
|
|
288
|
+
* `/transcriptions`, `/caption_jobs`, `/jumpstyle_jobs`, `/songs/:id/separate`,
|
|
289
|
+
* `/tools_downloader/*`).
|
|
302
290
|
*/
|
|
303
291
|
export declare const PLAYLIST_IMPORT_PREVIEW_BURST_LIMIT_PER_MINUTE = 20;
|
|
304
292
|
/**
|
|
305
|
-
* How long
|
|
293
|
+
* How long the server waits on a download before giving up on one import:
|
|
306
294
|
* ten minutes. A sensible floor for `waitTimeoutMs`, and the reason a shorter
|
|
307
295
|
* one is a client-side deadline rather than a cancellation.
|
|
308
296
|
*/
|
|
309
297
|
export declare const SONG_IMPORT_SERVER_TIMEOUT_MS = 600000;
|
|
310
298
|
/**
|
|
311
|
-
* Default pause between polls of a song import: 1.5s
|
|
312
|
-
*
|
|
313
|
-
* progress every 3 seconds anyway.
|
|
299
|
+
* Default pause between polls of a song import: 1.5s. Slower than the jobs
|
|
300
|
+
* default because progress is only written every 3 seconds anyway.
|
|
314
301
|
*/
|
|
315
302
|
export declare const SONG_IMPORT_POLL_INTERVAL_MS = 1500;
|
|
316
303
|
/**
|
|
@@ -324,20 +311,18 @@ export declare const SONG_IMPORT_POLL_INTERVAL_MS = 1500;
|
|
|
324
311
|
export declare function songImportProgress(record: SongImport): Progress;
|
|
325
312
|
/** The `music.imports` namespace, reachable as `oms.music.imports`. */
|
|
326
313
|
export declare class MusicImportsNamespace extends Resource {
|
|
327
|
-
/** Spotify account mirroring.
|
|
314
|
+
/** Spotify account mirroring. Spotify-enabled accounts only - see the class. */
|
|
328
315
|
readonly spotify: SpotifySyncNamespace;
|
|
329
|
-
/** Admin-only fetch and transcode helpers. */
|
|
330
|
-
readonly srMachine: SRMachineNamespace;
|
|
331
316
|
constructor(http: ApiClient);
|
|
332
317
|
/**
|
|
333
318
|
* `GET /song_imports` - your import history, newest first only if you ask.
|
|
334
319
|
*
|
|
335
|
-
* Scoped
|
|
336
|
-
*
|
|
320
|
+
* Scoped to the caller, so `userId` can only ever narrow to yourself and a
|
|
321
|
+
* foreign one answers an empty page rather than a 403.
|
|
337
322
|
*
|
|
338
|
-
* **The
|
|
339
|
-
*
|
|
340
|
-
*
|
|
323
|
+
* **The list is pruned.** `complete` and `deduped` rows older than 30 days
|
|
324
|
+
* and `failed` rows older than 90 are deleted, so this is a recent-activity
|
|
325
|
+
* feed, not an archive. It is also dominated by
|
|
341
326
|
* `spotify_sync` rows on any account with the sync on - one per track per
|
|
342
327
|
* daily run, thousands a day - so filter by `state` or `playlistId` unless
|
|
343
328
|
* you actually want that.
|
|
@@ -350,26 +335,26 @@ export declare class MusicImportsNamespace extends Resource {
|
|
|
350
335
|
* `GET /song_imports/:id` - one read, no waiting.
|
|
351
336
|
*
|
|
352
337
|
* @throws {OmsApiError} 404 `"song import not found"` for an id that is not
|
|
353
|
-
* yours, never a 403 - and for one
|
|
338
|
+
* yours, never a 403 - and for one that has already been pruned.
|
|
354
339
|
*/
|
|
355
340
|
get(id: SongImportId, options?: RequestOptions): Promise<SongImport>;
|
|
356
341
|
/**
|
|
357
342
|
* `POST /song_imports` - 201 with the row, before any downloading starts.
|
|
358
343
|
*
|
|
359
344
|
* Returns immediately in every case. Either the row is already terminal
|
|
360
|
-
* because dedupe hit ({@link SongImport.deduped}), or it is `"pending"` and
|
|
361
|
-
*
|
|
345
|
+
* because dedupe hit ({@link SongImport.deduped}), or it is `"pending"` and
|
|
346
|
+
* the download has been queued. Nothing about this call waits for audio.
|
|
362
347
|
*
|
|
363
|
-
* Which
|
|
364
|
-
* `"spotify_sync"` goes on the
|
|
365
|
-
* watching a spinner is not queued behind
|
|
366
|
-
*
|
|
348
|
+
* Which lane the download runs on depends on `sourceKind`: anything but
|
|
349
|
+
* `"spotify_sync"` goes on the interactive lane precisely so a person
|
|
350
|
+
* watching a spinner is not queued behind thousands of sync imports. Leave
|
|
351
|
+
* `sourceKind` alone and you get the good lane.
|
|
367
352
|
*
|
|
368
353
|
* ## The 401 that is really a 403
|
|
369
354
|
*
|
|
370
355
|
* `playlistId` pointing at a playlist you cannot update answers
|
|
371
|
-
* `401 "playlist not yours"
|
|
372
|
-
*
|
|
356
|
+
* `401 "playlist not yours"`, an authorization failure wearing an
|
|
357
|
+
* authentication status. Two consequences: it is an {@link OmsAuthError}, not an
|
|
373
358
|
* {@link OmsApiError}, so a `catch` sorting by class puts it in the wrong
|
|
374
359
|
* pile; and if the client was built with a token provider that implements
|
|
375
360
|
* `onUnauthorized`, the transport spends one pointless refresh on it before
|
|
@@ -377,8 +362,8 @@ export declare class MusicImportsNamespace extends Resource {
|
|
|
377
362
|
*
|
|
378
363
|
* @throws {TypeError} before any request when neither mode is satisfied.
|
|
379
364
|
* @throws {OmsApiError} 400 `"source_url is not allowed"` /
|
|
380
|
-
* `"artwork_url is not allowed"`
|
|
381
|
-
* missing mode, `404 "playlist not found"`.
|
|
365
|
+
* `"artwork_url is not allowed"` for a URL that is not public http(s),
|
|
366
|
+
* `400` naming the missing mode, `404 "playlist not found"`.
|
|
382
367
|
* @throws {OmsAuthError} 401 `"playlist not yours"`. See above.
|
|
383
368
|
*/
|
|
384
369
|
create(input: CreateSongImportInput, options?: RequestOptions): Promise<SongImport>;
|
|
@@ -391,12 +376,12 @@ export declare class MusicImportsNamespace extends Resource {
|
|
|
391
376
|
* semantics. `onProgress` is fed through {@link songImportProgress}, so its
|
|
392
377
|
* `loaded` is out of 100 even though the wire value is out of 1.
|
|
393
378
|
*
|
|
394
|
-
* Resolves for `"failed"` as well as `"complete"`: a download the
|
|
379
|
+
* Resolves for `"failed"` as well as `"complete"`: a download the server
|
|
395
380
|
* could not do is an ANSWER. Check `state` before reading `song_id`.
|
|
396
381
|
*
|
|
397
|
-
* `waitTimeoutMs` has no default here either.
|
|
398
|
-
*
|
|
399
|
-
*
|
|
382
|
+
* `waitTimeoutMs` has no default here either. The server gives a download
|
|
383
|
+
* ten minutes ({@link SONG_IMPORT_SERVER_TIMEOUT_MS}) and may then retry it,
|
|
384
|
+
* so a real import can outlive any deadline you pick; a deadline
|
|
400
385
|
* here abandons the WAIT, never the import, which keeps running and can be
|
|
401
386
|
* read later with {@link get}.
|
|
402
387
|
*
|
|
@@ -431,25 +416,20 @@ export declare class MusicImportsNamespace extends Resource {
|
|
|
431
416
|
* Nothing is written and nothing is enqueued, which is exactly why it is easy
|
|
432
417
|
* to mistake for cheap.
|
|
433
418
|
*
|
|
434
|
-
* ## It is not cheap
|
|
419
|
+
* ## It is not cheap
|
|
435
420
|
*
|
|
436
|
-
* Each call
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* serves. On 2026-07-27 a load generator drove this endpoint at roughly 900
|
|
440
|
-
* requests a minute with each one parked 20-40 seconds, and the whole API -
|
|
441
|
-
* health check included - went down with it. At the time it was the one
|
|
442
|
-
* yt-dlp-backed route with neither a rack-attack rule nor a controller-level
|
|
443
|
-
* limit. It now has both:
|
|
421
|
+
* Each call fetches metadata for a URL the CALLER chose and holds a server
|
|
422
|
+
* thread for up to 60 seconds while it does. It therefore carries two
|
|
423
|
+
* budgets:
|
|
444
424
|
*
|
|
445
425
|
* - {@link PLAYLIST_IMPORT_PREVIEW_HOURLY_LIMIT} 60 an hour, keyed by user
|
|
446
|
-
* id
|
|
426
|
+
* id. This one does NOT set `Retry-After`;
|
|
447
427
|
* - {@link PLAYLIST_IMPORT_PREVIEW_BURST_LIMIT_PER_MINUTE} 20 a minute, from
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
428
|
+
* a bucket SHARED with upscale, background removal, transcription, vocal
|
|
429
|
+
* separation, captions, jumpstyle, `/songs/:id/separate` and the
|
|
430
|
+
* downloader. Importing a playlist while a separation is running spends
|
|
431
|
+
* the same budget. This one does set `Retry-After`, which the transport
|
|
432
|
+
* honours.
|
|
453
433
|
*
|
|
454
434
|
* So: one preview per user action, never one per row of a list, and never
|
|
455
435
|
* inside a retry loop. This method therefore does NOT retry by default -
|
|
@@ -457,11 +437,11 @@ export declare class MusicImportsNamespace extends Resource {
|
|
|
457
437
|
* no, which a replay will not change. Pass `retry: {}` to opt back in.
|
|
458
438
|
*
|
|
459
439
|
* @throws {OmsApiError} 400 `"url is required"` when blank; 400
|
|
460
|
-
* `"url is not allowed"`
|
|
461
|
-
* at `/account/dashboard` for ANY `open.spotify.com` or
|
|
462
|
-
* Spotify is never previewed here, it goes through
|
|
440
|
+
* `"url is not allowed"` for a URL that is not public http(s); 400 with a
|
|
441
|
+
* message pointing at `/account/dashboard` for ANY `open.spotify.com` or
|
|
442
|
+
* `spotify.com` URL - Spotify is never previewed here, it goes through
|
|
463
443
|
* {@link MusicImportsNamespace.spotify}; 502 carrying the first 200
|
|
464
|
-
* characters of
|
|
444
|
+
* characters of the downloader's own error.
|
|
465
445
|
* @throws {OmsQuotaError} 429 from either budget above.
|
|
466
446
|
*/
|
|
467
447
|
previewPlaylist(url: string, options?: RequestOptions): Promise<PlaylistImportPreview>;
|
|
@@ -483,8 +463,8 @@ export type SpotifySyncPlaylistState = "pending" | "running" | "complete" | "fai
|
|
|
483
463
|
*
|
|
484
464
|
* `queued` and `skipped` count TRACKS, and together they are the walk's
|
|
485
465
|
* progress against `total`. What they are not is downloads: `queued` means a
|
|
486
|
-
*
|
|
487
|
-
*
|
|
466
|
+
* song import was created on the slow bulk lane. The audio arrives minutes or
|
|
467
|
+
* hours later.
|
|
488
468
|
*/
|
|
489
469
|
export interface SpotifySyncPlaylistProgress {
|
|
490
470
|
/** Spotify's playlist id, or the literal `"liked"` for the liked-songs mirror. */
|
|
@@ -500,11 +480,10 @@ export interface SpotifySyncPlaylistProgress {
|
|
|
500
480
|
}
|
|
501
481
|
/**
|
|
502
482
|
* Progress of the last (or current) sync run, stored on the Spotify identity
|
|
503
|
-
* rather than
|
|
483
|
+
* rather than as a job.
|
|
504
484
|
*
|
|
505
|
-
* Every key is optional here because a freshly linked identity carries `{}`
|
|
506
|
-
* the
|
|
507
|
-
* sync starts. Treat a missing `state` as `"idle"`.
|
|
485
|
+
* Every key is optional here because a freshly linked identity carries `{}`
|
|
486
|
+
* until the first sync starts. Treat a missing `state` as `"idle"`.
|
|
508
487
|
*/
|
|
509
488
|
export interface SpotifySyncProgress {
|
|
510
489
|
readonly state?: SpotifySyncRunState;
|
|
@@ -515,7 +494,7 @@ export interface SpotifySyncProgress {
|
|
|
515
494
|
* their text: `"Token refresh failed - please relink Spotify."` means the
|
|
516
495
|
* refresh token is dead and the user must go through the link flow again,
|
|
517
496
|
* while a stale run rewritten by {@link SpotifySyncNamespace.status} says
|
|
518
|
-
* `"Sincronização interrompida"` (the
|
|
497
|
+
* `"Sincronização interrompida"` (the server writes that one in Portuguese).
|
|
519
498
|
*/
|
|
520
499
|
readonly error?: string | null;
|
|
521
500
|
readonly playlists?: SpotifySyncPlaylistProgress[];
|
|
@@ -526,7 +505,7 @@ export interface SpotifySyncProgress {
|
|
|
526
505
|
* All three keys are absent on an identity that has never been configured, and
|
|
527
506
|
* absent is NOT `false` for any of them - each defaults differently:
|
|
528
507
|
*
|
|
529
|
-
* - `sync_liked` defaults to `true
|
|
508
|
+
* - `sync_liked` defaults to `true`;
|
|
530
509
|
* - `enabled_playlists` absent or `null` means EVERY eligible playlist, not
|
|
531
510
|
* none. An empty array means none;
|
|
532
511
|
* - `auto_sync` absent means "on if this identity has ever synced before",
|
|
@@ -572,9 +551,8 @@ export interface SpotifyPlaylistOption {
|
|
|
572
551
|
readonly owner: string | null;
|
|
573
552
|
readonly cover_url: string | null;
|
|
574
553
|
/**
|
|
575
|
-
* Whether this playlist is currently selected.
|
|
576
|
-
* `enabled_playlists
|
|
577
|
-
* unconfigured identity EVERY row comes back `true`.
|
|
554
|
+
* Whether this playlist is currently selected. On an unconfigured identity
|
|
555
|
+
* (no `enabled_playlists` saved) EVERY row comes back `true`.
|
|
578
556
|
*/
|
|
579
557
|
readonly enabled: boolean;
|
|
580
558
|
}
|
|
@@ -583,9 +561,9 @@ export interface SpotifyPlaylistOption {
|
|
|
583
561
|
*
|
|
584
562
|
* The list is already filtered to what can actually be synced: playlists owned
|
|
585
563
|
* by `"spotify"` (the editorial ones) and other people's non-collaborative
|
|
586
|
-
* playlists are dropped, because
|
|
587
|
-
*
|
|
588
|
-
*
|
|
564
|
+
* playlists are dropped, because Spotify does not let this integration read
|
|
565
|
+
* their tracks. A playlist the user can see in the Spotify app and not here is
|
|
566
|
+
* that rule, not a bug.
|
|
589
567
|
*/
|
|
590
568
|
export interface SpotifySyncPreview {
|
|
591
569
|
readonly sync_liked: boolean;
|
|
@@ -594,17 +572,16 @@ export interface SpotifySyncPreview {
|
|
|
594
572
|
/**
|
|
595
573
|
* Body of `PATCH /spotify_syncs/settings`.
|
|
596
574
|
*
|
|
597
|
-
* **Presence-sensitive, and two of the three keys delete data.**
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* delta.
|
|
575
|
+
* **Presence-sensitive, and two of the three keys delete data.** An omitted
|
|
576
|
+
* key is left alone and a present one is applied - which means you cannot
|
|
577
|
+
* express "leave enabled_playlists alone" by sending `null`, and you must send
|
|
578
|
+
* the WHOLE list every time rather than a delta.
|
|
602
579
|
*
|
|
603
580
|
* The destruction is immediate and synchronous, inside the PATCH:
|
|
604
581
|
*
|
|
605
582
|
* - `enabledPlaylists` **destroys the local copy** of every synced playlist
|
|
606
583
|
* whose Spotify id is not in the new list. Songs stay in the library; the
|
|
607
|
-
* playlist
|
|
584
|
+
* playlist and its rows do not. Re-enabling it later
|
|
608
585
|
* re-creates it from scratch on the next sync;
|
|
609
586
|
* - `syncLiked: false` **destroys the local "liked" mirror** the same way.
|
|
610
587
|
*
|
|
@@ -642,41 +619,36 @@ export interface StartSpotifySyncInput {
|
|
|
642
619
|
*/
|
|
643
620
|
readonly playlistIds?: string[];
|
|
644
621
|
}
|
|
645
|
-
/**
|
|
646
|
-
* A `"running"` sync older than this is treated as lost. Two hours, matching
|
|
647
|
-
* `SpotifySyncsController::STALE_RUNNING_AFTER`.
|
|
648
|
-
*/
|
|
622
|
+
/** A `"running"` sync older than this is treated as lost. Two hours. */
|
|
649
623
|
export declare const SPOTIFY_SYNC_STALE_AFTER_MS = 7200000;
|
|
650
624
|
/** True while a sync run is in flight. Anything else - `"idle"` included - is not. */
|
|
651
625
|
export declare function isSpotifySyncRunning(status: SpotifySyncStatus): boolean;
|
|
652
626
|
/**
|
|
653
627
|
* Spotify account mirroring, reachable as `oms.music.imports.spotify`.
|
|
654
628
|
*
|
|
655
|
-
* ## Every method here 403s unless
|
|
629
|
+
* ## Every method here 403s unless Spotify is enabled for the account
|
|
656
630
|
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
* of two, and the two fail in completely different places:
|
|
631
|
+
* Every route answers `403 "Spotify is not enabled for this account"` unless
|
|
632
|
+
* an administrator has enabled Spotify for the account. That is step one of
|
|
633
|
+
* two, and the two fail in completely different places:
|
|
661
634
|
*
|
|
662
|
-
* 1. **The
|
|
635
|
+
* 1. **The account flag.** Without it, `GET /auth/link/spotify` refuses before
|
|
663
636
|
* it ever redirects to Spotify: the user is bounced straight back with
|
|
664
637
|
* `?error=spotify_not_allowlisted`, having seen no Spotify screen at all.
|
|
665
638
|
* Every method in this class also 403s.
|
|
666
|
-
* 2. **
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* the last moment: the redirect to Spotify happens, the user signs
|
|
670
|
-
* then Spotify refuses the authorization itself.
|
|
671
|
-
*
|
|
672
|
-
* is redirected back with the same `?error=spotify_not_allowlisted`, never
|
|
639
|
+
* 2. **Spotify's own allowlist.** The integration runs in Spotify's
|
|
640
|
+
* Development Mode, which admits at most 25 named users. With the flag set
|
|
641
|
+
* but the user not registered on Spotify's side, the link flow LOOKS right
|
|
642
|
+
* up to the last moment: the redirect to Spotify happens, the user signs
|
|
643
|
+
* in, and then Spotify refuses the authorization itself. The user is
|
|
644
|
+
* redirected back with the same `?error=spotify_not_allowlisted`, never
|
|
673
645
|
* linked, with nothing in this API having recorded an attempt. From the
|
|
674
|
-
* outside it looks like the login silently died
|
|
675
|
-
*
|
|
646
|
+
* outside it looks like the login silently died, and the fix is on
|
|
647
|
+
* Spotify's side.
|
|
676
648
|
*
|
|
677
|
-
* Because step 2 fails outside
|
|
678
|
-
* answering `{ connected: false }` on an
|
|
679
|
-
*
|
|
649
|
+
* Because step 2 fails outside this API, {@link SpotifySyncNamespace.status}
|
|
650
|
+
* answering `{ connected: false }` on an enabled account is the normal symptom
|
|
651
|
+
* of it. `connected: false` means "no linked identity", which covers both
|
|
680
652
|
* "never tried" and "tried and Spotify said no".
|
|
681
653
|
*
|
|
682
654
|
* ## There is no sync id
|
|
@@ -693,15 +665,15 @@ export declare class SpotifySyncNamespace extends Resource {
|
|
|
693
665
|
* `GET /spotify_syncs/status` - the whole state of the link in one call.
|
|
694
666
|
*
|
|
695
667
|
* **This GET writes.** If the stored progress says `"running"` and started
|
|
696
|
-
* more than {@link SPOTIFY_SYNC_STALE_AFTER_MS} ago, the
|
|
697
|
-
*
|
|
698
|
-
*
|
|
668
|
+
* more than {@link SPOTIFY_SYNC_STALE_AFTER_MS} ago, the server rewrites it
|
|
669
|
+
* to `"failed"` before answering, because a run that died leaves an eternal
|
|
670
|
+
* `"running"` behind and
|
|
699
671
|
* {@link SpotifySyncNamespace.start} refuses to queue another while one is
|
|
700
672
|
* "in flight". Calling this is therefore how a stuck account gets unstuck -
|
|
701
673
|
* which also means it is not safe to treat as a cacheable read.
|
|
702
674
|
*
|
|
703
675
|
* @throws {OmsApiError} 403 `"Spotify is not enabled for this account"` when
|
|
704
|
-
*
|
|
676
|
+
* Spotify is not enabled for the account. See the class comment.
|
|
705
677
|
*/
|
|
706
678
|
status(options?: RequestOptions): Promise<SpotifySyncStatus>;
|
|
707
679
|
/**
|
|
@@ -717,7 +689,7 @@ export declare class SpotifySyncNamespace extends Resource {
|
|
|
717
689
|
* @throws {OmsApiError} 404 `"link your Spotify account first"` when there is
|
|
718
690
|
* no identity; 502 `"Spotify auth failed: ..."` when the refresh token is
|
|
719
691
|
* dead (the user must relink); 502 with Spotify's own message for anything
|
|
720
|
-
* else upstream; 403
|
|
692
|
+
* else upstream; 403 when Spotify is not enabled for the account.
|
|
721
693
|
*/
|
|
722
694
|
preview(options?: RequestOptions): Promise<SpotifySyncPreview>;
|
|
723
695
|
/**
|
|
@@ -731,8 +703,8 @@ export declare class SpotifySyncNamespace extends Resource {
|
|
|
731
703
|
* never deselected. Read {@link SpotifySyncNamespace.preview} first and send
|
|
732
704
|
* back the ids you got from it.
|
|
733
705
|
*
|
|
734
|
-
* @throws {OmsApiError} 404 `"link your Spotify account first"`; 403
|
|
735
|
-
* the
|
|
706
|
+
* @throws {OmsApiError} 404 `"link your Spotify account first"`; 403 when
|
|
707
|
+
* Spotify is not enabled for the account.
|
|
736
708
|
*/
|
|
737
709
|
updateSettings(input: UpdateSpotifySyncSettingsInput, options?: RequestOptions): Promise<SpotifySyncSettingsResult>;
|
|
738
710
|
/**
|
|
@@ -744,22 +716,22 @@ export declare class SpotifySyncNamespace extends Resource {
|
|
|
744
716
|
* previous run. That is what makes {@link SpotifySyncNamespace.waitForSync}
|
|
745
717
|
* race-free when it follows a `start()`.
|
|
746
718
|
*
|
|
747
|
-
* A manual run is not the same as the nightly one. It
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
719
|
+
* A manual run is not the same as the nightly one. It re-walks every
|
|
720
|
+
* enabled playlist even when Spotify says nothing changed - it is the "I
|
|
721
|
+
* think something is out of step" button - which is why it is much more
|
|
722
|
+
* expensive than the automatic sync and why it should be user-initiated,
|
|
723
|
+
* never polled into.
|
|
752
724
|
*
|
|
753
|
-
* What finishing means: `"complete"` says the walk is done and a
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
725
|
+
* What finishing means: `"complete"` says the walk is done and a song import
|
|
726
|
+
* exists for every new track. The downloads themselves are still queued
|
|
727
|
+
* behind however many thousand imports the run just created. The library
|
|
728
|
+
* fills in for a long time afterwards.
|
|
757
729
|
*
|
|
758
730
|
* @throws {OmsApiError} 409 `"a sync is already running"` - unless the
|
|
759
731
|
* running one is over two hours old, in which case
|
|
760
732
|
* {@link SpotifySyncNamespace.status} rewrites it to failed first and this
|
|
761
|
-
* then succeeds; 404 `"link your Spotify account first"`; 403
|
|
762
|
-
*
|
|
733
|
+
* then succeeds; 404 `"link your Spotify account first"`; 403 when Spotify
|
|
734
|
+
* is not enabled for the account.
|
|
763
735
|
*/
|
|
764
736
|
start(input?: StartSpotifySyncInput, options?: RequestOptions): Promise<SpotifySyncQueued>;
|
|
765
737
|
/**
|
|
@@ -789,11 +761,12 @@ export declare class SpotifySyncNamespace extends Resource {
|
|
|
789
761
|
* count (the liked mirror always does until it finishes).
|
|
790
762
|
*
|
|
791
763
|
* Polling here is also what clears a lost `"running"`, since `status` rewrites
|
|
792
|
-
* a stale one - so a wait against a
|
|
793
|
-
* than never.
|
|
764
|
+
* a stale one - so a wait against a run that died ends after two hours
|
|
765
|
+
* rather than never.
|
|
794
766
|
*
|
|
795
767
|
* @throws {OmsTimeoutError} `code: "timeout"` / `"aborted"`, as everywhere.
|
|
796
|
-
* @throws {OmsApiError} 403
|
|
768
|
+
* @throws {OmsApiError} 403 when Spotify is not enabled for the account, on
|
|
769
|
+
* the first poll.
|
|
797
770
|
*/
|
|
798
771
|
waitForSync(options?: WaitOptions): Promise<SpotifySyncStatus>;
|
|
799
772
|
/**
|
|
@@ -812,122 +785,6 @@ export declare class SpotifySyncNamespace extends Resource {
|
|
|
812
785
|
/** The one description of "watching a sync", shared by the two watchers. */
|
|
813
786
|
private syncPollPlan;
|
|
814
787
|
}
|
|
815
|
-
/** What `GET /s_r_machine/metadata` answers. Two keys, both nullable. */
|
|
816
|
-
export interface SRMachineMetadata {
|
|
817
|
-
readonly title: string | null;
|
|
818
|
-
readonly artist: string | null;
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* The SR Machine helpers, reachable as `oms.music.imports.srMachine`.
|
|
822
|
-
*
|
|
823
|
-
* ## Admin only, and the check is blunt
|
|
824
|
-
*
|
|
825
|
-
* `before_action :gatekeep` answers `403 "You SHALL NOT use this resource"` to
|
|
826
|
-
* anyone who is not an administrator. There is no allowlist, no scope and no
|
|
827
|
-
* per-user quota; the whole guard is `Current.user.admin?`.
|
|
828
|
-
*
|
|
829
|
-
* ## What it is
|
|
830
|
-
*
|
|
831
|
-
* Four unrelated primitives left over from the "slowed + reverb" video tool:
|
|
832
|
-
* fetch the artwork behind a URL, fetch its audio, read its title and artist,
|
|
833
|
-
* and transcode an arbitrary audio blob to Opus. Nothing here touches the
|
|
834
|
-
* library - no song is created, nothing is stored, and the bytes come back in
|
|
835
|
-
* the response body. If you want a track IN the library, use
|
|
836
|
-
* {@link MusicImportsNamespace.create} instead; this is the raw pipe.
|
|
837
|
-
*
|
|
838
|
-
* Three of the four shell out to yt-dlp against a caller-supplied URL and hold
|
|
839
|
-
* a Puma thread while they do it, exactly like
|
|
840
|
-
* {@link MusicImportsNamespace.previewPlaylist} - but note they are NOT in
|
|
841
|
-
* rack-attack's `expensive_tools` pattern and have no controller-level limit
|
|
842
|
-
* either, so the only ceiling is the general 600/min. The admin gate is what
|
|
843
|
-
* stands in for a budget here. Do not build a batch loop on top of these.
|
|
844
|
-
*
|
|
845
|
-
* A YouTube URL carrying `?list=` is truncated at the `?list=` before the fetch,
|
|
846
|
-
* so a link copied from inside a playlist resolves to the single video rather
|
|
847
|
-
* than the playlist. That happens server-side, in all three fetchers.
|
|
848
|
-
*
|
|
849
|
-
* All three fetchers respond with `Content-Disposition: attachment` and a
|
|
850
|
-
* fixed filename (`artwork.jpg`, `audio.opus`), so `FileOutput.filename` is
|
|
851
|
-
* that constant rather than anything derived from the source.
|
|
852
|
-
*/
|
|
853
|
-
export declare class SRMachineNamespace extends Resource {
|
|
854
|
-
/**
|
|
855
|
-
* `GET /s_r_machine/metadata` - the title and artist yt-dlp reads off a URL.
|
|
856
|
-
*
|
|
857
|
-
* Both keys can be `null` for a source that carries no tags; it is a
|
|
858
|
-
* best-effort read, not a lookup.
|
|
859
|
-
*
|
|
860
|
-
* Not retried by default: it parks a thread for up to the sidecar's
|
|
861
|
-
* 60-second metadata timeout, and a failure means the source refused.
|
|
862
|
-
*
|
|
863
|
-
* @throws {OmsAuthError} 403 `"You SHALL NOT use this resource"` for a
|
|
864
|
-
* non-admin.
|
|
865
|
-
* @throws {OmsApiError} 400 `"url is not allowed"` from the SSRF guard; 500
|
|
866
|
-
* when yt-dlp itself blows up - this controller has no `bad_gateway!`
|
|
867
|
-
* rescue, so an upstream failure surfaces as a server error AND fires a
|
|
868
|
-
* Discord alert.
|
|
869
|
-
*/
|
|
870
|
-
metadata(url: string, options?: RequestOptions): Promise<SRMachineMetadata>;
|
|
871
|
-
/**
|
|
872
|
-
* `GET /s_r_machine/artwork` - the cover behind a URL, as `image/jpeg`.
|
|
873
|
-
*
|
|
874
|
-
* Buffered fully into memory in every runtime, React Native included. It is a
|
|
875
|
-
* cover, so that is fine; {@link SRMachineNamespace.audio} is where it is not.
|
|
876
|
-
*
|
|
877
|
-
* @throws {OmsAuthError} 403 for a non-admin.
|
|
878
|
-
* @throws {OmsApiError} 400 `"url is not allowed"`; 500 on a fetch failure.
|
|
879
|
-
*/
|
|
880
|
-
artwork(url: string, options?: RequestOptions): Promise<FileOutput>;
|
|
881
|
-
/**
|
|
882
|
-
* `GET /s_r_machine/audio` - the audio behind a URL, as `audio/opus`.
|
|
883
|
-
*
|
|
884
|
-
* The whole track is downloaded server-side, held in memory there, and sent
|
|
885
|
-
* back in one body which this then buffers into memory again on the client.
|
|
886
|
-
* Nothing streams. On a phone that is a whole track in the JavaScript heap,
|
|
887
|
-
* and there is no signed-URL alternative here the way there is for library
|
|
888
|
-
* media - this endpoint has no storage node behind it. Reach for it on
|
|
889
|
-
* desktop, think twice on React Native, and never for a batch.
|
|
890
|
-
*
|
|
891
|
-
* Generous `timeoutMs` by default because a long track legitimately takes
|
|
892
|
-
* minutes.
|
|
893
|
-
*
|
|
894
|
-
* @throws {OmsAuthError} 403 for a non-admin.
|
|
895
|
-
* @throws {OmsApiError} 400 `"url is not allowed"`; 500 on a fetch failure.
|
|
896
|
-
*/
|
|
897
|
-
audio(url: string, options?: RequestOptions): Promise<FileOutput>;
|
|
898
|
-
/**
|
|
899
|
-
* `POST /s_r_machine/convert-opus` - transcode an audio file to Opus.
|
|
900
|
-
*
|
|
901
|
-
* Multipart, field name `file`, and the only method in this namespace that
|
|
902
|
-
* uploads. The response is `audio/opus` bytes, not JSON.
|
|
903
|
-
*
|
|
904
|
-
* Works on all three clients: a React Native `{ uri, name, type }` descriptor
|
|
905
|
-
* goes into the `FormData` verbatim and is streamed off disk by the native
|
|
906
|
-
* layer, while a browser or Bun caller passes a `FileInput` carrying a Blob
|
|
907
|
-
* or a `Uint8Array`. A `ReadableStream` is buffered first, because
|
|
908
|
-
* `FormData` has no streaming entry.
|
|
909
|
-
*
|
|
910
|
-
* The upload has no client-side size cap here because the server declares
|
|
911
|
-
* none - but Cloudflare sits in front of production and rejects a request
|
|
912
|
-
* body over roughly 100 MB with a 413 that never reaches Rails. There is no
|
|
913
|
-
* chunked path for this route, so a file above that simply cannot go through
|
|
914
|
-
* it.
|
|
915
|
-
*
|
|
916
|
-
* The server reads the whole part into memory (`params[:file].read`) and
|
|
917
|
-
* hands it to ffmpeg, so a large input is a large allocation on the Mac Mini
|
|
918
|
-
* as well as on the caller.
|
|
919
|
-
*
|
|
920
|
-
* @throws {OmsAuthError} 403 `"You SHALL NOT use this resource"` for a
|
|
921
|
-
* non-admin.
|
|
922
|
-
* @throws {OmsApiError} 500 when no `file` part was sent (`params[:file]` is
|
|
923
|
-
* `nil` and `.read` raises) or when ffmpeg refuses the input. Neither is a
|
|
924
|
-
* graceful 400.
|
|
925
|
-
* @throws {TypeError} when a React Native descriptor is passed on a runtime
|
|
926
|
-
* whose `FormData` is the web one, which would otherwise upload the literal
|
|
927
|
-
* text `"[object Object]"` and answer 500.
|
|
928
|
-
*/
|
|
929
|
-
convertToOpus(file: FileInput | NativeFile, options?: RequestOptions): Promise<FileOutput>;
|
|
930
|
-
}
|
|
931
788
|
/**
|
|
932
789
|
* Renders a sync run as the SDK's shared {@link Progress}.
|
|
933
790
|
*
|