@omelhorsite/sdk 0.3.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 (92) hide show
  1. package/README.md +113 -131
  2. package/dist/index.js +1049 -1246
  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 +2 -2
  10. package/dist/types/internal/attachments.d.ts +17 -0
  11. package/dist/types/internal/helpers.d.ts +9 -0
  12. package/dist/types/listing.d.ts +90 -0
  13. package/dist/types/local/qr.d.ts +1 -1
  14. package/dist/types/local/wordlist.d.ts +2 -3
  15. package/dist/types/resources/account.d.ts +19 -19
  16. package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
  17. package/dist/types/resources/admin/chests.d.ts +51 -0
  18. package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
  19. package/dist/types/resources/admin/identities.d.ts +99 -0
  20. package/dist/types/resources/admin/index.d.ts +77 -0
  21. package/dist/types/resources/admin/jobs.d.ts +97 -0
  22. package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
  23. package/dist/types/resources/admin/notepads.d.ts +49 -0
  24. package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
  25. package/dist/types/resources/admin/quotas.d.ts +114 -0
  26. package/dist/types/resources/admin/shortLinks.d.ts +201 -0
  27. package/dist/types/resources/admin/types.d.ts +222 -0
  28. package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
  29. package/dist/types/resources/admin.d.ts +17 -13
  30. package/dist/types/resources/auth/index.d.ts +1 -1
  31. package/dist/types/resources/auth/passkeys.d.ts +127 -163
  32. package/dist/types/resources/auth/sessions.d.ts +116 -161
  33. package/dist/types/resources/chests.d.ts +27 -31
  34. package/dist/types/resources/content/analysis.d.ts +62 -0
  35. package/dist/types/resources/content/blogs.d.ts +471 -0
  36. package/dist/types/resources/content/feedbacks.d.ts +263 -0
  37. package/dist/types/resources/content/index.d.ts +52 -0
  38. package/dist/types/resources/content/intel/articles.d.ts +230 -0
  39. package/dist/types/resources/content/intel/config.d.ts +135 -0
  40. package/dist/types/resources/content/intel/index.d.ts +53 -0
  41. package/dist/types/resources/content/intel/items.d.ts +91 -0
  42. package/dist/types/resources/content/intel/reports.d.ts +108 -0
  43. package/dist/types/resources/content/intel/scripts.d.ts +152 -0
  44. package/dist/types/resources/content/intel/sources.d.ts +236 -0
  45. package/dist/types/resources/content/intel/stats.d.ts +105 -0
  46. package/dist/types/resources/content/intel/types.d.ts +86 -0
  47. package/dist/types/resources/content/jokes.d.ts +93 -0
  48. package/dist/types/resources/content/notifications.d.ts +147 -0
  49. package/dist/types/resources/content/serviceUsages.d.ts +73 -0
  50. package/dist/types/resources/content/servicesStatus.d.ts +194 -0
  51. package/dist/types/resources/content/siteConfig.d.ts +43 -0
  52. package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
  53. package/dist/types/resources/content.d.ts +41 -236
  54. package/dist/types/resources/dynamicQrs.d.ts +29 -45
  55. package/dist/types/resources/forms.d.ts +37 -58
  56. package/dist/types/resources/jobs.d.ts +35 -44
  57. package/dist/types/resources/library/annotations.d.ts +162 -0
  58. package/dist/types/resources/library/books.d.ts +534 -0
  59. package/dist/types/resources/library/chat.d.ts +259 -0
  60. package/dist/types/resources/library/index.d.ts +39 -0
  61. package/dist/types/resources/library/shelves.d.ts +237 -0
  62. package/dist/types/resources/library/types.d.ts +120 -0
  63. package/dist/types/resources/library.d.ts +20 -22
  64. package/dist/types/resources/media.d.ts +48 -61
  65. package/dist/types/resources/movies/addons.d.ts +388 -0
  66. package/dist/types/resources/movies/collections.d.ts +352 -0
  67. package/dist/types/resources/movies/index.d.ts +29 -0
  68. package/dist/types/resources/movies/types.d.ts +50 -0
  69. package/dist/types/resources/movies/watchProgress.d.ts +277 -0
  70. package/dist/types/resources/movies.d.ts +27 -53
  71. package/dist/types/resources/music/artists.d.ts +182 -246
  72. package/dist/types/resources/music/imports.d.ts +190 -218
  73. package/dist/types/resources/music/index.d.ts +8 -7
  74. package/dist/types/resources/music/playlists.d.ts +85 -117
  75. package/dist/types/resources/music/social.d.ts +153 -228
  76. package/dist/types/resources/music/songs.d.ts +165 -227
  77. package/dist/types/resources/realtime.d.ts +75 -88
  78. package/dist/types/resources/shortLinks.d.ts +38 -47
  79. package/dist/types/resources/social/groupChats.d.ts +458 -0
  80. package/dist/types/resources/social/index.d.ts +19 -0
  81. package/dist/types/resources/social/messages.d.ts +424 -0
  82. package/dist/types/resources/social/relationships.d.ts +322 -0
  83. package/dist/types/resources/social/types.d.ts +37 -0
  84. package/dist/types/resources/social.d.ts +21 -3
  85. package/dist/types/resources/storage/upload.d.ts +42 -56
  86. package/dist/types/resources/storage.d.ts +94 -109
  87. package/dist/types/resources/tickets.d.ts +19 -10
  88. package/dist/types/resources/tools/backgroundRemoval.d.ts +11 -13
  89. package/dist/types/resources/tools/captions.d.ts +107 -135
  90. package/dist/types/resources/tools/upscale.d.ts +12 -16
  91. package/dist/types/types.d.ts +29 -38
  92. package/package.json +1 -1
@@ -5,45 +5,36 @@
5
5
  *
6
6
  * ## An artist row belongs to ONE user
7
7
  *
8
- * There is no global artist table. `Artist` is scoped by `user_id` and every
9
- * route here runs inside `Artist.viewable_by(Current.user)`, so two accounts
10
- * that both own a Chico Buarque track own two different artist rows with two
11
- * different integer ids. Never cache an artist id across identities, and never
12
- * hand one to another user's client.
8
+ * There is no global artist table. Every artist is scoped to the account that
9
+ * owns it, so two accounts that both own a Chico Buarque track own two
10
+ * different artist rows with two different integer ids. Never cache an artist
11
+ * id across identities, and never hand one to another user's client.
13
12
  *
14
- * Rows are created as a side effect of importing songs (`Songs::ArtistAttacher`)
15
- * and by `ArtistResolver`; this namespace has no `create`. `POST /artists` is
16
- * not routed - `resources :artists, only: [index, show, update, destroy]`.
13
+ * Rows are created as a side effect of importing songs; this namespace has no
14
+ * `create`, and `POST /artists` is not routed.
17
15
  *
