@omelhorsite/sdk 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +4 -4
  2. package/dist/index.js +4 -4
  3. package/dist/types/auth/device.d.ts +1 -1
  4. package/dist/types/auth/index.d.ts +2 -2
  5. package/dist/types/auth/tokens.d.ts +15 -15
  6. package/dist/types/client.d.ts +10 -10
  7. package/dist/types/errors.d.ts +12 -15
  8. package/dist/types/http.d.ts +74 -118
  9. package/dist/types/index.d.ts +1 -2
  10. package/dist/types/local/qr.d.ts +1 -1
  11. package/dist/types/local/wordlist.d.ts +2 -3
  12. package/dist/types/resources/account.d.ts +14 -17
  13. package/dist/types/resources/auth/index.d.ts +1 -1
  14. package/dist/types/resources/auth/passkeys.d.ts +127 -163
  15. package/dist/types/resources/auth/sessions.d.ts +110 -152
  16. package/dist/types/resources/chests.d.ts +27 -31
  17. package/dist/types/resources/dynamicQrs.d.ts +29 -45
  18. package/dist/types/resources/forms.d.ts +37 -58
  19. package/dist/types/resources/jobs.d.ts +28 -40
  20. package/dist/types/resources/media.d.ts +48 -61
  21. package/dist/types/resources/music/artists.d.ts +179 -245
  22. package/dist/types/resources/music/imports.d.ts +181 -210
  23. package/dist/types/resources/music/index.d.ts +8 -7
  24. package/dist/types/resources/music/playlists.d.ts +77 -110
  25. package/dist/types/resources/music/social.d.ts +153 -228
  26. package/dist/types/resources/music/songs.d.ts +160 -206
  27. package/dist/types/resources/realtime.d.ts +75 -88
  28. package/dist/types/resources/shortLinks.d.ts +33 -45
  29. package/dist/types/resources/storage/upload.d.ts +42 -56
  30. package/dist/types/resources/storage.d.ts +71 -104
  31. package/dist/types/resources/tools/backgroundRemoval.d.ts +11 -13
  32. package/dist/types/resources/tools/captions.d.ts +107 -135
  33. package/dist/types/resources/tools/upscale.d.ts +12 -16
  34. package/dist/types/types.d.ts +29 -38
  35. package/package.json +1 -1
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * The `music.songs` namespace: the library, its lyrics and its stems.
3
3
  *
4
- * Everything under here is strictly single-tenant. `Song.viewable_by` is
5
- * `where(user: current_user)` with no grant, no share and no public branch, so
6
- * there is no such thing as reading somebody else's track by id: a foreign id
7
- * is a `404 "Resource not found"`, never a `403`. The same is true of
4
+ * Everything under here is strictly single-tenant: there is no such thing as
5
+ * reading somebody else's track by id. A foreign id is a
6
+ * `404 "Resource not found"`, never a `403`. The same is true of
8
7
  * `liked_songs`, of `/artist_metadata/:name` (which resolves against YOUR
9
8
  * artist roster, not a global one) and of `/songs/artist_pictures`. Cross-user
10
9
  * song payloads exist in this API, but they arrive through jams and social
@@ -14,50 +13,42 @@
14
13
  *
15
14
  * **Song ids are integers.** So are `liked_songs` ids. The ids sitting NEXT to
16
15
  * them are not: `user_id` is a string uuid, every `*_media_id` is a string, and
17
- * a vocal separation's own `id` is a string. On ActionCable the same song ids
18
- * come back as strings. Compare with `===` against the right type or nothing
19
- * will ever match.
16
+ * a vocal separation's own `id` is a string. On the realtime stream the same
17
+ * song ids come back as strings. Compare with `===` against the right type or
18
+ * nothing will ever match.
20
19
  *
21
20
  * **Media never travels inline.** A song carries ids of storage nodes, not
22
21
  * bytes and not URLs. Resolve one with `oms.media` (`GET /media/:id/data_url`
23
- * hands back a short-lived signed URL, which is what you give a player). The
24
- * `oms.storage` route `GET /fs_nodes/:id/data_url` reaches the same bytes and
25
- * the web frontend still uses it, but the backend calls it a temporary alias
26
- * for that frontend, so new code should ask `oms.media`. Prefer the compressed
27
- * twin either way: `compressed_audio_media_id`
28
- * before `audio_media_id`, `compressed_artwork_media_id` before
29
- * `artwork_media_id`. The originals are lossless files on a Raspberry Pi and an
30
- * album grid that reaches for them takes seconds per tile.
22
+ * hands back a short-lived signed URL, which is what you give a player).
23
+ * `GET /fs_nodes/:id/data_url` is a legacy alias that reaches the same bytes;
24
+ * new code should ask `oms.media`. Prefer the compressed twin either way:
25
+ * `compressed_audio_media_id` before `audio_media_id`,
26
+ * `compressed_artwork_media_id` before `artwork_media_id`. The originals are
27
+ * lossless files on slow storage and an album grid that reaches for them takes
28
+ * seconds per tile.
31
29
  *
32
- * **Every key that names a media node is sent TWICE.** `ApplicationBlueprint.
33
- * media_id_fields` emits `<name>_media_id` and `<name>_fs_node_id` with the
34
- * identical value, the second being a temporary shim for the old web frontend.
35
- * Read the `_media_id` spelling; the twin is declared here only so that code
36
- * ported from the web app keeps type-checking, and it will be removed
37
- * server-side without a major version of this SDK.
30
+ * **Every key that names a media node is sent TWICE.** `<name>_media_id` and
31
+ * `<name>_fs_node_id` carry the identical value, the second being a legacy
32
+ * alias. Read the `_media_id` spelling; the twin is declared here only so that
33
+ * older code keeps type-checking, and it will be removed server-side without a
34
+ * major version of this SDK.
38
35
  *
39
36
  * ## An OAuth token cannot reach any of this
40
37
  *