18
16
  * ## Almost everything here shares ONE 60/min bucket
19
17
  *
20
- * `Rack::Attack` throttles `/lyrics`, `/artists/`, `/artist_metadata/` and
21
- * `/music_radios/` together at **60 requests a minute** - one bucket for all
22
- * four families, keyed by the literal `Authorization` header (or by IP when the
23
- * call is anonymous). A screen that opens an artist page while a lyrics panel
24
- * polls is spending the same budget twice.
18
+ * `/lyrics*`, `/artists/*`, `/artist_metadata/*` and `/music_radios/*` share
19
+ * **60 requests a minute** - one bucket for all four families, keyed by the
20
+ * `Authorization` header (or by IP when the call is anonymous). A screen that
21
+ * opens an artist page while a lyrics panel polls is spending the same budget
22
+ * twice.
25
23
  *
26
- * The regular expression is `\A/(lyrics|artists/|artist_metadata/|music_radios/)`
27
- * and the trailing slash after `artists` is load-bearing: the path is
28
- * normalised before it is matched, so `GET /artists` (the roster index) does
29
- * NOT match and falls back to the general authenticated ceiling of 600/min.
30
- * `/artists/overview`, `/artists/:id`, both uploads, `PATCH` and `DELETE` all
31
- * DO match. The `/artist_imports*` and `/artist_syncs*` routes do not (neither
32
- * `artist_imports` nor `artist_syncs` is `artists/`) and are on the general
33
- * ceiling as well.
34
- *
35
- * Every method below states which of the two it lands in.
24
+ * `GET /artists` itself (the roster index) is NOT in that bucket and falls
25
+ * under the general authenticated ceiling of 600/min. `/artists/overview`,
26
+ * `/artists/:id`, both uploads, `PATCH` and `DELETE` all are. `/artist_imports*`
27
+ * and `/artist_syncs*` are on the general ceiling as well. Every method below
28
+ * states which of the two it lands in.
36
29
  *
37
30
  * ## An OAuth access token cannot reach any of this
38
31
  *
39
- * `Authentication#enforce_oauth_scope!` denies by default: a Doorkeeper token
40
- * reaches an action only when its controller declared an `oauth_scope` for it.
41
- * No music controller declares one, so a CLI or MCP host holding an OAuth
42
- * token gets `403 {"error":"insufficient_scope"}` on every route in this file,
43
- * whatever scopes it was granted. Use a session token (`POST /sessions`) or,
44
- * in the browser, the session cookie.
32
+ * Every route in this file answers `403 {"error":"insufficient_scope"}` to an
33
+ * OAuth access token, whatever scopes it was granted. Use a session token
34
+ * (`POST /sessions`) or, in the browser, the session cookie.
45
35
  */
46
36
  import { ApiClient, Resource } from "../../http";
37
+ import type { ListParams } from "../../listing";
47
38
  import type { FileInput, NativeFile, Paginated, RequestOptions, Timestamp } from "../../types";
48
39
  /**
49
40
  * Primary key of an artist. An INTEGER, unlike users, sessions and storage
@@ -53,25 +44,23 @@ export type ArtistId = number;
53
44
  /** Primary key of an artist import. Also an integer. */
54
45
  export type ArtistImportId = number;
55
46
  /**
56
- * Page size the web roster and the mobile roster both use (FR-37: infinite
57
- * scroll, 60 a page). Nothing on the server requires it; it is here so the
58
- * three clients page identically and their caches line up.
47
+ * Default roster page size, 60. Nothing on the server requires it; it is here
48
+ * so every caller pages identically and caches line up.
59
49
  */
60
50
  export declare const ARTIST_ROSTER_PAGE_SIZE = 60;
61
51
  /** Largest artist image or banner the server will accept, in bytes. */
62
52
  export declare const ARTIST_IMAGE_MAX_BYTES: number;
63
53
  /**
64
- * Content types `Artists::ImageAttacher` maps directly to a stored extension.
54
+ * Content types the server maps directly to a stored extension.
65
55
  *
66
- * A file whose type is not in this list is NOT necessarily refused: the
67
- * attacher falls back to the extension of the filename and only gives up when
68
- * that is empty too. So `application/octet-stream` + `cover.png` is accepted
56
+ * A file whose type is not in this list is NOT necessarily refused: the server
57
+ * falls back to the extension of the filename and only gives up when that is
58
+ * empty too. So `application/octet-stream` + `cover.png` is accepted
69
59
  * and `application/octet-stream` + `cover` is a 400.
70
60
  */
71
61
  export declare const ARTIST_IMAGE_CONTENT_TYPES: readonly ["image/jpeg", "image/png", "image/webp", "image/gif"];
72
62
  /**
73
- * An artist, as `GET /artists` renders it (the blueprint's default view plus
74
- * two computed fields).
63
+ * An artist, as `GET /artists` renders it.
75
64
  *
76
65
  * ## Every image field, and which of them can be trusted
77
66
  *
@@ -86,13 +75,12 @@ export declare const ARTIST_IMAGE_CONTENT_TYPES: readonly ["image/jpeg", "image/
86
75
  * it - it is written only by `GET /songs/artist_pictures`, which lives in the
87
76
  * songs namespace. See {@link Artist.pictures_fetched_at} for why a fresh
88
77
  * timestamp there is not a promise.
89
- * - `external_image_url` comes from Last.fm through `ArtistResolver`, and is
90
- * usually null on purpose: Last.fm retired artist images and now answers with
91
- * one grey-star placeholder for everybody, which the resolver filters out
92
- * rather than store.
78
+ * - `external_image_url` comes from Last.fm, and is usually null on purpose:
79
+ * Last.fm retired artist images and now answers with one grey-star
80
+ * placeholder for everybody, which is filtered out rather than stored.
93
81
  * - `gallery_image_urls` (extended view only) is Wikipedia/Wikimedia.
94
82
  *
95
- * The resolution chain all three clients implement, in order:
83
+ * The resolution chain, in order:
96
84
  * `compressed_image_media_id` -> `image_media_id` -> the size-appropriate
97
85
  * `picture_*` (medium for an avatar, `picture_xl` for a hero) -> `picture` ->
98
86
  * `gallery_image_urls[0]` -> `fallback_artwork_media_id` -> `external_image_url`
@@ -100,10 +88,8 @@ export declare const ARTIST_IMAGE_CONTENT_TYPES: readonly ["image/jpeg", "image/
100
88
  *
101
89
  * ## `*_media_id` and `*_fs_node_id` are the same value, twice
102
90
  *
103
- * `ApplicationBlueprint.media_id_fields` emits both keys with an identical
104
- * value for every attachment, because the old web frontend still reads the
105
- * legacy `_fs_node_id` name. Read `_media_id`; the twin is scheduled to go.
106
- * Both docs in `oms-music/docs` document only the legacy name.
91
+ * Both keys carry an identical value for every attachment. Read `_media_id`;
92
+ * the `_fs_node_id` twin is a legacy alias scheduled to go.
107
93
  */
108
94
  export interface Artist {
109
95
  /** Integer, and unique only within one user's library. */
@@ -130,9 +116,8 @@ export interface Artist {
130
116
  /** Legacy twin of {@link Artist.image_media_id}. Same value. */
131
117
  readonly image_fs_node_id: string | null;
132
118
  /**
133
- * Legacy compressed variant. Only the `artists:merge` rake helpers ever
134
- * adopt one; nothing generates new ones, and an upload purges the stale
135
- * companion, so on a modern row this is null.
119
+ * Legacy compressed variant. Nothing generates new ones, and an upload
120
+ * purges the stale companion, so on a modern row this is null.
136
121
  */
137
122
  readonly compressed_image_media_id: string | null;
138
123
  /** Legacy twin of {@link Artist.compressed_image_media_id}. */
@@ -164,21 +149,18 @@ export interface Artist {
164
149
  * When the Deezer picture set was last WRITTEN - which is not the same as
165
150
  * when it was last checked.
166
151
  *
167
- * `ArtistPicturesFetcher` stamps this only on a successful answer (a hit, or
168
- * a genuine "not on Deezer" miss). A failure - and a quota refusal is a
169
- * failure that arrives as `HTTP 200` with an `{"error": ...}` body - stamps
170
- * NOTHING and only sets a 30-minute in-cache guard, precisely so an
171
- * over-quota sweep cannot mark a whole library as "fetched, no picture" for
172
- * three days. That is the shape of the bug this behaviour exists to prevent,
173
- * so do not read a stale timestamp as "Deezer has nothing for this artist".
152
+ * It is stamped only on a successful answer (a hit, or a genuine "not on
153
+ * Deezer" miss). A failure - a Deezer quota refusal included - stamps NOTHING
154
+ * and only pauses retries for 30 minutes, precisely so an over-quota sweep
155
+ * cannot mark a whole library as "fetched, no picture" for three days. So do
156
+ * not read a stale timestamp as "Deezer has nothing for this artist".
174
157
  */
175
158
  readonly pictures_fetched_at: Timestamp | null;
176
159
  /**
177
160
  * When Last.fm's biography was last fetched, successfully OR NOT.
178
161
  *
179
- * `ArtistResolver#populate!` rescues every transport failure and then stamps
180
- * all three `*_fetched_at` columns anyway, so the freshness gate stops
181
- * hammering an upstream that is down. The row therefore looks populated
162
+ * A failed fetch still stamps all three `*_fetched_at` fields, so an
163
+ * upstream that is down is not hammered. The row therefore looks populated
182
164
  * while `bio_html` is still null. A null `bio_html` next to a recent
183
165
  * `bio_fetched_at` means "we tried and got nothing", not "never tried".
184
166
  */
@@ -186,25 +168,21 @@ export interface Artist {
186
168
  /** Same semantics as {@link Artist.bio_fetched_at}, for the similar list. */
187
169
  readonly similar_fetched_at: Timestamp | null;
188
170
  /**
189
- * Number of `song_artists` join rows, counting every credit (primary,
190
- * featured and with), not distinct songs.
171
+ * Number of credits (primary, featured and with), not distinct songs.
191
172
  *
192
- * Computed per response, never stored. On `GET /artists` it comes from one
193
- * grouped `COUNT` for the whole page; everywhere else the blueprint runs its
194
- * own `COUNT(*)`. Always present.
173
+ * Computed per response, never stored. Always present.
195
174
  */
196
175
  readonly songs_count: number;
197
176
  /**
198
177
  * Artwork of one of this artist's own lead songs, for a card that has no
199
- * picture at all. A media id, resolvable through `oms.media` (the canonical
200
- * `/media/:id/data`); `/fs_nodes/:id/data` is the alias kept for the web
201
- * frontend and reaches the same bytes.
202
- *
203
- * NON-NULL ONLY ON `GET /artists` AND `GET /artists/:id`, where the
204
- * controller precomputes the whole page with a single `DISTINCT ON` query.
205
- * `PATCH` and both uploads render the blueprint standalone and answer null
206
- * here even for an artist that has one - so a card redrawn from an upload
207
- * response loses its fallback. Keep the value you already had.
178
+ * picture at all. A media id, resolvable through `oms.media`
179
+ * (`/media/:id/data`); `/fs_nodes/:id/data` is a legacy alias that reaches
180
+ * the same bytes.
181
+ *
182
+ * NON-NULL ONLY ON `GET /artists` AND `GET /artists/:id`. `PATCH` and both
183
+ * uploads answer null here even for an artist that has one - so a card
184
+ * redrawn from an upload response loses its fallback. Keep the value you
185
+ * already had.
208
186
  */
209
187
  readonly fallback_artwork_media_id: string | null;
210
188
  /** Legacy twin of {@link Artist.fallback_artwork_media_id}. */
@@ -213,28 +191,19 @@ export interface Artist {
213
191
  /** One entry of Last.fm's similar-artists list. */
214
192
  export interface ArtistSimilarEntry {
215
193
  readonly name: string;
216
- /**
217
- * Last.fm's similarity score, `0..1`. A NUMBER: the resolver runs `to_f`
218
- * before storing it. The web frontend types the whole `similar` field as an
219
- * object with an `artists` array, which is the DATABASE column's shape and
220
- * not the payload's - the blueprint flattens it to this array.
221
- */
194
+ /** Last.fm's similarity score, `0..1`. A number. */
222
195
  readonly match: number | null;
223
196
  readonly mbid: string | null;
224
197
  }
225
198
  /**
226
- * The `:extended` view: everything in {@link Artist} plus three fields.
227
- *
228
- * Blueprinter views INHERIT the base fields, so this is a superset and never a
229
- * subset. Returned by `GET /artists/:id`, `PATCH /artists/:id` and both upload
230
- * endpoints. `oms-music/docs/api-music.md` describes `:compact` and `:card` as
231
- * narrow subsets; they are not, for the same inheritance reason, and
232
- * `docs/API.md` says so correctly.
199
+ * The extended artist: everything in {@link Artist} plus three fields, a
200
+ * superset and never a subset. Returned by `GET /artists/:id`,
201
+ * `PATCH /artists/:id` and both upload endpoints.
233
202
  */
234
203
  export interface ArtistExtended extends Artist {
235
204
  /**
236
205
  * Last.fm biography summary. Contains HTML (and a trailing "Read more"
237
- * anchor); sanitised on the `/artist_metadata` shim but NOT here, so treat it
206
+ * anchor); sanitised on `/artist_metadata/:name` but NOT here, so treat it
238
207
  * as untrusted markup and sanitise before injecting it.
239
208
  */
240
209
  readonly bio_html: string | null;
@@ -242,15 +211,15 @@ export interface ArtistExtended extends Artist {
242
211
  * Wikipedia/Wikimedia photos of the artist.
243
212
  *
244
213
  * Writable through {@link MusicArtistsNamespace.update}, and OVERWRITTEN
245
- * without warning by the next metadata refresh: `ArtistResolver#populate!`
246
- * always assigns `gallery_image_urls` from `Artists::GalleryFetcher`, so a
247
- * hand-curated list survives only until the artist's gallery TTL (30 days,
248
- * jittered by up to 7) expires and someone opens the artist page. There is
214
+ * without warning by the next metadata refresh, which always replaces it
215
+ * with what Wikimedia returns. A hand-curated list survives only until the
216
+ * artist's gallery TTL (30 days, jittered by up to 7) expires and someone
217
+ * opens the artist page. There is
249
218
  * no "pinned" flag. If a client offers gallery editing, it has to be
250
219
  * prepared to re-apply it.
251
220
  */
252
221
  readonly gallery_image_urls: string[];
253
- /** Flattened from the stored `similar_json`. Empty array, never null. */
222
+ /** Empty array, never null. */
254
223
  readonly similar: ArtistSimilarEntry[];
255
224
  }
256
225
  /**
@@ -258,23 +227,23 @@ export interface ArtistExtended extends Artist {
258
227
  *
259
228
  * The server's allowlist for this index is exactly `id`, `name`, `slug`,
260
229
  * `canonical_name`, `created_at` and `updated_at`. Any other key inside
261
- * `search` / `exact_search` is a **400**, not a silently ignored filter -
262
- * `CrudActions#reject_unknown_filter_keys!` fails closed on purpose, because
263
- * the alternative (dropping the key) answers with the UNFILTERED table.
230
+ * `search` / `exact_search` is a **400**, not a silently ignored filter: the
231
+ * allowlist fails closed on purpose, because the alternative (dropping the
232
+ * key) would answer with the UNFILTERED list.
264
233
  */
265
- export interface ListArtistsParams {
234
+ export declare const ARTIST_FILTER_COLUMNS: readonly ["id", "name", "slug", "canonical_name", "created_at"];
235
+ export interface ListArtistsParams extends ListParams<(typeof ARTIST_FILTER_COLUMNS)[number]> {
266
236
  /**
267
237
  * Substring match on the display name - `search[name]`. This is the roster
268
238
  * search box.
269
239
  *
270
- * `QuerySearcher#string_search` slugifies BOTH sides before comparing:
271
- * lowercase, accents transliterated, every run of anything outside
272
- * `[a-z0-9-]` replaced by a hyphen, then wrapped in `%`. So the query is
273
- * really `slug(name) LIKE %slug(input)%`, and three consequences follow:
274
- * `"Beyoncé"` finds `"Beyonce"` and the reverse; `"chico buarque"` and
275
- * `"chico-buarque"` are the SAME query; and punctuation is erased on both
276
- * sides, so `"P!nk"` matches `"P nk"` too. An accent this table's
277
- * `TRANSLATE` map does not list survives as a hyphen, which still matches.
240
+ * The server slugifies BOTH sides before comparing: lowercase, accents
241
+ * transliterated, every run of anything outside `[a-z0-9-]` replaced by a
242
+ * hyphen, then a substring match. Three consequences follow: `"Beyoncé"`
243
+ * finds `"Beyonce"` and the reverse; `"chico buarque"` and `"chico-buarque"`
244
+ * are the SAME query; and punctuation is erased on both sides, so `"P!nk"`
245
+ * matches `"P nk"` too. An accent the transliteration does not know survives
246
+ * as a hyphen, which still matches.
278
247
  */
279
248
  readonly name?: string;
280
249
  /** Exact slug. Cheaper and less surprising than a `name` search. */
@@ -292,12 +261,11 @@ export interface ListArtistsParams {
292
261
  * `"column:asc"` / `"column:desc"`.
293
262
  *
294
263
  * PASS ONE. The index has no default order, so paging without it is paging
295
- * an unordered relation: Postgres may hand back the same row on two pages
296
- * and never hand back another. `name:asc` and `created_at:desc` are what the
297
- * clients use.
264
+ * an unordered set: the server may hand back the same row on two pages
265
+ * and never hand back another. `name:asc` and `created_at:desc` are the
266
+ * usual choices.
298
267
  *
299
- * `QueryModifier#apply_ordering` checks the column against
300
- * `model.column_names` and SILENTLY IGNORES anything else, so
268
+ * A column the record does not have is SILENTLY IGNORED, so
301
269
  * `songs_count:desc` is not an error and is not an ordering either - that
302
270
  * field is computed per response and cannot be sorted on. Sort client-side,
303
271
  * or read {@link MusicArtistsNamespace.overview}, which ranks for you.
@@ -366,11 +334,8 @@ export interface ArtistNeglectedEntry {
366
334
  }
367
335
  /**
368
336
  * The editorial header of the Artists page: five aggregate queries answered as
369
- * one document.
370
- *
371
- * The artists inside are rendered with the blueprint's `:card` view, which
372
- * inherits the base fields, so each one is a whole {@link Artist} - including
373
- * `fallback_artwork_media_id`, which the controller precomputes here.
337
+ * one document. Each artist inside is a whole {@link Artist},
338
+ * `fallback_artwork_media_id` included.
374
339
  */
375
340
  export interface ArtistOverview {
376
341
  readonly stats: ArtistOverviewStats;
@@ -400,13 +365,13 @@ export interface ArtistOverview {
400
365
  /** In the library, nothing played in 90 days. Up to 12. */
401
366
  readonly neglected: ArtistNeglectedEntry[];
402
367
  }
403
- /** `ArtistImport::STATES`. */
368
+ /** Every state an artist import can be in. */
404
369
  export declare const ARTIST_IMPORT_STATES: readonly ["queued", "running", "complete", "failed"];
405
370
  /** State of a bulk artist import. */
406
371
  export type ArtistImportState = (typeof ARTIST_IMPORT_STATES)[number];
407
372
  /**
408
- * Cadence the three clients poll an in-flight import at, from
409
- * `oms-music/docs/API.md` section 14. There is no cable channel for imports.
373
+ * Sensible cadence to poll an in-flight import at. Nothing pushes import
374
+ * progress over the realtime stream.
410
375
  */
411
376
  export declare const ARTIST_IMPORT_POLL_INTERVAL_MS = 1500;
412
377
  /**
@@ -420,10 +385,8 @@ export declare function isArtistImportTerminal(state: ArtistImportState | string
420
385
  /**
421
386
  * One bulk import of an artist's Spotify catalogue into the library.
422
387
  *
423
- * The counters exist because the pre-refactor flow swallowed Spotify errors
424
- * inside the job and left the user watching a spinner. Read them together:
425
- * `processed_albums` of `total_albums` is progress, and
426
- * `queued + skipped + failed` is what became of the tracks.
388
+ * Read the counters together: `processed_albums` of `total_albums` is
389
+ * progress, and `queued + skipped + failed` is what became of the tracks.
427
390
  */
428
391
  export interface ArtistImport {
429
392
  readonly id: ArtistImportId;
@@ -438,7 +401,7 @@ export interface ArtistImport {
438
401
  readonly album_ids: string[];
439
402
  readonly state: ArtistImportState;
440
403
  readonly total_albums: number | null;
441
- /** Null until the job has expanded the albums into tracks. */
404
+ /** Null until the albums have been expanded into tracks. */
442
405
  readonly total_tracks: number | null;
443
406
  readonly processed_albums: number;
444
407
  /** Tracks handed to the song-import pipeline. */
@@ -460,11 +423,7 @@ export interface ArtistImportRosterMatch {
460
423
  readonly id: ArtistId;
461
424
  readonly name: string;
462
425
  readonly slug: string;
463
- /**
464
- * A real URL (`picture_medium`, then `external_image_url`), or null. An
465
- * earlier version put a raw storage node id in this field, which no client
466
- * could load as an image.
467
- */
426
+ /** A real URL (`picture_medium`, then `external_image_url`), or null. */
468
427
  readonly image_url: string | null;
469
428
  }
470
429
  /** A Spotify search hit offered by the import picker. */
@@ -533,9 +492,9 @@ export interface ListArtistImportsParams {
533
492
  *
534
493
  * ## Every route here needs a LINKED SPOTIFY IDENTITY, not a flag
535
494
  *
536
- * The gate is `Current.user.identities.find_by(provider: "spotify")`. Without
537
- * one the answer is `400 "Connect Spotify first."`, and when the stored refresh
538
- * token no longer works it is `400 "Spotify connection needs to be relinked."`
495
+ * Without one the answer is `400 "Connect Spotify first."`, and when the
496
+ * stored refresh token no longer works it is
497
+ * `400 "Spotify connection needs to be relinked."`
539
498
  * Both are plain JSON strings, both arrive as an {@link OmsApiError} with
540
499
  * status 400, and the only way to tell them apart is the message - so match on
541
500
  * it if the UI needs to distinguish "connect" from "reconnect".
@@ -544,11 +503,10 @@ export interface ListArtistImportsParams {
544
503
  *
545
504
  * ## These calls are SLOW and they are on the general ceiling
546
505
  *
547
- * `/artist_imports*` does not match the 60/min proxy bucket, so it sits under
548
- * the general authenticated ceiling (600/min). What bounds it in practice is
549
- * Spotify: {@link search} and {@link albums} both call out synchronously on the
550
- * request thread, and {@link albums} pages through an entire discography. The
551
- * app allows 60 seconds for each, and so does this namespace.
506
+ * `/artist_imports*` sits under the general authenticated ceiling (600/min).
507
+ * What bounds it in practice is Spotify: {@link search} and {@link albums}
508
+ * both call out synchronously, and {@link albums} pages through an entire
509
+ * discography. This namespace allows 60 seconds for each.
552
510
  */
553
511
  export declare class MusicArtistImportsNamespace extends Resource {
554
512
  /**
@@ -577,12 +535,9 @@ export declare class MusicArtistImportsNamespace extends Resource {
577
535
  *
578
536
  * The server wraps the array in `{ items: [...] }`; this returns the array.
579
537
  *
580
- * `spotifyArtistId` is validated here rather than sent empty, because the
581
- * controller uses `params.require` and a missing key raises
582
- * `ActionController::ParameterMissing`. That escapes the API's own error
583
- * convention - it is a framework 400 with a framework body rather than a
584
- * bare JSON string - and it trips `ErrorReporting`, which pages the owner on
585
- * Discord for what is really a client bug.
538
+ * `spotifyArtistId` is validated here rather than sent empty: a missing key
539
+ * answers a generic 400 whose body is not one of the API's bare JSON
540
+ * strings.
586
541
  *
587
542
  * @throws {OmsError} `invalid_request` when `spotifyArtistId` is blank.
588
543
  * @throws {OmsApiError} 400 `"Connect Spotify first."` with no linked
@@ -595,21 +550,21 @@ export declare class MusicArtistImportsNamespace extends Resource {
595
550
  /**
596
551
  * `POST /artist_imports` - queues every track of the chosen albums.
597
552
  *
598
- * Answers `201` immediately with a `queued` record; the work happens in
599
- * `ArtistImportJob`. Watch it with {@link list} at
553
+ * Answers `201` immediately with a `queued` record; the work happens in the
554
+ * background. Watch it with {@link list} at
600
555
  * {@link ARTIST_IMPORT_POLL_INTERVAL_MS} - THERE IS NO `GET
601
556
  * /artist_imports/:id`, so polling means re-reading the recent list and
602
557
  * finding your id in it.
603
558
  *
604
559
  * Not retried, and this one matters more than most: a replay does not
605
- * deduplicate, it creates a second `ArtistImport` row and runs the whole
606
- * catalogue through the pipeline again. Pass `retry: {}` only if you are
560
+ * deduplicate, it creates a second import and runs the whole catalogue
561
+ * through the pipeline again. Pass `retry: {}` only if you are
607
562
  * prepared to explain the duplicate.
608
563
  *
609
564
  * @throws {OmsError} `invalid_request` when `spotifyArtistId` is blank or
610
- * `albumIds` is empty. The first would raise `ParameterMissing` on the
611
- * server (see {@link albums}); the second is a clean
612
- * `400 "album_ids required"`, checked here only so the round trip is saved.
565
+ * `albumIds` is empty. The first would answer a generic 400 (see
566
+ * {@link albums}); the second is a clean `400 "album_ids required"`,
567
+ * checked here only so the round trip is saved.
613
568
  * @throws {OmsApiError} 400 for the two Spotify-identity messages.
614
569
  */
615
570
  create(input: CreateArtistImportInput, options?: RequestOptions): Promise<ArtistImport>;
@@ -622,32 +577,29 @@ export declare class MusicArtistImportsNamespace extends Resource {
622
577
  *
623
578
  * This is also the poll: filter for a `state` that
624
579
  * {@link isArtistImportTerminal} rejects to know whether anything is still
625
- * running. Nothing pushes import progress over the cable.
580
+ * running. Nothing pushes import progress over the realtime stream.
626
581
  */
627
582
  list(params?: ListArtistImportsParams, options?: RequestOptions): Promise<ArtistImport[]>;
628
583
  }
629
584
  /**
630
585
  * Primary key of an artist sync. An integer, like {@link ArtistId} and
631
586
  * {@link ArtistImportId}, and interchangeable with NEITHER: a sync is keyed on
632
- * a SPOTIFY artist id and never references a local `Artist` row at all. Three
587
+ * a SPOTIFY artist id and never references a local artist at all. Three
633
588
  * integer id spaces meet in this file and only the field name tells them apart.
634
589
  */
635
590
  export type ArtistSyncId = number;
636
591
  /**
637
592
  * A followed artist, as `/artist_syncs` renders it.
638
593
  *
639
- * ## This payload is hand-built, so the base fields you expect are MISSING
594
+ * ## The base fields you expect are MISSING
640
595
  *
641
- * There is no `ArtistSyncBlueprint`. `ArtistSyncsController#serialize` writes
642
- * the hash literally, which is why this is the one music record with no
643
- * `created_at` and no `updated_at` - the convention that every payload carries
644
- * them holds everywhere a Blueprinter view is involved and stops here. Do not
645
- * sort a list of these by `created_at` client-side; the server already returns
646
- * them newest-first and that ordering is the only one available.
596
+ * This is the one music record with no `created_at` and no `updated_at`. Do
597
+ * not sort a list of these by `created_at` client-side; the server already
598
+ * returns them newest-first and that ordering is the only one available.
647
599
  *
648
- * `known_album_ids` is likewise not exposed. The row stores the full array of
649
- * Spotify album ids (a `jsonb` column) and only its SIZE crosses the wire, so
650
- * the SDK cannot tell you WHICH albums are already known - only how many.
600
+ * The known album ids themselves are not exposed either: only their COUNT
601
+ * crosses the wire, so the SDK cannot tell you WHICH albums are already known
602
+ * - only how many.
651
603
  */
652
604
  export interface ArtistSync {
653
605
  readonly id: ArtistSyncId;
@@ -663,12 +615,11 @@ export interface ArtistSync {
663
615
  */
664
616
  readonly artist_name: string | null;
665
617
  /**
666
- * Whether `ArtistDailySyncDispatcherJob` will pick this row up (its scope is
667
- * `where(enabled: true)`).
618
+ * Whether the daily check will pick this row up.
668
619
  *
669
620
  * Always `true` on anything this SDK can produce: `create` sets it, and there
670
621
  * is no update route to turn it off. A `false` here can only have been
671
- * written by the console, and the only way a client can stop a sync is
622
+ * written by an administrator, and the only way a client can stop a sync is
672
623
  * {@link MusicArtistSyncsNamespace.delete}. Render it, do not offer a toggle.
673
624
  */
674
625
  readonly enabled: boolean;
@@ -676,21 +627,20 @@ export interface ArtistSync {
676
627
  * When the daily check last ran, ISO-8601, or `null` in the vanishingly
677
628
  * short window before `create` saves the row.
678
629
  *
679
- * A TOUCH, not a success marker. `ArtistSyncCheckJob` writes it on the happy
680
- * path AND in both of its rescue arms (dead refresh token, Spotify upstream
681
- * error), so a fresh timestamp proves the job ran, never that Spotify
682
- * answered. There is no field that records the last failure - it is a
683
- * `Rails.logger.warn` and nothing else - so a UI cannot honestly say "last
630
+ * A TOUCH, not a success marker. It is written whether the check succeeded
631
+ * or failed (dead refresh token, Spotify upstream error), so a fresh
632
+ * timestamp proves the check ran, never that Spotify answered. There is no
633
+ * field that records the last failure, so a UI cannot honestly say "last
684
634
  * checked, all good".
685
635
  */
686
636
  readonly last_checked_at: Timestamp | null;
687
637
  /**
688
- * How many Spotify album ids the snapshot holds. `Array(known_album_ids).size`.
638
+ * How many Spotify album ids the snapshot holds.
689
639
  *
690
640
  * This is the baseline the daily diff runs against, not a count of songs
691
- * imported. It GROWS and never shrinks, because the job stores the union
692
- * (`known | current_ids`) precisely so an album Spotify hides and later shows
693
- * again cannot re-import as new.
641
+ * imported. It GROWS and never shrinks, because each check stores the union
642
+ * of what it knew and what it saw, precisely so an album Spotify hides and
643
+ * later shows again cannot re-import as new.
694
644
  */
695
645
  readonly known_album_count: number;
696
646
  }
@@ -723,7 +673,7 @@ export interface CreateArtistSyncInput {
723
673
  * | --- | --- | --- |
724
674
  * | what it does | imports the albums you CHOSE, now | watches for albums released LATER |
725
675
  * | back catalogue | yes, that is the point | never |
726
- * | when work happens | immediately, `ArtistImportJob` | daily, 05:00 server time |
676
+ * | when work happens | immediately | daily, 05:00 server time |
727
677
  * | you pick albums | yes, `albumIds` is required | no, there is no album argument |
728
678
  * | repeating the call | duplicates the whole import | idempotent, one row per artist |
729
679
  *
@@ -735,40 +685,35 @@ export interface CreateArtistSyncInput {
735
685
  *
736
686
  * ## What the sync produces is an ArtistImport, so watch it there
737
687
  *
738
- * `ArtistDailySyncDispatcherJob` runs at 05:00, walks every enabled row and
739
- * schedules each check at a random offset inside a 30-minute window (the same
740
- * anti-stampede discipline as the Spotify sync). Each `ArtistSyncCheckJob`
741
- * re-walks the artist's catalogue, diffs it against the snapshot and, when
742
- * something is new, creates an ordinary `ArtistImport` holding ONLY the new
743
- * album ids - `last_message` starts as `"Novo lançamento detectado pelo sync
744
- * diário…"`, which is the marker that tells an automatic import from one a
745
- * person asked for.
688
+ * The daily check runs at 05:00 server time, at a random offset inside a
689
+ * 30-minute window. It re-walks the artist's catalogue, diffs it against the
690
+ * snapshot and, when something is new, creates an ordinary
691
+ * {@link ArtistImport} holding ONLY the new album ids - `last_message` starts
692
+ * as `"Novo lançamento detectado pelo sync diário…"`, which is the marker that
693
+ * tells an automatic import from one a person asked for.
746
694
  *
747
695
  * So there is no progress on this namespace and nothing to poll here. Progress
748
696
  * lives in {@link MusicArtistImportsNamespace.list}, mixed in with manual
749
- * imports. Nothing pushes either over the cable.
697
+ * imports. Nothing pushes either over the realtime stream.
750
698
  *
751
699
  * ## A LINKED SPOTIFY IDENTITY is required to write, not to read
752
700
  *
753
- * {@link create} is gated on `Current.user.identities.find_by(provider:
754
- * "spotify")` and answers `400 "Connect Spotify first."` without one. {@link
701
+ * {@link create} answers `400 "Connect Spotify first."` without one. {@link
755
702
  * list} and {@link delete} are not gated, which matters after an unlink: the
756
- * rows survive, they still list, they can still be deleted, and the daily job
757
- * quietly skips them (`return unless identity`) without recording that it did.
703
+ * rows survive, they still list, they can still be deleted, and the daily
704
+ * check quietly skips them without recording that it did.
758
705
  *
759
706
  * ## Ceilings and cost
760
707
  *
761
708
  * Every route here is on the GENERAL authenticated ceiling of 600/min.
762
- * `/artist_syncs` does not match the 60/min `\A/(lyrics|artists/|...)` bucket -
763
- * the underscore breaks the `artists/` prefix - and it is not in
764
- * `EXPENSIVE_TOOL_PATHS` either, even though {@link create} is by far the
765
- * slowest call in this file.
709
+ * `/artist_syncs` is not in the 60/min `/artists/*` bucket and has no budget
710
+ * of its own, even though {@link create} is by far the slowest call in this
711
+ * file.
766
712
  *
767
713
  * ## An OAuth access token cannot reach any of this
768
714
  *
769
- * `ArtistSyncsController` declares no `oauth_scope`, and the gate denies by
770
- * default, so a Doorkeeper token gets `403 {"error":"insufficient_scope"}`
771
- * here as it does everywhere else in `music`. Session token or cookie only.
715
+ * An OAuth access token gets `403 {"error":"insufficient_scope"}` here as it
716
+ * does everywhere else in `music`. Session token or cookie only.
772
717
  */
773
718
  export declare class MusicArtistSyncsNamespace extends Resource {
774
719
  /**
@@ -784,8 +729,8 @@ export declare class MusicArtistSyncsNamespace extends Resource {
784
729
  * `/artist_imports` uses and the opposite of the bare arrays the rest of the
785
730
  * API returns - and is unwrapped here.
786
731
  *
787
- * Safe to retry, and scoped to the caller: `ArtistSync.viewable_by` is
788
- * `where(user:)`, so there is no way to read anyone else's follows.
732
+ * Safe to retry, and scoped to the caller: there is no way to read anyone
733
+ * else's follows.
789
734
  */
790
735
  list(options?: RequestOptions): Promise<ArtistSync[]>;
791
736
  /**
@@ -798,35 +743,31 @@ export declare class MusicArtistSyncsNamespace extends Resource {
798
743
  *
799
744
  * ## Idempotent, unlike its neighbour
800
745
  *
801
- * `find_or_initialize_by(user:, spotify_artist_id:)` behind a unique index,
802
- * so calling this twice for one artist updates a single row instead of
803
- * creating a second - it re-enables the sync, overwrites the name if one was
804
- * sent, and touches `last_checked_at`. It does NOT re-snapshot: the Spotify
805
- * walk is guarded by `if sync.known_album_ids.blank?`, so a second create is
806
- * cheap and, more importantly, cannot silently widen the baseline and swallow
807
- * releases that arrived in between.
746
+ * One row per artist per account, so calling this twice for one artist
747
+ * updates a single row instead of creating a second - it re-enables the
748
+ * sync, overwrites the name if one was sent, and touches `last_checked_at`.
749
+ * It does NOT re-snapshot when a snapshot already exists, so a second create
750
+ * is cheap and, more importantly, cannot silently widen the baseline and
751
+ * swallow releases that arrived in between.
808
752
  *
809
753
  * That is why this one opts INTO retries (`retry: {}`) while
810
754
  * {@link MusicArtistImportsNamespace.create} refuses them: a replayed follow
811
755
  * converges on the same row, a replayed import runs a whole discography
812
756
  * twice. Pass `retry: false` to opt back out.
813
757
  *
814
- * ## It is slow, because it walks the discography on the request thread
758
+ * ## It is slow, because it walks the discography before answering
815
759
  *
816
- * `SpotifyClient#each_artist_album` pages through every album before the
817
- * response is written, exactly like
760
+ * Every album is paged through before the response is written, exactly like
818
761
  * {@link MusicArtistImportsNamespace.albums}. Sixty seconds by default. The
819
762
  * one case that is instant is a re-create over a row that already has a
820
763
  * snapshot.
821
764
  *
822
765
  * An artist with a genuinely EMPTY catalogue never stops paying that cost:
823
- * `[]` is `blank?`, so every create for it walks Spotify again.
766
+ * an empty snapshot counts as no snapshot, so every create for it walks
767
+ * Spotify again.
824
768
  *
825
769
  * @throws {OmsError} `invalid_request` when `spotifyArtistId` is blank. The
826
- * server would answer `400` for it too - `params.require` raises
827
- * `ParameterMissing` and this controller, unlike the import one, RESCUES it
828
- * into a normal bad request, so it does not page the owner - but the round
829
- * trip buys nothing.
770
+ * server would answer `400` for it too, but the round trip buys nothing.
830
771
  * @throws {OmsApiError} 400 `"Connect Spotify first."` with no linked
831
772
  * identity, `"Spotify connection needs to be relinked."` on a dead refresh
832
773
  * token, or `"Spotify upstream error: ..."` (truncated to 200 characters).
@@ -854,8 +795,8 @@ export declare class MusicArtistSyncsNamespace extends Resource {
854
795
  * would find nothing and report `404` for a row it had just removed.
855
796
  *
856
797
  * @throws {OmsApiError} 404 `"Artist sync not found"` for an id that is not
857
- * yours or no longer exists. A non-numeric id lands here too, cast to `0`
858
- * by the column type rather than rejected.
798
+ * yours or no longer exists. A non-numeric id lands here too rather than
799
+ * being rejected.
859
800
  */
860
801
  delete(id: ArtistSyncId, options?: RequestOptions): Promise<void>;
861
802
  }
@@ -869,14 +810,12 @@ export declare class MusicArtistsNamespace extends Resource {
869
810
  /**
870
811
  * `GET /artists` - the roster.
871
812
  *
872
- * On the GENERAL ceiling (600/min), not the 60/min artist bucket: the
873
- * throttle's pattern is `/artists/` with a trailing slash and the index path
874
- * is normalised to `/artists`. It is the only route in this namespace with
875
- * that luxury.
813
+ * On the GENERAL ceiling (600/min), not the 60/min artist bucket. It is the
814
+ * only route in this namespace with that luxury.
876
815
  *
877
- * Rows are the blueprint's default view: no `bio_html`, no
878
- * `gallery_image_urls`, no `similar`. `songs_count` and
879
- * `fallback_artwork_media_id` ARE filled in here.
816
+ * Rows are the base {@link Artist}: no `bio_html`, no `gallery_image_urls`,
817
+ * no `similar`. `songs_count` and `fallback_artwork_media_id` ARE filled in
818
+ * here.
880
819
  *
881
820
  * ALWAYS PASS `order`. See {@link ListArtistsParams.order} - the relation
882
821
  * has no order of its own, and paging an unordered relation loses rows.
@@ -900,17 +839,16 @@ export declare class MusicArtistsNamespace extends Resource {
900
839
  * Three lookups in order: a purely numeric segment is an id, otherwise a
901
840
  * slug, otherwise a canonical name. So `get(42)`, `get("chico-buarque")` and
902
841
  * `get("Chico Buarque")` all work, and an artist whose slug is all digits
903
- * would be unreachable by slug - a case the backend does not handle and
842
+ * would be unreachable by slug - a case the server does not handle and
904
843
  * nothing in practice produces.
905
844
  *
906
845
  * ## This call can be slow the FIRST time, and it is not the network
907
846
  *
908
- * The controller runs `ArtistResolver.refresh_if_stale`, which is
909
- * stale-while-revalidate with one exception. A row that has never been
910
- * populated is filled in INLINE, on the request thread: a MusicBrainz search,
911
- * two Last.fm calls and a Wikimedia gallery fetch before the response is
912
- * written. A row that merely went stale is served immediately and refreshed
913
- * by a background job, deduplicated to one job per artist per ten minutes.
847
+ * Metadata is refreshed stale-while-revalidate, with one exception. A row
848
+ * that has never been populated is filled in INLINE, before the response: a
849
+ * MusicBrainz search, two Last.fm calls and a Wikimedia gallery fetch. A row
850
+ * that merely went stale is served immediately and refreshed in the
851
+ * background, at most once per artist per ten minutes.
914
852
  *
915
853
  * So budget a generous `timeoutMs` for a cold artist, and do not read a
916
854
  * slow first load as a broken server. What you must NOT do is retry it fast:
@@ -918,7 +856,7 @@ export declare class MusicArtistsNamespace extends Resource {
918
856
  *
919
857
  * ## What null metadata means
920
858
  *
921
- * If every external call fails, the resolver still stamps `bio_fetched_at`,
859
+ * If every external call fails, the server still stamps `bio_fetched_at`,
922
860
  * `similar_fetched_at` and the gallery timestamp, so the upstream is not
923
861
  * hammered on every page view. The response is a normal `200` with
924
862
  * `bio_html: null` and `similar: []`, and it will stay that way until the TTL
@@ -938,8 +876,8 @@ export declare class MusicArtistsNamespace extends Resource {
938
876
  /**
939
877
  * `GET /artists/overview` - the whole Artists page header in one request.
940
878
  *
941
- * **Cached server-side for one hour, per user**, under
942
- * `artists_overview:v2:<user id>`. Two consequences worth designing around:
879
+ * **Cached server-side for one hour, per user.** Two consequences worth
880
+ * designing around:
943
881
  * polling it is pointless, and an artist you just renamed or gave a new
944
882
  * picture keeps its old card here for up to an hour while
945
883
  * {@link MusicArtistsNamespace.get} already shows the new one. A client that
@@ -954,12 +892,10 @@ export declare class MusicArtistsNamespace extends Resource {
954
892
  *
955
893
  * ## The body is FLAT
956
894
  *
957
- * `{ name, gallery_image_urls }` at the top level. The web frontend sends
958
- * `{ artist: { ... } }`, which `params.permit(:name, gallery_image_urls: [])`
959
- * permits nothing out of: the update then assigns an empty hash, saves
960
- * successfully and answers `200` with the record UNCHANGED. A nested body is
961
- * not an error, it is a silent no-op, and it is the reason this method takes
962
- * the fields rather than a body.
895
+ * `{ name, gallery_image_urls }` at the top level. A body nested under an
896
+ * `artist` key is not an error, it is a silent no-op: the server answers
897
+ * `200` with the record UNCHANGED. That is the reason this method takes the
898
+ * fields rather than a body.
963
899
  *
964
900
  * Fields outside those two are dropped in silence as well (the fail-closed
965
901
  * 400 applies to filter buckets, not to update params), so read the returned
@@ -967,8 +903,8 @@ export declare class MusicArtistsNamespace extends Resource {
967
903
  *
968
904
  * ## Addressed by NUMERIC ID ONLY
969
905
  *
970
- * Unlike {@link get}, this goes through the generic CRUD lookup
971
- * (`find_by(id:)`), so a slug 404s - and with a different message,
906
+ * Unlike {@link get}, this is addressed by id only, so a slug 404s - and
907
+ * with a different message,
972
908
  * `"Resource not found"` rather than `"Artist not found"`. Resolve the slug
973
909
  * with {@link get} first.
974
910
  *
@@ -986,8 +922,8 @@ export declare class MusicArtistsNamespace extends Resource {
986
922
  *
987
923
  * ## A refusal arrives as 401, not 400
988
924
  *
989
- * `Artist#destroyable_by?` is `owner && song_artists.empty?`, and the CRUD
990
- * action turns a false there into
925
+ * An artist can be destroyed only by its owner and only when it has no
926
+ * credits left; anything else is
991
927
  * `401 "You are not authorized to destroy this resource"`. So "this artist
992
928
  * still has songs" and "this artist is not yours" are the SAME response, and
993
929
  * the SDK surfaces both as an {@link OmsAuthError}.
@@ -1023,8 +959,8 @@ export declare class MusicArtistsNamespace extends Resource {
1023
959
  /**
1024
960
  * `POST /artists/:id/upload_banner` - multipart, field name **`banner`**.
1025
961
  *
1026
- * The field is `banner`, NOT `image`. The web frontend sends `image` to this
1027
- * route and gets `400 "banner required"` for it; do not copy that.
962
+ * The field is `banner`, NOT `image`; sending `image` here answers
963
+ * `400 "banner required"`.
1028
964
  *
1029
965
  * ## Shared rules for both uploads
1030
966
  *
@@ -1044,13 +980,13 @@ export declare class MusicArtistsNamespace extends Resource {
1044
980
  * - Uploading purges the legacy `compressed_*` companion, because nothing
1045
981
  * regenerates it for artists and a leftover copy would keep rendering the
1046
982
  * OLD picture. Expect `compressed_banner_media_id` to be null afterwards.
1047
- * - The response is the `:extended` view rendered STANDALONE, so
1048
- * `fallback_artwork_media_id` comes back null even for an artist that has
1049
- * one. Merge the response into what you already had; do not replace it.
983
+ * - The response is an {@link ArtistExtended} whose
984
+ * `fallback_artwork_media_id` is null even for an artist that has one.
985
+ * Merge the response into what you already had; do not replace it.
1050
986
  * - Addressed like {@link get}, not like {@link update}: id, slug or
1051
987
  * canonical name all resolve, and the 404 message is `"Artist not found"`.
1052
988
  *
1053
- * ## The three clients
989
+ * ## The three runtimes
1054
990
  *
1055
991
  * React Native passes the picked `{ uri, name, type }` straight through; the
1056
992
  * transport appends it verbatim, which is the only thing that works there.