41
- * None of the five controllers behind this namespace (`songs`, `liked_songs`,
42
- * `lyrics`, `artist_metadata`, `music/external_search`) declares an
43
- * `oauth_scope`, and `enforce_oauth_scope!` denies by omission: an OAuth access
44
- * token gets `403 {"error":"insufficient_scope"}` on EVERY method here, before
45
- * the action runs. Music needs a session cookie or a personal token. That is a
46
- * server-side gap rather than a design decision, so it may open later; until it
47
- * does, an {@link OmsAuthError} with status 403 and that body means "wrong kind
48
- * of credential", not "wrong user".
38
+ * An OAuth access token gets `403 {"error":"insufficient_scope"}` on EVERY
39
+ * method here. Music needs a session cookie or a personal token. That gap may
40
+ * close later; until it does, an {@link OmsAuthError} with status 403 and that
41
+ * body means "wrong kind of credential", not "wrong user".
49
42
  *
50
43
  * ## The 60-per-minute bucket nobody expects to share
51
44
  *
52
- * rack-attack throttles `/lyrics*`, `/artists/*`, `/artist_metadata/*` and
53
- * `/music_radios/*` through ONE rule - `external_proxy/by_session`, 60 requests
54
- * per minute keyed by the literal `Authorization` header - because all four
55
- * proxy to somebody else's servers (lrclib, Genius, Last.fm, Wikipedia, Deezer)
56
- * and getting our IP banned there breaks the feature for everyone. One counter,
57
- * four route families: fetching lyrics for sixty tracks in a minute leaves zero
58
- * budget for artist metadata, and the 429 lands on whichever call is unlucky
59
- * enough to be the sixty-first. Pace a backfill at roughly one request per
60
- * second and it will never be seen.
45
+ * `/lyrics*`, `/artists/*`, `/artist_metadata/*` and `/music_radios/*` share
46
+ * ONE budget of 60 requests per minute, keyed by the `Authorization` header,
47
+ * because all four proxy to somebody else's servers (lrclib, Genius, Last.fm,
48
+ * Wikipedia, Deezer). One counter, four route families: fetching lyrics for
49
+ * sixty tracks in a minute leaves zero budget for artist metadata, and the 429
50
+ * lands on whichever call is unlucky enough to be the sixty-first. Pace a
51
+ * backfill at roughly one request per second and it will never be seen.
61
52
  *
62
53
  * The rest of the namespace lives under the general ceiling (600/min
63
54
  * authenticated), with two exceptions that have their own budgets and their own
@@ -77,16 +68,16 @@ import type { VocalSeparation } from "../tools/vocalSeparation";
77
68
  * primary keys; `users`, `sessions`, `fs_nodes`, `playback_states` and
78
69
  * `vocal_separations` are string uuids. Both spellings are accepted wherever a
79
70
  * song id is taken as an argument, because a caller that read the id off a
80
- * cable message is holding a string, but the JSON these methods RETURN always
81
- * carries a number.
71
+ * realtime message is holding a string, but the JSON these methods RETURN
72
+ * always carries a number.
82
73
  */
83
74
  export type SongId = number | string;
84
75
  /** How an artist is credited on a track. */
85
76
  export type SongArtistRole = "primary" | "featured" | "with";
86
77
  /**
87
- * One row of `song_artists`, nested under {@link Song.artists}.
78
+ * One artist credit, nested under {@link Song.artists}.
88
79
  *
89
- * `id` is the id of the JOIN row, not of the artist - the artist's own id is
80
+ * `id` is the id of the credit row, not of the artist - the artist's own id is
90
81
  * `artist_id`. Getting those two the wrong way round is how a link to an artist
91
82
  * page ends up pointing at a random other artist, so prefer `slug` for routing
92
83
  * and keep `artist_id` for lookups.
@@ -98,14 +89,14 @@ export type SongArtistRole = "primary" | "featured" | "with";
98
89
  * `picture_xl`) lives on the artist record.
99
90
  */
100
91
  export interface SongArtistCredit extends Omit<BaseRecord, "id"> {
101
- /** Id of the `song_artists` join row. NOT the artist id. */
92
+ /** Id of the credit row. NOT the artist id. */
102
93
  readonly id: number;
103
94
  readonly song_id: number;
104
95
  readonly artist_id: number;
105
96
  /** Ordering within the credits. Sort by it; the server does not. */
106
97
  readonly position: number;
107
98
  readonly role: SongArtistRole;
108
- /** `null` only if the join row outlived its artist, which the schema prevents. */
99
+ /** `null` only if the credit outlived its artist, which the server prevents. */
109
100
  readonly name: string | null;
110
101
  readonly slug: string | null;
111
102
  /** Artist avatar the user uploaded, as a storage node id. Usually `null`. */
@@ -126,15 +117,10 @@ export type SongSourceKind = "upload" | "yt_dlp" | "spotify_sync";
126
117
  /**
127
118
  * A track in the library.
128
119
  *
129
- * `SongBlueprint` declares no extra fields in its `:extended` view, so the
130
- * shape below is what every route in this namespace answers with - index,
120
+ * The shape below is what every route in this namespace answers with - index,
131
121
  * show, update and import alike. There is no narrower view to guard against.
132
- *
133
- * Two fields the web frontend's own type declares do NOT exist on the wire:
134
- * `track_number` and `disc_number` (they are optional there and always
135
- * `undefined`), and neither does the legacy `artist` string column, which was
136
- * dropped when `song_artists` landed. {@link Song.artists} is the only artist
137
- * source.
122
+ * There is no `track_number`, no `disc_number` and no legacy `artist` string
123
+ * on the wire; {@link Song.artists} is the only artist source.
138
124
  */
139
125
  export interface Song extends Omit<BaseRecord, "id"> {
140
126
  /** Integer primary key. See {@link SongId}. */
@@ -181,7 +167,7 @@ export interface Song extends Omit<BaseRecord, "id"> {
181
167
  /**
182
168
  * Stamped when a separation starts and cleared when it settles, so a
183
169
  * non-null value means "a separation is in flight". It can go stale if a
184
- * worker dies; `GET /songs/:id/separation` clears it as a side effect, which
170
+ * run dies; `GET /songs/:id/separation` clears it as a side effect, which
185
171
  * is one reason to poll that route rather than re-reading the song.
186
172
  */
187
173
  readonly vocal_separation_started_at: Timestamp | null;
@@ -220,8 +206,8 @@ export interface Song extends Omit<BaseRecord, "id"> {
220
206
  /**
221
207
  * A row of `GET /songs/albums`.
222
208
  *
223
- * Not a database entity: the endpoint groups the caller's songs in Ruby and
224
- * this is the summary it builds. `name: null` is the bucket of songs with no
209
+ * Not a stored record: the endpoint groups the caller's songs and this is the
210
+ * summary it builds. `name: null` is the bucket of songs with no
225
211
  * album, and it is a legitimate row rather than an error.
226
212
  */
227
213
  export interface SongAlbumSummary {
@@ -262,8 +248,7 @@ export interface SongSeparationStatus {
262
248
  * The run itself, or `null` when this song has never had one.
263
249
  *
264
250
  * The same `VocalSeparation` record the `tools.vocalSeparation` namespace
265
- * returns - one model, one blueprint, two ways in - so it is imported rather
266
- * than redeclared here. For a song-owned run `vocals_url` and
251
+ * returns, so it is imported rather than redeclared here. For a song-owned run `vocals_url` and
267
252
  * `instrumental_url` are permanently `null`: the stems are written onto the
268
253
  * song as storage nodes, not attached to this row.
269
254
  */
@@ -277,7 +262,7 @@ export interface LikedSong extends Omit<BaseRecord, "id"> {
277
262
  readonly song_id: number;
278
263
  /** The cursor {@link ListLikedSongsParams.before} pages on. */
279
264
  readonly liked_at: Timestamp;
280
- /** Always present: the route renders the `:extended` view, which inlines it. */
265
+ /** Always present. */
281
266
  readonly song: Song;
282
267
  }
283
268
  /** Lyrics for one track. */
@@ -317,10 +302,8 @@ export interface ArtistMetadataSimilar {
317
302
  /**
318
303
  * The legacy payload `GET /artist_metadata/:name` answers with.
319
304
  *
320
- * `ArtistMetadataBlueprint` inherits `Blueprinter::Base` directly rather than
321
- * `ApplicationBlueprint`, so this is one of the very few records in the API
322
- * with NO `created_at` / `updated_at`. It also never 404s - see
323
- * {@link MusicSongsNamespace.artistMetadata}.
305
+ * One of the very few records in the API with NO `created_at` / `updated_at`.
306
+ * It also never 404s - see {@link MusicSongsNamespace.artistMetadata}.
324
307
  */
325
308
  export interface ArtistMetadata {
326
309
  /** `null` on the not-found branch. Integer when the artist exists. */
@@ -358,10 +341,9 @@ export interface ArtistMetadata {
358
341
  /**
359
342
  * Where an external-search hit came from.
360
343
  *
361
- * The frontend's own union also lists `"soundcloud"` and `"bandcamp"`. The
362
- * current `Music::ExternalSearch` queries Spotify, iTunes and YouTube and
363
- * nothing else, so those two branches are dead - keep handling them if you
364
- * ported code that does, but do not wait for them.
344
+ * `"soundcloud"` and `"bandcamp"` are kept for compatibility only: the search
345
+ * queries Spotify, iTunes and YouTube and nothing else, so they never appear
346
+ * today.
365
347
  */
366
348
  export type MusicExternalSource = "spotify" | "itunes" | "youtube" | "soundcloud" | "bandcamp";
367
349
  /** What to ask the external providers for. */
@@ -428,9 +410,9 @@ export interface SongFilters {
428
410
  /**
429
411
  * Partial title match, sent as `search[title]`.
430
412
  *
431
- * The comparison is slug-shaped: the server lowercases, strips accents
432
- * through a `TRANSLATE`, replaces every run of non-alphanumerics with a
433
- * hyphen and then does a `LIKE %...%` on both sides. So `"cafe"` finds
413
+ * The comparison is slug-shaped: the server lowercases, strips accents,
414
+ * replaces every run of non-alphanumerics with a hyphen and then does a
415
+ * substring match on both sides. So `"cafe"` finds
434
416
  * "Café", `"nao quero"` finds "Não Quero", and punctuation is irrelevant. It
435
417
  * is not a full-text index and there is no ranking.
436
418
  *
@@ -441,9 +423,8 @@ export interface SongFilters {
441
423
  readonly title?: string;
442
424
  /**
443
425
  * Exact album, sent as `exact_search[album]`. Pass `null` for the no-album
444
- * bucket: the transport encodes it as the backend's `\b` sentinel and
445
- * `Searchable.exact_search` turns that into `WHERE album IS NULL`, which is
446
- * the only way to ask for it.
426
+ * bucket: the transport encodes it as the server's `\b` null sentinel, which
427
+ * is the only way to ask for it.
447
428
  *
448
429
  * Not a substring match. `search[album]` exists and IS partial, but it can
449
430
  * never express the null bucket, so this field takes the exact route and the
@@ -457,10 +438,9 @@ export interface SongFilters {
457
438
  /**
458
439
  * Narrow to one artist, by canonical name OR by slug.
459
440
  *
460
- * This one is not a column and does not behave like the others. The
461
- * controller reads it straight out of `params`, resolves it against YOUR
462
- * artist roster (canonical name first, then slug) and joins through
463
- * `song_artists`. Consequences:
441
+ * This one is not a column and does not behave like the others. The server
442
+ * resolves it against YOUR artist roster (canonical name first, then slug)
443
+ * and matches on credits. Consequences:
464
444
  *
465
445
  * - it is EXACT even though it is spelled like a search. `search[artist]`
466
446
  * and `exact_search[artist]` are the same code path; there is no partial
@@ -468,8 +448,7 @@ export interface SongFilters {
468
448
  * filter client-side or go through `oms.music.artists`;
469
449
  * - an artist you do not have resolves to nothing and yields an EMPTY list,
470
450
  * not a 404 and not an error. An empty page is genuinely ambiguous here;
471
- * - it changes what {@link MusicSongsNamespace.albums} deduplicates on, which
472
- * is the point of that endpoint's `filter_artist_id`.
451
+ * - it changes what {@link MusicSongsNamespace.albums} deduplicates on.
473
452
  */
474
453
  readonly artist?: string;
475
454
  /**
@@ -490,23 +469,21 @@ export interface ListSongsParams extends SongFilters, ListParams<(typeof SONG_FI
490
469
  * Defaults to `"created_at:asc"`, which is also the endpoint's own base
491
470
  * order and the one that makes paging stable. Two traps:
492
471
  *
493
- * - a column the model does not have is IGNORED, silently. `modifiers[order]`
494
- * is an allowed key so the request is not rejected, and `QueryModifier`
495
- * simply returns before ordering, handing back the base order. A typo
496
- * costs you nothing but the sort you asked for;
497
- * - a real column REPLACES the base order (`reorder`, not `order`), tie
498
- * breaker included. Ordering 4000 tracks by `title:asc` when several share
499
- * a title gives Postgres licence to return them in a different sequence per
500
- * page, which duplicates and drops rows across a paged walk. Prefer
501
- * `created_at` or `id` for anything you intend to page through.
472
+ * - a column the record does not have is IGNORED, silently. `modifiers[order]`
473
+ * is an allowed key so the request is not rejected; the base order is
474
+ * handed back instead. A typo costs you nothing but the sort you asked for;
475
+ * - a real column REPLACES the base order, tie breaker included. Ordering
476
+ * 4000 tracks by `title:asc` when several share a title lets the database
477
+ * return them in a different sequence per page, which duplicates and drops
478
+ * rows across a paged walk. Prefer `created_at` or `id` for anything you
479
+ * intend to page through.
502
480
  *
503
481
  * A third `:`-separated segment pins specific values first
504
- * (`"album:asc:Clube da Esquina,Acabou Chorare"`), at the cost of an extra
505
- * `SELECT DISTINCT` over the column.
482
+ * (`"album:asc:Clube da Esquina,Acabou Chorare"`).
506
483
  */
507
484
  readonly order?: string;
508
485
  /**
509
- * `modifiers[random]=true` - shuffle server-side with `RANDOM()`.
486
+ * `modifiers[random]=true` - shuffle server-side.
510
487
  *
511
488
  * Mutually destructive with paging: the ordering is re-evaluated per request,
512
489
  * so page 2 of a random listing is a fresh shuffle and shares rows with page
@@ -529,7 +506,7 @@ export interface ListSongAlbumsParams extends SongFilters, ListParams<(typeof SO
529
506
  /**
530
507
  * Fields {@link MusicSongsNamespace.update} can change.
531
508
  *
532
- * Four real columns and three virtual inputs. The virtual ones are the
509
+ * Four real fields and three virtual inputs. The virtual ones are the
533
510
  * complicated half; each carries its own note.
534
511
  */
535
512
  export interface UpdateSongInput {
@@ -545,14 +522,14 @@ export interface UpdateSongInput {
545
522
  * there is no comma-splitting heuristic, so a name that genuinely contains a
546
523
  * comma survives.
547
524
  *
548
- * An EMPTY array is not "remove every artist": it is `blank?` server-side,
549
- * which drops it back to `nil` and re-runs the legacy parser. There is no way
550
- * through this endpoint to leave a song with no artists at all.
525
+ * An EMPTY array is not "remove every artist": the server treats it as
526
+ * absent and re-runs the legacy parser. There is no way through this
527
+ * endpoint to leave a song with no artists at all.
551
528
  */
552
529
  readonly artistNames?: string[];
553
530
  /**
554
531
  * The featured credits, replacing whatever is there. Sending this key at all
555
- * is what switches the backend out of its legacy mode.
532
+ * is what switches the server out of its legacy mode.
556
533
  *
557
534
  * That legacy mode is a heuristic over the TITLE: with no `featured_artist_
558
535
  * names` key present, the server re-reads `"Song (feat. X)"` and rebuilds the
@@ -568,7 +545,7 @@ export interface UpdateSongInput {
568
545
  /**
569
546
  * Legacy single-line artist input, re-parsed server-side (it splits on
570
547
  * commas and on "feat."). Prefer {@link UpdateSongInput.artistNames}; this
571
- * exists for parity with the old web form.
548
+ * exists for compatibility.
572
549
  */
573
550
  readonly artist?: string;
574
551
  /**
@@ -593,16 +570,15 @@ export interface ListLikedSongsParams {
593
570
  *
594
571
  * Deliberately not an offset. The list is ordered by `liked_at` descending
595
572
  * and liking one track mid-scroll shifts every later offset page by one,
596
- * which shows a duplicate and hides a row. A `Date` is encoded as ISO-8601,
597
- * which is what `Time.zone.parse` wants; an unparseable string is a
598
- * `400 "Invalid before timestamp"`.
573
+ * which shows a duplicate and hides a row. A `Date` is encoded as ISO-8601;
574
+ * an unparseable string is a `400 "Invalid before timestamp"`.
599
575
  */
600
576
  readonly before?: string | Date;
601
577
  }
602
578
  /** Arguments for {@link MusicSongsNamespace.startSeparation}. */
603
579
  export interface StartSongSeparationInput {
604
580
  /**
605
- * Which sidecar model to run. Omit for the default. The selectable list is
581
+ * Which separation model to run. Omit for the default. The selectable list is
606
582
  * `oms.tools.vocalSeparation.models()`; an id that is not on it is a
607
583
  * `400 "Unknown model"`.
608
584
  */
@@ -624,9 +600,8 @@ export interface ModifySongMetadataInput {
624
600
  /** The file to retag. Hard cap 50 MiB, enforced before anything else happens. */
625
601
  readonly audio: FileInput | NativeFile;
626
602
  /**
627
- * At least one tag is REQUIRED. An empty bag raises server-side and comes
628
- * back as a 500 with a Discord page attached, so this method rejects it
629
- * locally instead.
603
+ * At least one tag is REQUIRED. An empty bag comes back as a 500, so this
604
+ * method rejects it locally instead.
630
605
  */
631
606
  readonly metadata: SongFileMetadata;
632
607
  }
@@ -668,14 +643,14 @@ export declare function isMusicExternalSearchRateLimited(error: unknown): boolea
668
643
  * `"Chico Buarque, Milton Nascimento (feat. Elis Regina)"`: primaries joined
669
644
  * with `", "`, then a `feat.` clause. Pure string building, no request.
670
645
  *
671
- * `with` credits are excluded unless `includeWith` is set, which mirrors what
672
- * the three clients do - they render only in a credits dialog and in media
673
- * session metadata, where completeness beats line length.
646
+ * `with` credits are excluded unless `includeWith` is set; they belong in a
647
+ * credits dialog and in media session metadata, where completeness beats line
648
+ * length.
674
649
  *
675
- * Written here because {@link Song.artists} arrives UNSORTED and every client
676
- * that forgot to sort by `position` printed the credits in insertion order,
677
- * which is roughly random. It also copes with a jam entry, whose `artist_names`
678
- * is a pre-joined string and whose `artists` array is empty.
650
+ * Written here because {@link Song.artists} arrives UNSORTED and forgetting to
651
+ * sort by `position` prints the credits in insertion order, which is roughly
652
+ * random. It also copes with a jam entry, whose `artist_names` is a pre-joined
653
+ * string and whose `artists` array is empty.
679
654
  */
680
655
  export declare function songArtistsLine(song: Pick<Song, "artists"> & Partial<Pick<Song, "artist_names">>, includeWith?: boolean): string;
681
656
  /** The `music.songs` namespace, reachable as `oms.music.songs`. */
@@ -683,15 +658,13 @@ export declare class MusicSongsNamespace extends Resource {
683
658
  /**
684
659
  * `GET /songs` - the caller's library, oldest first.
685
660
  *
686
- * Pagination is FORCED here and nowhere else in this namespace: the
687
- * controller overrides `modifiers_params` for `index` only, so a request with
688
- * no page modifier is given `1:500` and one asking for more than 500 is
689
- * clamped to it. That is a DoS guard, not tidiness - a five-thousand-track
690
- * library serialises megabytes of JSON with every credit inlined, and a
691
- * handful of concurrent unbounded listings used to exhaust the Puma threads.
692
- * The SDK sends a page modifier every time, so the clamp never surprises you
693
- * and {@link Paginated.pageSize} always reports the size the rows were
694
- * counted against.
661
+ * Pagination is FORCED here and nowhere else in this namespace: a request
662
+ * with no page modifier is given `1:500` and one asking for more than 500 is
663
+ * clamped to it. That is a guard, not tidiness - a five-thousand-track
664
+ * library serialises megabytes of JSON with every credit inlined. The SDK
665
+ * sends a page modifier every time, so the clamp never surprises you and
666
+ * {@link Paginated.pageSize} always reports the size the rows were counted
667
+ * against.
695
668
  *
696
669
  * Order defaults to `created_at:asc`, the endpoint's own base order, which is
697
670
  * what makes a paged walk stable. See {@link ListSongsParams.order} before
@@ -704,14 +677,12 @@ export declare class MusicSongsNamespace extends Resource {
704
677
  * @throws {OmsAuthError} 401 when anonymous, 403 for an OAuth token.
705
678
  * @throws {OmsApiError} 400 naming the offending key when a filter is not in
706
679
  * {@link SONG_FILTER_COLUMNS}. Filters fail closed on purpose: silently
707
- * dropping an unknown one used to answer with the UNFILTERED set.
680
+ * dropping an unknown one would answer with the UNFILTERED set.
708
681
  */
709
682
  list(params?: ListSongsParams, options?: RequestOptions): Promise<Paginated<Song>>;
710
683
  /**
711
- * `GET /songs/:id` - one track.
712
- *
713
- * Renders the `:extended` view, which `SongBlueprint` leaves identical to the
714
- * default one, so this returns exactly what a row of {@link list} carries.
684
+ * `GET /songs/:id` - one track. Returns exactly what a row of {@link list}
685
+ * carries.
715
686
  *
716
687
  * @throws {OmsApiError} 404 `"Resource not found"` - for an id that does not
717
688
  * exist AND for one that belongs to somebody else, indistinguishably. The
@@ -722,18 +693,16 @@ export declare class MusicSongsNamespace extends Resource {
722
693
  * `PATCH /songs/:id` - edits metadata, and optionally replaces the artwork.
723
694
  *
724
695
  * JSON normally; multipart as soon as {@link UpdateSongInput.artwork} is
725
- * present, because that is the only way to carry a file. Both encodings reach
726
- * the same code path server-side, and this method papers over the two places
727
- * where they would otherwise behave differently:
728
- *
729
- * - **clearing a column in multipart.** Every form field is a string, so
730
- * there is no `null` to send. The backend's `\b` sentinel is decoded for
731
- * update params exactly as it is for filters, so `album: null` is written
732
- * as that one character and clears the column. (The Expo app's own comment
733
- * claims multipart cannot express this and splits the request in two; it
734
- * can, and it does not need to.)
696
+ * present, because that is the only way to carry a file. Both encodings
697
+ * behave the same server-side, and this method papers over the two places
698
+ * where they would otherwise differ:
699
+ *
700
+ * - **clearing a field in multipart.** Every form field is a string, so
701
+ * there is no `null` to send. The server's `\b` null sentinel is decoded
702
+ * for update fields exactly as it is for filters, so `album: null` is
703
+ * written as that one character and clears the field.
735
704
  * - **an empty `featuredArtistNames`.** Appending an empty array appends
736
- * nothing, and an absent key is what puts the backend back into its
705
+ * nothing, and an absent key is what puts the server back into its
737
706
  * title-parsing legacy mode - the opposite of what "no featured artists"
738
707
  * means. This sends the single empty string the server reads as an explicit
739
708
  * empty list.
@@ -742,8 +711,7 @@ export declare class MusicSongsNamespace extends Resource {
742
711
  * credits you did not touch. Send `featuredArtistNames` whenever you care
743
712
  * about them.
744
713
  *
745
- * @throws {OmsAuthError} 401 when the song is not yours - `update` checks
746
- * `updatable_by?` and answers 401, not 403 or 404.
714
+ * @throws {OmsAuthError} 401 when the song is not yours - not 403 or 404.
747
715
  * @throws {OmsApiError} 400 `"Music storage quota exceeded"` when the artwork
748
716
  * would not fit in the music quota.
749
717
  */
@@ -768,20 +736,20 @@ export declare class MusicSongsNamespace extends Resource {
768
736
  *
769
737
  * Answers **200**, not 201, with the created song. Do not branch on the code.
770
738
  *
771
- * Two size limits and they are not the same one. Rails rejects anything over
772
- * {@link SONG_IMPORT_MAX_BYTES} (1 GiB) with a 400 - but production sits
773
- * behind Cloudflare, which refuses a request body over roughly 100 MB with
774
- * its own `413` before Rails ever sees it. A big FLAC therefore fails with an
775
- * HTML-ish 413 that says nothing about songs. There is no chunked import
776
- * route; that ceiling is real.
739
+ * Two size limits and they are not the same one. The API rejects anything
740
+ * over {@link SONG_IMPORT_MAX_BYTES} (1 GiB) with a 400 - but the CDN in
741
+ * front of it refuses a request body over roughly 100 MB with its own `413`
742
+ * before the API ever sees it. A big FLAC therefore fails with an HTML-ish
743
+ * 413 that says nothing about songs. There is no chunked import route; that
744
+ * ceiling is real.
777
745
  *
778
746
  * On React Native pass the picker's `{ uri, name, type }` object directly -
779
747
  * it is appended verbatim and streamed off disk by the native layer.
780
748
  *
781
749
  * @throws {OmsApiError} 400 for a missing file, a file over 1 GiB, an
782
750
  * extension outside {@link SONG_IMPORT_EXTENSIONS}, or
783
- * `"Music storage quota exceeded"`; 415 with the model's validation
784
- * messages when the audio itself will not import.
751
+ * `"Music storage quota exceeded"`; 415 with validation messages when the
752
+ * audio itself will not import.
785
753
  */
786
754
  import(file: FileInput | NativeFile, options?: RequestOptions): Promise<Song>;
787
755
  /**
@@ -790,10 +758,9 @@ export declare class MusicSongsNamespace extends Resource {
790
758
  * Takes the same filters as {@link list}, and is the endpoint every album
791
759
  * grid is built on. It is also the most expensive read in this namespace, for
792
760
  * a reason worth understanding: the forced pagination that protects
793
- * `GET /songs` is applied to the `index` action ONLY, so this action loads
794
- * the whole filtered library, eager-loads the credits and deduplicates in
795
- * Ruby by `[album, primary artist]`. On a five-thousand-track library that is
796
- * a full table scan per call.
761
+ * `GET /songs` does not apply here, so this route loads the whole filtered
762
+ * library and deduplicates by `[album, primary artist]`. On a
763
+ * five-thousand-track library that is a full scan per call.
797
764
  *
798
765
  * Paging it does not fix that and is usually a mistake:
799
766
  * {@link ListSongAlbumsParams.page} pages the SONGS that get scanned, and the
@@ -814,8 +781,8 @@ export declare class MusicSongsNamespace extends Resource {
814
781
  *
815
782
  * A flat array of strings, ordered by name, and that is the whole payload.
816
783
  *
817
- * It IGNORES every filter you could send it: the action never touches the
818
- * listing scope, it plucks names straight off the artists table. This method
784
+ * It IGNORES every filter you could send it: it reads names straight off the
785
+ * roster. This method
819
786
  * therefore takes no parameters at all rather than accepting some that would
820
787
  * do nothing. It also does not go through `/artists`, so despite the shared
821
788
  * subject it does NOT spend the 60/min external-proxy budget.
@@ -830,9 +797,8 @@ export declare class MusicSongsNamespace extends Resource {
830
797
  * artist you already have.
831
798
  *
832
799
  * Lookup only: an artist absent from your roster returns `[]` and no stub row
833
- * is created, which is deliberate - the old frontend used to send slugs and
834
- * joined display strings ("100 gecs, Lil West, Tony Velour") here and
835
- * polluted the artist table with them.
800
+ * is created, which is deliberate - a slug or a joined display string
801
+ * ("100 gecs, Lil West, Tony Velour") must not mint stub artists.
836
802
  *
837
803
  * The array holds zero or one entry. Zero means either "not your artist" or
838
804
  * "Deezer has never given us a picture for them", and the two are not
@@ -854,8 +820,8 @@ export declare class MusicSongsNamespace extends Resource {
854
820
  *
855
821
  * The odd one out in this namespace: it writes nothing to the library, reads
856
822
  * nothing from it, and answers with BINARY rather than JSON. The file is
857
- * remuxed with ffmpeg (`-c:a copy`, so the audio is never re-encoded) with the
858
- * new tags and, when given one, an embedded cover.
823
+ * remuxed with the new tags (the audio is never re-encoded) and, when given
824
+ * one, an embedded cover.
859
825
  *
860
826
  * The result is buffered fully into memory in every runtime. That is
861
827
  * unavoidable - there is no URL to hand a downloader - but it means a 50 MiB
@@ -863,17 +829,16 @@ export declare class MusicSongsNamespace extends Resource {
863
829
  * the server suggested in `Content-Disposition`.
864
830
  *
865
831
  * At least one tag must be present, and this method enforces that locally
866
- * because the server does not fail gracefully: an empty `metadata` hash
867
- * raises inside the service, escapes as a 500, and pages a human on Discord.
868
- * A missing audio file does the same. Both are avoided here.
832
+ * because the server does not fail gracefully: an empty `metadata` bag is a
833
+ * 500, and so is a missing audio file. Both are avoided here.
869
834
  *
870
- * The web frontend also sends `metadata[track_number]`; the backend's permit
871
- * list drops it in silence, so it is not offered here.
835
+ * `metadata[track_number]` is dropped in silence server-side, so it is not
836
+ * offered here.
872
837
  *
873
838
  * @throws {TypeError} when `metadata` carries no usable tag.
874
839
  * @throws {OmsApiError} 413 `"File too big"` above
875
840
  * {@link SONG_METADATA_MAX_BYTES}, checked before the body is read. Note
876
- * that Cloudflare's own ~100 MB body limit sits above it and never fires
841
+ * that the CDN's own ~100 MB body limit sits above it and never fires
877
842
  * first.
878
843
  */
879
844
  modifyMetadata(input: ModifySongMetadataInput, options?: RequestOptions): Promise<FileOutput>;
@@ -886,18 +851,14 @@ export declare class MusicSongsNamespace extends Resource {
886
851
  * enqueued. So there is no need to guard the call site - but also no way to
887
852
  * force a re-run without deleting the stems first.
888
853
  *
889
- * **Rate limit:** 20 requests per minute, from the `expensive_tools` bucket
890
- * SHARED with `POST /vocal_separations`, the upscaler, transcriptions,
891
- * caption jobs, jumpstyle and the yt-dlp previews. It is there because each
892
- * call schedules minutes of CPU and several gigabytes of RAM on the native
893
- * sidecar; a load generator that walked a library firing one separation per
894
- * song pushed the machine into swap, which is exactly what this budget now
895
- * prevents. Do not batch a library through it.
854
+ * **Rate limit:** 20 requests per minute, from a bucket SHARED with
855
+ * `POST /vocal_separations`, the upscaler, transcriptions, caption jobs,
856
+ * jumpstyle and the downloader previews. Each call schedules minutes of CPU
857
+ * and gigabytes of RAM. Do not batch a library through it.
896
858
  *
897
859
  * The run itself is asynchronous. Poll {@link separation} roughly every three
898
860
  * seconds; the returned row's `status` moves `pending` to `processing` to
899
- * `complete` or `failed`, and there is no `canceled` no matter what the web
900
- * frontend's own type says.
861
+ * `complete` or `failed`, and there is no `canceled`.
901
862
  *
902
863
  * @throws {OmsQuotaError} 429 when the shared expensive-tools budget is spent.
903
864
  * @throws {OmsAuthError} 401 when the song is not yours.
@@ -915,16 +876,15 @@ export declare class MusicSongsNamespace extends Resource {
915
876
  * `complete` a moment before the stems are attached, and a song can have
916
877
  * stems from a run that was swept long ago.
917
878
  *
918
- * Reading this has a SIDE EFFECT, and it is a useful one:
919
- * `ClearStaleSeparationFlag` runs first and clears
920
- * {@link Song.vocal_separation_started_at} when the flag outlived its job (a
921
- * worker that died mid-run leaves it set forever). A UI that decides "a
879
+ * Reading this has a SIDE EFFECT, and it is a useful one: the server first
880
+ * clears {@link Song.vocal_separation_started_at} when the flag outlived its
881
+ * run (a run that died midway leaves it set forever). A UI that decides "a
922
882
  * separation is in flight" from the song record alone can therefore be stuck
923
883
  * on a spinner that only this call will clear.
924
884
  *
925
885
  * Poll at about three seconds. Nothing on this route is throttled beyond the
926
- * general ceiling, but `progress_percent` is fetched live from the sidecar on
927
- * every call while the run is processing, so a tighter loop costs real work.
886
+ * general ceiling, but `progress_percent` is fetched live on every call
887
+ * while the run is processing, so a tighter loop costs real work.
928
888
  *
929
889
  * @throws {OmsApiError} 404 for a song that is not yours.
930
890
  */
@@ -953,18 +913,17 @@ export declare class MusicSongsNamespace extends Resource {
953
913
  * End of list is a short page, as everywhere in this API. There is no count.
954
914
  *
955
915
  * Each row carries a whole {@link Song} with its credits, so 500 likes is a
956
- * large response; the server preloads to avoid the N+1, but the bytes are
957
- * still bytes. 100 is the size the clients actually use.
916
+ * large response. 100 is a sensible size.
958
917
  *
959
- * @throws {OmsApiError} 400 `"Invalid before timestamp"` for a cursor
960
- * `Time.zone.parse` cannot read. Pass a `Date` and this cannot happen.
918
+ * @throws {OmsApiError} 400 `"Invalid before timestamp"` for a cursor the
919
+ * server cannot parse. Pass a `Date` and this cannot happen.
961
920
  */
962
921
  listLiked(params?: ListLikedSongsParams, options?: RequestOptions): Promise<LikedSong[]>;
963
922
  /**
964
923
  * `GET /liked_songs/ids` - just the song ids, as integers.
965
924
  *
966
- * The cheap way to render heart icons over a listing. It plucks a single
967
- * column with no pagination and no cap, so it returns EVERY like in one
925
+ * The cheap way to render heart icons over a listing. It is a single flat
926
+ * list with no pagination and no cap, so it returns EVERY like in one
968
927
  * array - which is the point, and also means it grows without bound. At a few
969
928
  * thousand likes it is still a handful of kilobytes.
970
929
  *
@@ -975,8 +934,8 @@ export declare class MusicSongsNamespace extends Resource {
975
934
  /**
976
935
  * `POST /liked_songs` - likes a track. Answers `201` with the new row.
977
936
  *
978
- * Genuinely idempotent: the controller is a `find_or_create_by!`, so liking
979
- * twice returns the same row rather than erroring or creating a duplicate.
937
+ * Genuinely idempotent: liking twice returns the same row rather than
938
+ * erroring or creating a duplicate.
980
939
  * That is why this is one of the few `POST`s in the SDK that opts INTO the
981
940
  * retry policy - a replay after a lost answer cannot produce a second like,
982
941
  * and the alternative is a heart that silently did nothing.
@@ -1033,17 +992,16 @@ export declare class MusicSongsNamespace extends Resource {
1033
992
  * view. Cached per song, target and lyrics digest, which means the second
1034
993
  * request for a translation is free and a lyrics refetch invalidates it.
1035
994
  *
1036
- * **Retries are disabled and this one really matters.** The hourly cap is
1037
- * enforced by an `increment`-then-compare counter, so a rejected call STILL
1038
- * INCREMENTS it: a client that retries a 429 three times has pushed itself
1039
- * three further past the cap without ever getting an answer. The 429 also
995
+ * **Retries are disabled and this one really matters.** The hourly cap
996
+ * counts a rejected call too: a client that retries a 429 three times has
997
+ * pushed itself three further past the cap without ever getting an answer. The 429 also
1040
998
  * carries no `Retry-After`, so the transport would back off by its own
1041
999
  * schedule - a few hundred milliseconds - into a limit measured in hours.
1042
1000
  * Wait out the window instead. Pass `retry` explicitly if you disagree.
1043
1001
  *
1044
- * **Two limits at once:** 60 fresh translations per user per HOUR (429,
1045
- * app-level), on top of the shared 60/min external-proxy bucket that every
1046
- * `/lyrics*` call draws on.
1002
+ * **Two limits at once:** 60 fresh translations per user per HOUR (429), on
1003
+ * top of the shared 60/min external-proxy bucket that every `/lyrics*` call
1004
+ * draws on.
1047
1005
  *
1048
1006
  * @throws {OmsQuotaError} 429 from either budget.
1049
1007
  * @throws {OmsApiError} 400 `"Unsupported target"` for a locale outside
@@ -1058,8 +1016,8 @@ export declare class MusicSongsNamespace extends Resource {
1058
1016
  * `POST /lyrics/sync` - generates LRC timestamps for plain-text lyrics.
1059
1017
  *
1060
1018
  * Answers `201 { job_id }` and does the work in the background: it separates
1061
- * the vocals if there are no stems, transcribes them on the Whisper sidecar
1062
- * and aligns the known lines against the segments. Minutes of machine time
1019
+ * the vocals if there are no stems, transcribes them and aligns the known
1020
+ * lines against the segments. Minutes of machine time
1063
1021
  * per call, which is what the 10-per-hour cap is protecting.
1064
1022
  *
1065
1023
  * Wait for it either way: `oms.jobs.wait({ id: job_id })`, or simply re-read
@@ -1085,8 +1043,7 @@ export declare class MusicSongsNamespace extends Resource {
1085
1043
  * exceeded"`, not `429`.** Every handler that routes by status code reads
1086
1044
  * that as a malformed query and does the worst possible thing - rewords it
1087
1045
  * and tries again, spending more of a budget that is already gone. The limit
1088
- * is 30 requests per minute per user, counted in the controller rather than
1089
- * in rack-attack, which is why it never reaches the 429 path.
1046
+ * is 30 requests per minute per user.
1090
1047
  * {@link isMusicExternalSearchRateLimited} is the check to use, and the
1091
1048
  * transport will not retry a 400 on its own, so nothing recovers silently.
1092
1049
  *
@@ -1104,7 +1061,7 @@ export declare class MusicSongsNamespace extends Resource {
1104
1061
  * A blank query short-circuits to three empty lists WITHOUT spending budget,
1105
1062
  * which makes it safe to wire straight to an input. A cached hit does spend
1106
1063
  * it: the rate check runs before the 15-minute cache is consulted. Debounce
1107
- * and require two characters, as the clients do.
1064
+ * and require two characters.
1108
1065
  *
1109
1066
  * Every failing upstream is swallowed server-side, so a partial answer and a
1110
1067
  * complete one are indistinguishable - an empty `tracks` may mean "no
@@ -1120,13 +1077,10 @@ export declare class MusicSongsNamespace extends Resource {
1120
1077
  * **It never 404s.** An artist outside your roster comes back as `200` with
1121
1078
  * every field `null` except `name`, echoed back verbatim, and
1122
1079
  * `similar: []`. There is no error to catch and no flag to read: check
1123
- * whether `id` is null. That branch also creates nothing - the shim used to
1124
- * be handed slugs and joined display strings and would mint stub artists from
1125
- * them.
1080
+ * whether `id` is null. That branch also creates nothing.
1126
1081
  *
1127
1082
  * The payload has no `created_at` / `updated_at`, unlike essentially every
1128
- * other record in this API: `ArtistMetadataBlueprint` inherits
1129
- * `Blueprinter::Base` directly to pin the exact legacy key set.
1083
+ * other record in this API.
1130
1084
  *
1131
1085
  * Reading a stale artist triggers a lazy background refresh from Last.fm and
1132
1086
  * MusicBrainz, so the first call after a while may be slower and the second
@@ -1148,7 +1102,7 @@ export declare class MusicSongsNamespace extends Resource {
1148
1102
  * Builds the query for `/songs` and `/songs/albums`.
1149
1103
  *
1150
1104
  * `artist_role` is deliberately TOP LEVEL and not inside `exact_search`: the
1151
- * controller reads it off bare `params`, and nesting it would both miss the
1105
+ * server reads it as a plain parameter, and nesting it would both miss the
1152
1106
  * filter and trip the unknown-key check.
1153
1107
  */
1154
1108
  private songQuery;
@@ -1157,9 +1111,9 @@ export declare class MusicSongsNamespace extends Resource {
1157
1111
  /**
1158
1112
  * Multipart fields for an update, with the two encoding differences fixed.
1159
1113
  *
1160
- * `null` becomes the `\b` sentinel, which `CrudActions` decodes back to `nil`
1161
- * for update params exactly as it does for filters - a form field has no
1162
- * other way to say "clear this column". An empty `featured_artist_names`
1114
+ * `null` becomes the `\b` sentinel, which the server decodes back to null
1115
+ * for update fields exactly as it does for filters - a form field has no
1116
+ * other way to say "clear this field". An empty `featured_artist_names`
1163
1117
  * becomes a single empty string, because appending an empty array appends
1164
1118
  * nothing and an absent key means the opposite thing.
1165
1119
  */