@omelhorsite/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +112 -130
  2. package/dist/index.js +4800 -610
  3. package/dist/types/client.d.ts +60 -3
  4. package/dist/types/http.d.ts +444 -19
  5. package/dist/types/index.d.ts +5 -1
  6. package/dist/types/internal/attachments.d.ts +17 -0
  7. package/dist/types/internal/helpers.d.ts +9 -0
  8. package/dist/types/listing.d.ts +90 -0
  9. package/dist/types/resources/account.d.ts +71 -5
  10. package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
  11. package/dist/types/resources/admin/chests.d.ts +51 -0
  12. package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
  13. package/dist/types/resources/admin/identities.d.ts +99 -0
  14. package/dist/types/resources/admin/index.d.ts +77 -0
  15. package/dist/types/resources/admin/jobs.d.ts +97 -0
  16. package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
  17. package/dist/types/resources/admin/notepads.d.ts +49 -0
  18. package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
  19. package/dist/types/resources/admin/quotas.d.ts +114 -0
  20. package/dist/types/resources/admin/shortLinks.d.ts +201 -0
  21. package/dist/types/resources/admin/types.d.ts +222 -0
  22. package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
  23. package/dist/types/resources/admin.d.ts +1841 -0
  24. package/dist/types/resources/auth/index.d.ts +39 -0
  25. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  26. package/dist/types/resources/auth/sessions.d.ts +844 -0
  27. package/dist/types/resources/chests.d.ts +54 -3
  28. package/dist/types/resources/content/analysis.d.ts +62 -0
  29. package/dist/types/resources/content/blogs.d.ts +471 -0
  30. package/dist/types/resources/content/feedbacks.d.ts +263 -0
  31. package/dist/types/resources/content/index.d.ts +52 -0
  32. package/dist/types/resources/content/intel/articles.d.ts +230 -0
  33. package/dist/types/resources/content/intel/config.d.ts +135 -0
  34. package/dist/types/resources/content/intel/index.d.ts +53 -0
  35. package/dist/types/resources/content/intel/items.d.ts +91 -0
  36. package/dist/types/resources/content/intel/reports.d.ts +108 -0
  37. package/dist/types/resources/content/intel/scripts.d.ts +152 -0
  38. package/dist/types/resources/content/intel/sources.d.ts +236 -0
  39. package/dist/types/resources/content/intel/stats.d.ts +105 -0
  40. package/dist/types/resources/content/intel/types.d.ts +86 -0
  41. package/dist/types/resources/content/jokes.d.ts +93 -0
  42. package/dist/types/resources/content/notifications.d.ts +147 -0
  43. package/dist/types/resources/content/serviceUsages.d.ts +73 -0
  44. package/dist/types/resources/content/servicesStatus.d.ts +194 -0
  45. package/dist/types/resources/content/siteConfig.d.ts +43 -0
  46. package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
  47. package/dist/types/resources/content.d.ts +2775 -0
  48. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  49. package/dist/types/resources/forms.d.ts +176 -35
  50. package/dist/types/resources/index.d.ts +19 -8
  51. package/dist/types/resources/ipLookup.d.ts +20 -4
  52. package/dist/types/resources/jobs.d.ts +69 -25
  53. package/dist/types/resources/library/annotations.d.ts +162 -0
  54. package/dist/types/resources/library/books.d.ts +534 -0
  55. package/dist/types/resources/library/chat.d.ts +259 -0
  56. package/dist/types/resources/library/index.d.ts +39 -0
  57. package/dist/types/resources/library/shelves.d.ts +237 -0
  58. package/dist/types/resources/library/types.d.ts +120 -0
  59. package/dist/types/resources/library.d.ts +1433 -0
  60. package/dist/types/resources/linkTrees.d.ts +142 -30
  61. package/dist/types/resources/media.d.ts +351 -0
  62. package/dist/types/resources/movies/addons.d.ts +388 -0
  63. package/dist/types/resources/movies/collections.d.ts +352 -0
  64. package/dist/types/resources/movies/index.d.ts +29 -0
  65. package/dist/types/resources/movies/types.d.ts +50 -0
  66. package/dist/types/resources/movies/watchProgress.d.ts +277 -0
  67. package/dist/types/resources/movies.d.ts +1160 -0
  68. package/dist/types/resources/music/artists.d.ts +1068 -0
  69. package/dist/types/resources/music/imports.d.ts +941 -0
  70. package/dist/types/resources/music/index.d.ts +61 -0
  71. package/dist/types/resources/music/playlists.d.ts +1027 -0
  72. package/dist/types/resources/music/social.d.ts +1132 -0
  73. package/dist/types/resources/music/songs.d.ts +1167 -0
  74. package/dist/types/resources/notepads.d.ts +4 -1
  75. package/dist/types/resources/quotas.d.ts +7 -1
  76. package/dist/types/resources/realtime.d.ts +855 -0
  77. package/dist/types/resources/shortLinks.d.ts +49 -5
  78. package/dist/types/resources/social/groupChats.d.ts +458 -0
  79. package/dist/types/resources/social/index.d.ts +19 -0
  80. package/dist/types/resources/social/messages.d.ts +424 -0
  81. package/dist/types/resources/social/relationships.d.ts +322 -0
  82. package/dist/types/resources/social/types.d.ts +37 -0
  83. package/dist/types/resources/social.d.ts +1348 -0
  84. package/dist/types/resources/storage/upload.d.ts +158 -11
  85. package/dist/types/resources/storage.d.ts +111 -27
  86. package/dist/types/resources/tickets.d.ts +101 -13
  87. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  88. package/dist/types/resources/tools/captions.d.ts +448 -21
  89. package/dist/types/resources/tools/downloader.d.ts +21 -0
  90. package/dist/types/resources/tools/index.d.ts +57 -15
  91. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  92. package/dist/types/resources/tools/transcription.d.ts +35 -13
  93. package/dist/types/resources/tools/upscale.d.ts +23 -3
  94. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  95. package/dist/types/types.d.ts +249 -17
  96. package/package.json +2 -1
@@ -0,0 +1,1068 @@
1
+ /**
2
+ * The `music.artists` namespace: the artist roster, the Artists page header,
3
+ * artist artwork, the Spotify-backed "import a whole artist" flow, and the
4
+ * daily release watch that follows an artist for what it puts out NEXT.
5
+ *
6
+ * ## An artist row belongs to ONE user
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.
13
+ *
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]`.
17
+ *
18
+ * ## Almost everything here shares ONE 60/min bucket
19
+ *
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.
25
+ *
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.
36
+ *
37
+ * ## An OAuth access token cannot reach any of this
38
+ *
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.
45
+ */
46
+ import { ApiClient, Resource } from "../../http";
47
+ import type { ListParams } from "../../listing";
48
+ import type { FileInput, NativeFile, Paginated, RequestOptions, Timestamp } from "../../types";
49
+ /**
50
+ * Primary key of an artist. An INTEGER, unlike users, sessions and storage
51
+ * nodes, which are strings. `user_id` on the same record is a string.
52
+ */
53
+ export type ArtistId = number;
54
+ /** Primary key of an artist import. Also an integer. */
55
+ export type ArtistImportId = number;
56
+ /**
57
+ * Page size the web roster and the mobile roster both use (FR-37: infinite
58
+ * scroll, 60 a page). Nothing on the server requires it; it is here so the
59
+ * three clients page identically and their caches line up.
60
+ */
61
+ export declare const ARTIST_ROSTER_PAGE_SIZE = 60;
62
+ /** Largest artist image or banner the server will accept, in bytes. */
63
+ export declare const ARTIST_IMAGE_MAX_BYTES: number;
64
+ /**
65
+ * Content types `Artists::ImageAttacher` maps directly to a stored extension.
66
+ *
67
+ * A file whose type is not in this list is NOT necessarily refused: the
68
+ * attacher falls back to the extension of the filename and only gives up when
69
+ * that is empty too. So `application/octet-stream` + `cover.png` is accepted
70
+ * and `application/octet-stream` + `cover` is a 400.
71
+ */
72
+ export declare const ARTIST_IMAGE_CONTENT_TYPES: readonly ["image/jpeg", "image/png", "image/webp", "image/gif"];
73
+ /**
74
+ * An artist, as `GET /artists` renders it (the blueprint's default view plus
75
+ * two computed fields).
76
+ *
77
+ * ## Every image field, and which of them can be trusted
78
+ *
79
+ * There are four independent sources of a picture on this record and they are
80
+ * refreshed by three different subsystems, so "this field is null" means
81
+ * something different for each:
82
+ *
83
+ * - `image_media_id` / `banner_media_id` are UPLOADS. They are the only fields
84
+ * a user controls, they change only through {@link MusicArtistsNamespace.uploadImage}
85
+ * and {@link MusicArtistsNamespace.uploadBanner}, and nothing overwrites them.
86
+ * - `picture*` is the cached Deezer set. NOTHING in this namespace refreshes
87
+ * it - it is written only by `GET /songs/artist_pictures`, which lives in the
88
+ * songs namespace. See {@link Artist.pictures_fetched_at} for why a fresh
89
+ * timestamp there is not a promise.
90
+ * - `external_image_url` comes from Last.fm through `ArtistResolver`, and is
91
+ * usually null on purpose: Last.fm retired artist images and now answers with
92
+ * one grey-star placeholder for everybody, which the resolver filters out
93
+ * rather than store.
94
+ * - `gallery_image_urls` (extended view only) is Wikipedia/Wikimedia.
95
+ *
96
+ * The resolution chain all three clients implement, in order:
97
+ * `compressed_image_media_id` -> `image_media_id` -> the size-appropriate
98
+ * `picture_*` (medium for an avatar, `picture_xl` for a hero) -> `picture` ->
99
+ * `gallery_image_urls[0]` -> `fallback_artwork_media_id` -> `external_image_url`
100
+ * -> initials.
101
+ *
102
+ * ## `*_media_id` and `*_fs_node_id` are the same value, twice
103
+ *
104
+ * `ApplicationBlueprint.media_id_fields` emits both keys with an identical
105
+ * value for every attachment, because the old web frontend still reads the
106
+ * legacy `_fs_node_id` name. Read `_media_id`; the twin is scheduled to go.
107
+ * Both docs in `oms-music/docs` document only the legacy name.
108
+ */
109
+ export interface Artist {
110
+ /** Integer, and unique only within one user's library. */
111
+ readonly id: ArtistId;
112
+ readonly created_at: Timestamp;
113
+ readonly updated_at: Timestamp;
114
+ /** Display name, as the user typed or as the import found it. */
115
+ readonly name: string;
116
+ /**
117
+ * Identity key: NFKC-folded, lowercased, `[-_./]` collapsed to spaces. Other
118
+ * punctuation survives on purpose, so `P!nk` and `Pink` stay distinct.
119
+ * Recomputed whenever `name` changes.
120
+ */
121
+ readonly canonical_name: string;
122
+ /**
123
+ * URL slug, derived from the name at creation and then FROZEN: renaming an
124
+ * artist does not move its slug, so bookmarks survive. Unique per user.
125
+ */
126
+ readonly slug: string;
127
+ /** Owner. A STRING id, unlike this record's own integer `id`. */
128
+ readonly user_id: string;
129
+ /** Uploaded square avatar, or null. */
130
+ readonly image_media_id: string | null;
131
+ /** Legacy twin of {@link Artist.image_media_id}. Same value. */
132
+ readonly image_fs_node_id: string | null;
133
+ /**
134
+ * Legacy compressed variant. Only the `artists:merge` rake helpers ever
135
+ * adopt one; nothing generates new ones, and an upload purges the stale
136
+ * companion, so on a modern row this is null.
137
+ */
138
+ readonly compressed_image_media_id: string | null;
139
+ /** Legacy twin of {@link Artist.compressed_image_media_id}. */
140
+ readonly compressed_image_fs_node_id: string | null;
141
+ /** Uploaded hero banner, or null. */
142
+ readonly banner_media_id: string | null;
143
+ /** Legacy twin of {@link Artist.banner_media_id}. */
144
+ readonly banner_fs_node_id: string | null;
145
+ /** See {@link Artist.compressed_image_media_id}. */
146
+ readonly compressed_banner_media_id: string | null;
147
+ /** Legacy twin of {@link Artist.compressed_banner_media_id}. */
148
+ readonly compressed_banner_fs_node_id: string | null;
149
+ /** MusicBrainz id, when the lookup found one. */
150
+ readonly mbid: string | null;
151
+ readonly lastfm_listeners: number | null;
152
+ readonly lastfm_playcount: number | null;
153
+ /**
154
+ * Last.fm's artist image. Null for nearly everyone - the placeholder every
155
+ * artist now gets is filtered out rather than stored.
156
+ */
157
+ readonly external_image_url: string | null;
158
+ /** Cached Deezer picture set. All five move together, or all stay null. */
159
+ readonly picture: string | null;
160
+ readonly picture_small: string | null;
161
+ readonly picture_medium: string | null;
162
+ readonly picture_big: string | null;
163
+ readonly picture_xl: string | null;
164
+ /**
165
+ * When the Deezer picture set was last WRITTEN - which is not the same as
166
+ * when it was last checked.
167
+ *
168
+ * `ArtistPicturesFetcher` stamps this only on a successful answer (a hit, or
169
+ * a genuine "not on Deezer" miss). A failure - and a quota refusal is a
170
+ * failure that arrives as `HTTP 200` with an `{"error": ...}` body - stamps
171
+ * NOTHING and only sets a 30-minute in-cache guard, precisely so an
172
+ * over-quota sweep cannot mark a whole library as "fetched, no picture" for
173
+ * three days. That is the shape of the bug this behaviour exists to prevent,
174
+ * so do not read a stale timestamp as "Deezer has nothing for this artist".
175
+ */
176
+ readonly pictures_fetched_at: Timestamp | null;
177
+ /**
178
+ * When Last.fm's biography was last fetched, successfully OR NOT.
179
+ *
180
+ * `ArtistResolver#populate!` rescues every transport failure and then stamps
181
+ * all three `*_fetched_at` columns anyway, so the freshness gate stops
182
+ * hammering an upstream that is down. The row therefore looks populated
183
+ * while `bio_html` is still null. A null `bio_html` next to a recent
184
+ * `bio_fetched_at` means "we tried and got nothing", not "never tried".
185
+ */
186
+ readonly bio_fetched_at: Timestamp | null;
187
+ /** Same semantics as {@link Artist.bio_fetched_at}, for the similar list. */
188
+ readonly similar_fetched_at: Timestamp | null;
189
+ /**
190
+ * Number of `song_artists` join rows, counting every credit (primary,
191
+ * featured and with), not distinct songs.
192
+ *
193
+ * Computed per response, never stored. On `GET /artists` it comes from one
194
+ * grouped `COUNT` for the whole page; everywhere else the blueprint runs its
195
+ * own `COUNT(*)`. Always present.
196
+ */
197
+ readonly songs_count: number;
198
+ /**
199
+ * Artwork of one of this artist's own lead songs, for a card that has no
200
+ * picture at all. A media id, resolvable through `oms.media` (the canonical
201
+ * `/media/:id/data`); `/fs_nodes/:id/data` is the alias kept for the web
202
+ * frontend and reaches the same bytes.
203
+ *
204
+ * NON-NULL ONLY ON `GET /artists` AND `GET /artists/:id`, where the
205
+ * controller precomputes the whole page with a single `DISTINCT ON` query.
206
+ * `PATCH` and both uploads render the blueprint standalone and answer null
207
+ * here even for an artist that has one - so a card redrawn from an upload
208
+ * response loses its fallback. Keep the value you already had.
209
+ */
210
+ readonly fallback_artwork_media_id: string | null;
211
+ /** Legacy twin of {@link Artist.fallback_artwork_media_id}. */
212
+ readonly fallback_artwork_fs_node_id: string | null;
213
+ }
214
+ /** One entry of Last.fm's similar-artists list. */
215
+ export interface ArtistSimilarEntry {
216
+ readonly name: string;
217
+ /**
218
+ * Last.fm's similarity score, `0..1`. A NUMBER: the resolver runs `to_f`
219
+ * before storing it. The web frontend types the whole `similar` field as an
220
+ * object with an `artists` array, which is the DATABASE column's shape and
221
+ * not the payload's - the blueprint flattens it to this array.
222
+ */
223
+ readonly match: number | null;
224
+ readonly mbid: string | null;
225
+ }
226
+ /**
227
+ * The `:extended` view: everything in {@link Artist} plus three fields.
228
+ *
229
+ * Blueprinter views INHERIT the base fields, so this is a superset and never a
230
+ * subset. Returned by `GET /artists/:id`, `PATCH /artists/:id` and both upload
231
+ * endpoints. `oms-music/docs/api-music.md` describes `:compact` and `:card` as
232
+ * narrow subsets; they are not, for the same inheritance reason, and
233
+ * `docs/API.md` says so correctly.
234
+ */
235
+ export interface ArtistExtended extends Artist {
236
+ /**
237
+ * Last.fm biography summary. Contains HTML (and a trailing "Read more"
238
+ * anchor); sanitised on the `/artist_metadata` shim but NOT here, so treat it
239
+ * as untrusted markup and sanitise before injecting it.
240
+ */
241
+ readonly bio_html: string | null;
242
+ /**
243
+ * Wikipedia/Wikimedia photos of the artist.
244
+ *
245
+ * Writable through {@link MusicArtistsNamespace.update}, and OVERWRITTEN
246
+ * without warning by the next metadata refresh: `ArtistResolver#populate!`
247
+ * always assigns `gallery_image_urls` from `Artists::GalleryFetcher`, so a
248
+ * hand-curated list survives only until the artist's gallery TTL (30 days,
249
+ * jittered by up to 7) expires and someone opens the artist page. There is
250
+ * no "pinned" flag. If a client offers gallery editing, it has to be
251
+ * prepared to re-apply it.
252
+ */
253
+ readonly gallery_image_urls: string[];
254
+ /** Flattened from the stored `similar_json`. Empty array, never null. */
255
+ readonly similar: ArtistSimilarEntry[];
256
+ }
257
+ /**
258
+ * Filters for {@link MusicArtistsNamespace.list}.
259
+ *
260
+ * The server's allowlist for this index is exactly `id`, `name`, `slug`,
261
+ * `canonical_name`, `created_at` and `updated_at`. Any other key inside
262
+ * `search` / `exact_search` is a **400**, not a silently ignored filter -
263
+ * `CrudActions#reject_unknown_filter_keys!` fails closed on purpose, because
264
+ * the alternative (dropping the key) answers with the UNFILTERED table.
265
+ */
266
+ export declare const ARTIST_FILTER_COLUMNS: readonly ["id", "name", "slug", "canonical_name", "created_at"];
267
+ export interface ListArtistsParams extends ListParams<(typeof ARTIST_FILTER_COLUMNS)[number]> {
268
+ /**
269
+ * Substring match on the display name - `search[name]`. This is the roster
270
+ * search box.
271
+ *
272
+ * `QuerySearcher#string_search` slugifies BOTH sides before comparing:
273
+ * lowercase, accents transliterated, every run of anything outside
274
+ * `[a-z0-9-]` replaced by a hyphen, then wrapped in `%`. So the query is
275
+ * really `slug(name) LIKE %slug(input)%`, and three consequences follow:
276
+ * `"Beyoncé"` finds `"Beyonce"` and the reverse; `"chico buarque"` and
277
+ * `"chico-buarque"` are the SAME query; and punctuation is erased on both
278
+ * sides, so `"P!nk"` matches `"P nk"` too. An accent this table's
279
+ * `TRANSLATE` map does not list survives as a hyphen, which still matches.
280
+ */
281
+ readonly name?: string;
282
+ /** Exact slug. Cheaper and less surprising than a `name` search. */
283
+ readonly slug?: string;
284
+ /**
285
+ * Exact canonical name. Fold it the way the server does before sending it
286
+ * (lowercase, `[-_./]` to spaces) or it will not match.
287
+ */
288
+ readonly canonicalName?: string;
289
+ /** 1-based page number. */
290
+ readonly page?: number;
291
+ /** Rows per page. Clamped to 500 by the server and by the SDK. */
292
+ readonly pageSize?: number;
293
+ /**
294
+ * `"column:asc"` / `"column:desc"`.
295
+ *
296
+ * PASS ONE. The index has no default order, so paging without it is paging
297
+ * an unordered relation: Postgres may hand back the same row on two pages
298
+ * and never hand back another. `name:asc` and `created_at:desc` are what the
299
+ * clients use.
300
+ *
301
+ * `QueryModifier#apply_ordering` checks the column against
302
+ * `model.column_names` and SILENTLY IGNORES anything else, so
303
+ * `songs_count:desc` is not an error and is not an ordering either - that
304
+ * field is computed per response and cannot be sorted on. Sort client-side,
305
+ * or read {@link MusicArtistsNamespace.overview}, which ranks for you.
306
+ */
307
+ readonly order?: string;
308
+ }
309
+ /** Body accepted by {@link MusicArtistsNamespace.update}. */
310
+ export interface UpdateArtistInput {
311
+ /**
312
+ * New display name. `canonical_name` is recomputed from it; `slug` is NOT,
313
+ * so the artist keeps the URL it was created with.
314
+ */
315
+ readonly name?: string;
316
+ /**
317
+ * Replaces the gallery wholesale - there is no merge. `[]` clears it.
318
+ *
319
+ * Every entry must start with `http://` or `https://` (blank entries are
320
+ * stripped first); one that does not fails the whole request with
321
+ * `400 "Gallery URLs must start with http:// or https://"`. And see
322
+ * {@link ArtistExtended.gallery_image_urls}: what you write here is
323
+ * temporary.
324
+ */
325
+ readonly gallery_image_urls?: string[];
326
+ }
327
+ /** Aggregate counters at the top of the Artists page. */
328
+ export interface ArtistOverviewStats {
329
+ /** Artists in the library. */
330
+ readonly artists: number;
331
+ /** Songs in the library. */
332
+ readonly songs: number;
333
+ /** Artists first seen in the last 30 days. */
334
+ readonly new_artists: number;
335
+ /**
336
+ * Total listening time, summed over the DURATION of the songs played rather
337
+ * than counting plays, so a 12-second interlude does not weigh the same as
338
+ * an eight-minute track. Covers the same window the shelves cover - see
339
+ * {@link ArtistOverview.heavy_rotation_window}.
340
+ */
341
+ readonly seconds_played: number;
342
+ }
343
+ /** The hero of the Artists page. */
344
+ export interface ArtistSpotlight {
345
+ readonly artist: Artist;
346
+ readonly songs_count: number;
347
+ /** Distinct non-empty `album` values across this artist's songs. */
348
+ readonly albums_count: number;
349
+ /** Plays in the active window. `0` when the spotlight came from the fallback. */
350
+ readonly play_count: number;
351
+ }
352
+ /** One row of the heavy-rotation shelf. */
353
+ export interface ArtistHeavyRotationEntry {
354
+ readonly artist: Artist;
355
+ readonly play_count: number;
356
+ }
357
+ /** Library artists similar to the spotlight, seeded by the spotlight itself. */
358
+ export interface ArtistSimilarShelf {
359
+ readonly seed: Artist;
360
+ /** Never empty - the whole shelf is null instead. Max 12. */
361
+ readonly artists: Artist[];
362
+ }
363
+ /** One row of the "you forgot about these" shelf. */
364
+ export interface ArtistNeglectedEntry {
365
+ readonly artist: Artist;
366
+ /** Primary credits the user owns. The shelf is ordered by it, descending. */
367
+ readonly songs_count: number;
368
+ }
369
+ /**
370
+ * The editorial header of the Artists page: five aggregate queries answered as
371
+ * one document.
372
+ *
373
+ * The artists inside are rendered with the blueprint's `:card` view, which
374
+ * inherits the base fields, so each one is a whole {@link Artist} - including
375
+ * `fallback_artwork_media_id`, which the controller precomputes here.
376
+ */
377
+ export interface ArtistOverview {
378
+ readonly stats: ArtistOverviewStats;
379
+ /**
380
+ * Which window `heavy_rotation`, `spotlight.play_count` and
381
+ * `stats.seconds_played` actually cover.
382
+ *
383
+ * The query prefers the last 30 days and falls back to ALL TIME when nothing
384
+ * was played in them, rather than answering with an empty page. Label the
385
+ * shelf from this field; a client that hardcodes "this month" will be lying
386
+ * to anyone who took a month off.
387
+ */
388
+ readonly heavy_rotation_window: "30d" | "all";
389
+ /**
390
+ * Null only for an empty library. With no play history it falls back to
391
+ * whoever the library holds the most songs of.
392
+ */
393
+ readonly spotlight: ArtistSpotlight | null;
394
+ /** Up to 12, most played first. Empty for a library that was never played. */
395
+ readonly heavy_rotation: ArtistHeavyRotationEntry[];
396
+ /**
397
+ * Null when there is no spotlight, when Last.fm never returned similars for
398
+ * it, or when none of them are in the library. Recommending an artist the
399
+ * user cannot play is noise, so the shelf disappears instead.
400
+ */
401
+ readonly similar: ArtistSimilarShelf | null;
402
+ /** In the library, nothing played in 90 days. Up to 12. */
403
+ readonly neglected: ArtistNeglectedEntry[];
404
+ }
405
+ /** `ArtistImport::STATES`. */
406
+ export declare const ARTIST_IMPORT_STATES: readonly ["queued", "running", "complete", "failed"];
407
+ /** State of a bulk artist import. */
408
+ export type ArtistImportState = (typeof ARTIST_IMPORT_STATES)[number];
409
+ /**
410
+ * Cadence the three clients poll an in-flight import at, from
411
+ * `oms-music/docs/API.md` section 14. There is no cable channel for imports.
412
+ */
413
+ export declare const ARTIST_IMPORT_POLL_INTERVAL_MS = 1500;
414
+ /**
415
+ * True once an import will never change again.
416
+ *
417
+ * Test against this rather than against a string you typed from memory: the
418
+ * terminal success state is `"complete"`, not `"completed"`, and a loop that
419
+ * waits for the wrong spelling never ends.
420
+ */
421
+ export declare function isArtistImportTerminal(state: ArtistImportState | string): boolean;
422
+ /**
423
+ * One bulk import of an artist's Spotify catalogue into the library.
424
+ *
425
+ * The counters exist because the pre-refactor flow swallowed Spotify errors
426
+ * inside the job and left the user watching a spinner. Read them together:
427
+ * `processed_albums` of `total_albums` is progress, and
428
+ * `queued + skipped + failed` is what became of the tracks.
429
+ */
430
+ export interface ArtistImport {
431
+ readonly id: ArtistImportId;
432
+ readonly created_at: Timestamp;
433
+ readonly updated_at: Timestamp;
434
+ /** Owner. A string id. */
435
+ readonly user_id: string;
436
+ readonly spotify_artist_id: string;
437
+ /** Echoed back from the request; null when the caller sent it blank. */
438
+ readonly spotify_artist_name: string | null;
439
+ /** Exactly the ids the caller asked for, in the order they were sent. */
440
+ readonly album_ids: string[];
441
+ readonly state: ArtistImportState;
442
+ readonly total_albums: number | null;
443
+ /** Null until the job has expanded the albums into tracks. */
444
+ readonly total_tracks: number | null;
445
+ readonly processed_albums: number;
446
+ /** Tracks handed to the song-import pipeline. */
447
+ readonly queued_count: number;
448
+ /** Tracks the library already had. */
449
+ readonly skipped_count: number;
450
+ readonly failed_count: number;
451
+ /** Human-readable progress line, e.g. `"Waiting in queue…"`. */
452
+ readonly last_message: string | null;
453
+ /** Set only in the `failed` state. */
454
+ readonly error_message: string | null;
455
+ readonly started_at: Timestamp | null;
456
+ readonly finished_at: Timestamp | null;
457
+ }
458
+ /** An artist the caller ALREADY has, offered by the import picker. */
459
+ export interface ArtistImportRosterMatch {
460
+ readonly kind: "roster";
461
+ /** The local {@link ArtistId}. An integer. */
462
+ readonly id: ArtistId;
463
+ readonly name: string;
464
+ readonly slug: string;
465
+ /**
466
+ * A real URL (`picture_medium`, then `external_image_url`), or null. An
467
+ * earlier version put a raw storage node id in this field, which no client
468
+ * could load as an image.
469
+ */
470
+ readonly image_url: string | null;
471
+ }
472
+ /** A Spotify search hit offered by the import picker. */
473
+ export interface ArtistImportSpotifyMatch {
474
+ readonly kind: "spotify";
475
+ /** SPOTIFY's id, a string. This is what the other two methods take. */
476
+ readonly id: string;
477
+ readonly name: string;
478
+ readonly followers: number | null;
479
+ readonly genres: string[];
480
+ /** Largest image Spotify offered, or null. */
481
+ readonly image_url: string | null;
482
+ readonly external_url: string | null;
483
+ }
484
+ /**
485
+ * The import picker's two lists: what you already have, and what Spotify
486
+ * found.
487
+ */
488
+ export interface ArtistImportSearchResult {
489
+ /** Up to 8 local matches, ordered by name. Available without Spotify. */
490
+ readonly roster: ArtistImportRosterMatch[];
491
+ /**
492
+ * Up to 10 Spotify hits.
493
+ *
494
+ * EMPTY IS AMBIGUOUS and deliberately so: it means no Spotify identity is
495
+ * linked, or Spotify genuinely matched nothing, or Spotify answered with an
496
+ * upstream error that the server logged and swallowed so the roster half
497
+ * would still render. Only a token-refresh failure is surfaced, as a 400.
498
+ */
499
+ readonly spotify: ArtistImportSpotifyMatch[];
500
+ }
501
+ /** One album in an artist's Spotify catalogue. */
502
+ export interface ArtistImportAlbum {
503
+ /** Spotify album id - this is what goes in `albumIds`. */
504
+ readonly id: string;
505
+ readonly name: string;
506
+ /** `"album" | "single" | "compilation"`, as Spotify spells it. */
507
+ readonly album_type: string | null;
508
+ /** `"album" | "single" | "compilation" | "appears_on"`. */
509
+ readonly album_group: string | null;
510
+ /** Spotify's partial date: `"2019"`, `"2019-05"` or `"2019-05-31"`. */
511
+ readonly release_date: string | null;
512
+ readonly total_tracks: number | null;
513
+ readonly image_url: string | null;
514
+ readonly external_url: string | null;
515
+ }
516
+ /** Arguments for {@link MusicArtistImportsNamespace.create}. */
517
+ export interface CreateArtistImportInput {
518
+ /** Spotify's artist id, NOT a local {@link ArtistId}. */
519
+ readonly spotifyArtistId: string;
520
+ /** Stored for display. Sent blank, it lands as null. */
521
+ readonly spotifyArtistName?: string;
522
+ /** Spotify album ids. At least one, or the request is a 400. */
523
+ readonly albumIds: string[];
524
+ }
525
+ /** Arguments for {@link MusicArtistImportsNamespace.list}. */
526
+ export interface ListArtistImportsParams {
527
+ /**
528
+ * How many of the newest imports to return. Clamped server-side to `1..50`;
529
+ * the default is 20. Out-of-range values are clamped, never rejected.
530
+ */
531
+ readonly limit?: number;
532
+ }
533
+ /**
534
+ * Bulk artist import, reachable as `oms.music.artists.imports`.
535
+ *
536
+ * ## Every route here needs a LINKED SPOTIFY IDENTITY, not a flag
537
+ *
538
+ * The gate is `Current.user.identities.find_by(provider: "spotify")`. Without
539
+ * one the answer is `400 "Connect Spotify first."`, and when the stored refresh
540
+ * token no longer works it is `400 "Spotify connection needs to be relinked."`
541
+ * Both are plain JSON strings, both arrive as an {@link OmsApiError} with
542
+ * status 400, and the only way to tell them apart is the message - so match on
543
+ * it if the UI needs to distinguish "connect" from "reconnect".
544
+ *
545
+ * {@link search} is the exception: its roster half works with no Spotify at all.
546
+ *
547
+ * ## These calls are SLOW and they are on the general ceiling
548
+ *
549
+ * `/artist_imports*` does not match the 60/min proxy bucket, so it sits under
550
+ * the general authenticated ceiling (600/min). What bounds it in practice is
551
+ * Spotify: {@link search} and {@link albums} both call out synchronously on the
552
+ * request thread, and {@link albums} pages through an entire discography. The
553
+ * app allows 60 seconds for each, and so does this namespace.
554
+ */
555
+ export declare class MusicArtistImportsNamespace extends Resource {
556
+ /**
557
+ * `GET /artist_imports/search?q=` - the import picker.
558
+ *
559
+ * A blank or whitespace-only query short-circuits to
560
+ * `{ roster: [], spotify: [] }` with a `200` and no upstream call, so
561
+ * debouncing an empty box is free. There is no minimum length beyond that
562
+ * and no pagination.
563
+ *
564
+ * Retried like any GET. Sixty seconds by default, because the Spotify leg is
565
+ * synchronous.
566
+ *
567
+ * @throws {OmsApiError} 400 `"Spotify connection needs to be relinked."`
568
+ * when the stored refresh token is dead. Note that a plain Spotify
569
+ * outage is NOT an error here - it comes back as an empty `spotify` array.
570
+ */
571
+ search(query: string, options?: RequestOptions): Promise<ArtistImportSearchResult>;
572
+ /**
573
+ * `GET /artist_imports/albums?spotify_artist_id=` - the artist's catalogue,
574
+ * deduplicated by lowercased album name, first occurrence wins.
575
+ *
576
+ * That dedup is why the result can be shorter than Spotify's own catalogue
577
+ * and why a deluxe edition sometimes disappears behind the standard one: the
578
+ * comparison is the NAME, not the id.
579
+ *
580
+ * The server wraps the array in `{ items: [...] }`; this returns the array.
581
+ *
582
+ * `spotifyArtistId` is validated here rather than sent empty, because the
583
+ * controller uses `params.require` and a missing key raises
584
+ * `ActionController::ParameterMissing`. That escapes the API's own error
585
+ * convention - it is a framework 400 with a framework body rather than a
586
+ * bare JSON string - and it trips `ErrorReporting`, which pages the owner on
587
+ * Discord for what is really a client bug.
588
+ *
589
+ * @throws {OmsError} `invalid_request` when `spotifyArtistId` is blank.
590
+ * @throws {OmsApiError} 400 `"Connect Spotify first."` with no linked
591
+ * identity, `"Spotify connection needs to be relinked."` on a dead refresh
592
+ * token, or `"Spotify upstream error: ..."` (truncated to 200 characters)
593
+ * when Spotify itself failed. Unlike {@link search}, an outage IS surfaced
594
+ * here.
595
+ */
596
+ albums(spotifyArtistId: string, options?: RequestOptions): Promise<ArtistImportAlbum[]>;
597
+ /**
598
+ * `POST /artist_imports` - queues every track of the chosen albums.
599
+ *
600
+ * Answers `201` immediately with a `queued` record; the work happens in
601
+ * `ArtistImportJob`. Watch it with {@link list} at
602
+ * {@link ARTIST_IMPORT_POLL_INTERVAL_MS} - THERE IS NO `GET
603
+ * /artist_imports/:id`, so polling means re-reading the recent list and
604
+ * finding your id in it.
605
+ *
606
+ * Not retried, and this one matters more than most: a replay does not
607
+ * deduplicate, it creates a second `ArtistImport` row and runs the whole
608
+ * catalogue through the pipeline again. Pass `retry: {}` only if you are
609
+ * prepared to explain the duplicate.
610
+ *
611
+ * @throws {OmsError} `invalid_request` when `spotifyArtistId` is blank or
612
+ * `albumIds` is empty. The first would raise `ParameterMissing` on the
613
+ * server (see {@link albums}); the second is a clean
614
+ * `400 "album_ids required"`, checked here only so the round trip is saved.
615
+ * @throws {OmsApiError} 400 for the two Spotify-identity messages.
616
+ */
617
+ create(input: CreateArtistImportInput, options?: RequestOptions): Promise<ArtistImport>;
618
+ /**
619
+ * `GET /artist_imports?limit=` - the caller's own imports, newest first.
620
+ *
621
+ * Not the List DSL: no `search`, no `modifiers`, no paging beyond `limit`,
622
+ * and the payload is wrapped in `{ items: [...] }` (unwrapped here). Fifty
623
+ * is the ceiling and it is clamped, not rejected.
624
+ *
625
+ * This is also the poll: filter for a `state` that
626
+ * {@link isArtistImportTerminal} rejects to know whether anything is still
627
+ * running. Nothing pushes import progress over the cable.
628
+ */
629
+ list(params?: ListArtistImportsParams, options?: RequestOptions): Promise<ArtistImport[]>;
630
+ }
631
+ /**
632
+ * Primary key of an artist sync. An integer, like {@link ArtistId} and
633
+ * {@link ArtistImportId}, and interchangeable with NEITHER: a sync is keyed on
634
+ * a SPOTIFY artist id and never references a local `Artist` row at all. Three
635
+ * integer id spaces meet in this file and only the field name tells them apart.
636
+ */
637
+ export type ArtistSyncId = number;
638
+ /**
639
+ * A followed artist, as `/artist_syncs` renders it.
640
+ *
641
+ * ## This payload is hand-built, so the base fields you expect are MISSING
642
+ *
643
+ * There is no `ArtistSyncBlueprint`. `ArtistSyncsController#serialize` writes
644
+ * the hash literally, which is why this is the one music record with no
645
+ * `created_at` and no `updated_at` - the convention that every payload carries
646
+ * them holds everywhere a Blueprinter view is involved and stops here. Do not
647
+ * sort a list of these by `created_at` client-side; the server already returns
648
+ * them newest-first and that ordering is the only one available.
649
+ *
650
+ * `known_album_ids` is likewise not exposed. The row stores the full array of
651
+ * Spotify album ids (a `jsonb` column) and only its SIZE crosses the wire, so
652
+ * the SDK cannot tell you WHICH albums are already known - only how many.
653
+ */
654
+ export interface ArtistSync {
655
+ readonly id: ArtistSyncId;
656
+ /** Spotify's artist id. The join key, and the natural key of the row. */
657
+ readonly spotify_artist_id: string;
658
+ /**
659
+ * Display name captured when the sync was created or last re-POSTed.
660
+ *
661
+ * `null` when the very first {@link MusicArtistSyncsNamespace.create} omitted
662
+ * it. It is never refreshed from Spotify, and it cannot be CLEARED: a create
663
+ * with a blank name keeps whatever the row already had (see
664
+ * {@link CreateArtistSyncInput.spotifyArtistName}).
665
+ */
666
+ readonly artist_name: string | null;
667
+ /**
668
+ * Whether `ArtistDailySyncDispatcherJob` will pick this row up (its scope is
669
+ * `where(enabled: true)`).
670
+ *
671
+ * Always `true` on anything this SDK can produce: `create` sets it, and there
672
+ * is no update route to turn it off. A `false` here can only have been
673
+ * written by the console, and the only way a client can stop a sync is
674
+ * {@link MusicArtistSyncsNamespace.delete}. Render it, do not offer a toggle.
675
+ */
676
+ readonly enabled: boolean;
677
+ /**
678
+ * When the daily check last ran, ISO-8601, or `null` in the vanishingly
679
+ * short window before `create` saves the row.
680
+ *
681
+ * A TOUCH, not a success marker. `ArtistSyncCheckJob` writes it on the happy
682
+ * path AND in both of its rescue arms (dead refresh token, Spotify upstream
683
+ * error), so a fresh timestamp proves the job ran, never that Spotify
684
+ * answered. There is no field that records the last failure - it is a
685
+ * `Rails.logger.warn` and nothing else - so a UI cannot honestly say "last
686
+ * checked, all good".
687
+ */
688
+ readonly last_checked_at: Timestamp | null;
689
+ /**
690
+ * How many Spotify album ids the snapshot holds. `Array(known_album_ids).size`.
691
+ *
692
+ * This is the baseline the daily diff runs against, not a count of songs
693
+ * imported. It GROWS and never shrinks, because the job stores the union
694
+ * (`known | current_ids`) precisely so an album Spotify hides and later shows
695
+ * again cannot re-import as new.
696
+ */
697
+ readonly known_album_count: number;
698
+ }
699
+ /** Arguments for {@link MusicArtistSyncsNamespace.create}. */
700
+ export interface CreateArtistSyncInput {
701
+ /**
702
+ * Spotify's artist id - the same string
703
+ * {@link MusicArtistImportsNamespace.search} returns on its `spotify` side,
704
+ * NOT a local {@link ArtistId}.
705
+ */
706
+ readonly spotifyArtistId: string;
707
+ /**
708
+ * Display name to store. Optional, and asymmetric: omitted on a FIRST create
709
+ * it lands as `null`, omitted on a re-create it leaves the existing name
710
+ * alone. Blank and absent behave identically, so there is no way to erase a
711
+ * name once written - only to overwrite it with another.
712
+ */
713
+ readonly spotifyArtistName?: string;
714
+ }
715
+ /**
716
+ * Daily release watch for a Spotify artist, reachable as
717
+ * `oms.music.artists.syncs`.
718
+ *
719
+ * ## `syncs` versus `imports`: a subscription versus a backfill
720
+ *
721
+ * They share a Spotify artist id and nothing else, and picking the wrong one
722
+ * is the mistake this namespace exists to make hard:
723
+ *
724
+ * | | {@link MusicArtistImportsNamespace} | this |
725
+ * | --- | --- | --- |
726
+ * | what it does | imports the albums you CHOSE, now | watches for albums released LATER |
727
+ * | back catalogue | yes, that is the point | never |
728
+ * | when work happens | immediately, `ArtistImportJob` | daily, 05:00 server time |
729
+ * | you pick albums | yes, `albumIds` is required | no, there is no album argument |
730
+ * | repeating the call | duplicates the whole import | idempotent, one row per artist |
731
+ *
732
+ * "Follow" here means FROM NOW ON. {@link create} takes a snapshot of the
733
+ * artist's current catalogue and stores the album ids; the discography that
734
+ * already exists is deliberately excluded from everything the sync will ever
735
+ * do. A user who wants both has to do both - follow for the future, and run an
736
+ * import for the past.
737
+ *
738
+ * ## What the sync produces is an ArtistImport, so watch it there
739
+ *
740
+ * `ArtistDailySyncDispatcherJob` runs at 05:00, walks every enabled row and
741
+ * schedules each check at a random offset inside a 30-minute window (the same
742
+ * anti-stampede discipline as the Spotify sync). Each `ArtistSyncCheckJob`
743
+ * re-walks the artist's catalogue, diffs it against the snapshot and, when
744
+ * something is new, creates an ordinary `ArtistImport` holding ONLY the new
745
+ * album ids - `last_message` starts as `"Novo lançamento detectado pelo sync
746
+ * diário…"`, which is the marker that tells an automatic import from one a
747
+ * person asked for.
748
+ *
749
+ * So there is no progress on this namespace and nothing to poll here. Progress
750
+ * lives in {@link MusicArtistImportsNamespace.list}, mixed in with manual
751
+ * imports. Nothing pushes either over the cable.
752
+ *
753
+ * ## A LINKED SPOTIFY IDENTITY is required to write, not to read
754
+ *
755
+ * {@link create} is gated on `Current.user.identities.find_by(provider:
756
+ * "spotify")` and answers `400 "Connect Spotify first."` without one. {@link
757
+ * list} and {@link delete} are not gated, which matters after an unlink: the
758
+ * rows survive, they still list, they can still be deleted, and the daily job
759
+ * quietly skips them (`return unless identity`) without recording that it did.
760
+ *
761
+ * ## Ceilings and cost
762
+ *
763
+ * Every route here is on the GENERAL authenticated ceiling of 600/min.
764
+ * `/artist_syncs` does not match the 60/min `\A/(lyrics|artists/|...)` bucket -
765
+ * the underscore breaks the `artists/` prefix - and it is not in
766
+ * `EXPENSIVE_TOOL_PATHS` either, even though {@link create} is by far the
767
+ * slowest call in this file.
768
+ *
769
+ * ## An OAuth access token cannot reach any of this
770
+ *
771
+ * `ArtistSyncsController` declares no `oauth_scope`, and the gate denies by
772
+ * default, so a Doorkeeper token gets `403 {"error":"insufficient_scope"}`
773
+ * here as it does everywhere else in `music`. Session token or cookie only.
774
+ */
775
+ export declare class MusicArtistSyncsNamespace extends Resource {
776
+ /**
777
+ * `GET /artist_syncs` - every artist this account follows, newest first.
778
+ *
779
+ * Not the List DSL and not a paginated index: no `search`, no
780
+ * `exact_search`, no `modifiers`, no `limit`, and NO `ETag`, so this cannot
781
+ * answer `304` and every poll pays for the full body. The server orders by
782
+ * `created_at DESC` and hands back everything; a user following two hundred
783
+ * artists gets two hundred rows.
784
+ *
785
+ * The array is wrapped in `{ items: [...] }` on the wire - the same envelope
786
+ * `/artist_imports` uses and the opposite of the bare arrays the rest of the
787
+ * API returns - and is unwrapped here.
788
+ *
789
+ * Safe to retry, and scoped to the caller: `ArtistSync.viewable_by` is
790
+ * `where(user:)`, so there is no way to read anyone else's follows.
791
+ */
792
+ list(options?: RequestOptions): Promise<ArtistSync[]>;
793
+ /**
794
+ * `POST /artist_syncs` - follow an artist, and snapshot what it has today.
795
+ *
796
+ * Answers `201` with the stored row. The response is worth reading rather
797
+ * than discarding: `known_album_count` is the size of the snapshot that was
798
+ * just taken, and it is the only confirmation that the catalogue walk
799
+ * actually happened.
800
+ *
801
+ * ## Idempotent, unlike its neighbour
802
+ *
803
+ * `find_or_initialize_by(user:, spotify_artist_id:)` behind a unique index,
804
+ * so calling this twice for one artist updates a single row instead of
805
+ * creating a second - it re-enables the sync, overwrites the name if one was
806
+ * sent, and touches `last_checked_at`. It does NOT re-snapshot: the Spotify
807
+ * walk is guarded by `if sync.known_album_ids.blank?`, so a second create is
808
+ * cheap and, more importantly, cannot silently widen the baseline and swallow
809
+ * releases that arrived in between.
810
+ *
811
+ * That is why this one opts INTO retries (`retry: {}`) while
812
+ * {@link MusicArtistImportsNamespace.create} refuses them: a replayed follow
813
+ * converges on the same row, a replayed import runs a whole discography
814
+ * twice. Pass `retry: false` to opt back out.
815
+ *
816
+ * ## It is slow, because it walks the discography on the request thread
817
+ *
818
+ * `SpotifyClient#each_artist_album` pages through every album before the
819
+ * response is written, exactly like
820
+ * {@link MusicArtistImportsNamespace.albums}. Sixty seconds by default. The
821
+ * one case that is instant is a re-create over a row that already has a
822
+ * snapshot.
823
+ *
824
+ * An artist with a genuinely EMPTY catalogue never stops paying that cost:
825
+ * `[]` is `blank?`, so every create for it walks Spotify again.
826
+ *
827
+ * @throws {OmsError} `invalid_request` when `spotifyArtistId` is blank. The
828
+ * server would answer `400` for it too - `params.require` raises
829
+ * `ParameterMissing` and this controller, unlike the import one, RESCUES it
830
+ * into a normal bad request, so it does not page the owner - but the round
831
+ * trip buys nothing.
832
+ * @throws {OmsApiError} 400 `"Connect Spotify first."` with no linked
833
+ * identity, `"Spotify connection needs to be relinked."` on a dead refresh
834
+ * token, or `"Spotify upstream error: ..."` (truncated to 200 characters).
835
+ * All three are bare JSON strings and only the text separates them.
836
+ */
837
+ create(input: CreateArtistSyncInput, options?: RequestOptions): Promise<ArtistSync>;
838
+ /**
839
+ * `DELETE /artist_syncs/:id` - unfollow.
840
+ *
841
+ * Takes the {@link ArtistSyncId}, NOT the Spotify artist id: the only place
842
+ * to get one is {@link list} or the record {@link create} returned.
843
+ *
844
+ * Destroys the row and its snapshot outright. Nothing already imported is
845
+ * touched, and re-following later starts from a FRESH snapshot of the
846
+ * catalogue as it stands then - which means anything released during the gap
847
+ * is now part of the baseline and will never be picked up. That gap is
848
+ * silent; if it matters, run an import for the missing albums.
849
+ *
850
+ * The server answers `200 {"ok": true}` here rather than the `204` the rest
851
+ * of the API uses for a destroy. This resolves to `undefined` either way -
852
+ * the body carries no information - but a caller reading `response.status`
853
+ * through {@link ApiClient.raw} should not expect 204.
854
+ *
855
+ * Not retried, by the transport's default for `DELETE`: the second attempt
856
+ * would find nothing and report `404` for a row it had just removed.
857
+ *
858
+ * @throws {OmsApiError} 404 `"Artist sync not found"` for an id that is not
859
+ * yours or no longer exists. A non-numeric id lands here too, cast to `0`
860
+ * by the column type rather than rejected.
861
+ */
862
+ delete(id: ArtistSyncId, options?: RequestOptions): Promise<void>;
863
+ }
864
+ /** The `music.artists` namespace, reachable as `oms.music.artists`. */
865
+ export declare class MusicArtistsNamespace extends Resource {
866
+ /** Bulk import of a Spotify artist's catalogue. */
867
+ readonly imports: MusicArtistImportsNamespace;
868
+ /** Daily watch for FUTURE releases. Not a backfill - see the class. */
869
+ readonly syncs: MusicArtistSyncsNamespace;
870
+ constructor(http: ApiClient);
871
+ /**
872
+ * `GET /artists` - the roster.
873
+ *
874
+ * On the GENERAL ceiling (600/min), not the 60/min artist bucket: the
875
+ * throttle's pattern is `/artists/` with a trailing slash and the index path
876
+ * is normalised to `/artists`. It is the only route in this namespace with
877
+ * that luxury.
878
+ *
879
+ * Rows are the blueprint's default view: no `bio_html`, no
880
+ * `gallery_image_urls`, no `similar`. `songs_count` and
881
+ * `fallback_artwork_media_id` ARE filled in here.
882
+ *
883
+ * ALWAYS PASS `order`. See {@link ListArtistsParams.order} - the relation
884
+ * has no order of its own, and paging an unordered relation loses rows.
885
+ *
886
+ * This index emits an `ETag` and can answer `304`. The SDK never sends
887
+ * `If-None-Match` itself, and a browser's HTTP cache turns a 304 back into a
888
+ * 200 with the cached body before `fetch` sees it - but a hand-written
889
+ * `If-None-Match` in `options.headers` on Bun or React Native would produce a
890
+ * bare 304, which the transport treats as a failure (`response.ok` is false)
891
+ * and raises as an {@link OmsApiError} with status 304 and no body. Do not
892
+ * send the header.
893
+ *
894
+ * @throws {OmsApiError} 400 `"Unknown search filter: ..."` for a filter key
895
+ * outside the allowlist. Filters fail closed rather than widening the
896
+ * query.
897
+ */
898
+ list(params?: ListArtistsParams, options?: RequestOptions): Promise<Paginated<Artist>>;
899
+ /**
900
+ * `GET /artists/:idOrSlug` - one artist, extended view.
901
+ *
902
+ * Three lookups in order: a purely numeric segment is an id, otherwise a
903
+ * slug, otherwise a canonical name. So `get(42)`, `get("chico-buarque")` and
904
+ * `get("Chico Buarque")` all work, and an artist whose slug is all digits
905
+ * would be unreachable by slug - a case the backend does not handle and
906
+ * nothing in practice produces.
907
+ *
908
+ * ## This call can be slow the FIRST time, and it is not the network
909
+ *
910
+ * The controller runs `ArtistResolver.refresh_if_stale`, which is
911
+ * stale-while-revalidate with one exception. A row that has never been
912
+ * populated is filled in INLINE, on the request thread: a MusicBrainz search,
913
+ * two Last.fm calls and a Wikimedia gallery fetch before the response is
914
+ * written. A row that merely went stale is served immediately and refreshed
915
+ * by a background job, deduplicated to one job per artist per ten minutes.
916
+ *
917
+ * So budget a generous `timeoutMs` for a cold artist, and do not read a
918
+ * slow first load as a broken server. What you must NOT do is retry it fast:
919
+ * this route is inside the shared 60/min bucket.
920
+ *
921
+ * ## What null metadata means
922
+ *
923
+ * If every external call fails, the resolver still stamps `bio_fetched_at`,
924
+ * `similar_fetched_at` and the gallery timestamp, so the upstream is not
925
+ * hammered on every page view. The response is a normal `200` with
926
+ * `bio_html: null` and `similar: []`, and it will stay that way until the TTL
927
+ * (7 days for the biography, jittered) expires. There is no field that says
928
+ * "the fetch failed" - a client cannot distinguish a burnt fetch from an
929
+ * artist Last.fm has never heard of, and should present both as "no
930
+ * biography" rather than as an error.
931
+ *
932
+ * Deezer pictures are NOT touched by this route at all; they are refreshed
933
+ * only through `GET /songs/artist_pictures`.
934
+ *
935
+ * **60/min**, shared with lyrics, artist metadata and radios.
936
+ *
937
+ * @throws {OmsApiError} 404 `"Artist not found"`.
938
+ */
939
+ get(idOrSlug: ArtistId | string, options?: RequestOptions): Promise<ArtistExtended>;
940
+ /**
941
+ * `GET /artists/overview` - the whole Artists page header in one request.
942
+ *
943
+ * **Cached server-side for one hour, per user**, under
944
+ * `artists_overview:v2:<user id>`. Two consequences worth designing around:
945
+ * polling it is pointless, and an artist you just renamed or gave a new
946
+ * picture keeps its old card here for up to an hour while
947
+ * {@link MusicArtistsNamespace.get} already shows the new one. A client that
948
+ * refreshes this after an edit should expect no change and not treat it as a
949
+ * failed write.
950
+ *
951
+ * **60/min**, shared bucket. One call per page open is the intended shape.
952
+ */
953
+ overview(options?: RequestOptions): Promise<ArtistOverview>;
954
+ /**
955
+ * `PATCH /artists/:id` - renames an artist and/or replaces its gallery.
956
+ *
957
+ * ## The body is FLAT
958
+ *
959
+ * `{ name, gallery_image_urls }` at the top level. The web frontend sends
960
+ * `{ artist: { ... } }`, which `params.permit(:name, gallery_image_urls: [])`
961
+ * permits nothing out of: the update then assigns an empty hash, saves
962
+ * successfully and answers `200` with the record UNCHANGED. A nested body is
963
+ * not an error, it is a silent no-op, and it is the reason this method takes
964
+ * the fields rather than a body.
965
+ *
966
+ * Fields outside those two are dropped in silence as well (the fail-closed
967
+ * 400 applies to filter buckets, not to update params), so read the returned
968
+ * record rather than assuming the write landed.
969
+ *
970
+ * ## Addressed by NUMERIC ID ONLY
971
+ *
972
+ * Unlike {@link get}, this goes through the generic CRUD lookup
973
+ * (`find_by(id:)`), so a slug 404s - and with a different message,
974
+ * `"Resource not found"` rather than `"Artist not found"`. Resolve the slug
975
+ * with {@link get} first.
976
+ *
977
+ * **60/min**, shared bucket. Not retried by default.
978
+ *
979
+ * @throws {OmsApiError} 400 `"Gallery URLs must start with http:// or https://"`,
980
+ * or a validation message (a name that collides with another artist's
981
+ * canonical name fails the per-user uniqueness index).
982
+ * @throws {OmsApiError} 404 `"Resource not found"` for an unknown id, a slug,
983
+ * or another user's artist.
984
+ */
985
+ update(id: ArtistId, input: UpdateArtistInput, options?: RequestOptions): Promise<ArtistExtended>;
986
+ /**
987
+ * `DELETE /artists/:id` - `204` on success.
988
+ *
989
+ * ## A refusal arrives as 401, not 400
990
+ *
991
+ * `Artist#destroyable_by?` is `owner && song_artists.empty?`, and the CRUD
992
+ * action turns a false there into
993
+ * `401 "You are not authorized to destroy this resource"`. So "this artist
994
+ * still has songs" and "this artist is not yours" are the SAME response, and
995
+ * the SDK surfaces both as an {@link OmsAuthError}.
996
+ *
997
+ * That has one consequence a caller should know about: a client configured
998
+ * with `tokens.onUnauthorized` will spend a token refresh on it and retry
999
+ * once before failing, because the transport reasonably assumes a 401 means
1000
+ * a stale credential. Nothing breaks, but the refresh is wasted. Check
1001
+ * `songs_count === 0` before calling, and detach the songs first otherwise.
1002
+ *
1003
+ * Numeric id only, like {@link update}. **60/min**, shared bucket. Not
1004
+ * retried, because a replayed `DELETE` reports `404` for a row it removed
1005
+ * perfectly well.
1006
+ *
1007
+ * @throws {OmsAuthError} 401 when the artist still has credits, or is not
1008
+ * yours.
1009
+ * @throws {OmsApiError} 404 `"Resource not found"`.
1010
+ */
1011
+ delete(id: ArtistId, options?: RequestOptions): Promise<void>;
1012
+ /**
1013
+ * `POST /artists/:id/upload_image` - multipart, field name `image`. Replaces
1014
+ * the square avatar and returns the extended record.
1015
+ *
1016
+ * See {@link uploadBanner} for everything the two share: sizes, accepted
1017
+ * types, quota, and what the response does not carry.
1018
+ *
1019
+ * **60/min**, shared bucket. Not retried: a replay re-uploads the bytes.
1020
+ *
1021
+ * @throws {OmsError} `invalid_request` when the size is known ahead of time
1022
+ * and exceeds {@link ARTIST_IMAGE_MAX_BYTES}.
1023
+ */
1024
+ uploadImage(id: ArtistId, image: FileInput | NativeFile, options?: RequestOptions): Promise<ArtistExtended>;
1025
+ /**
1026
+ * `POST /artists/:id/upload_banner` - multipart, field name **`banner`**.
1027
+ *
1028
+ * The field is `banner`, NOT `image`. The web frontend sends `image` to this
1029
+ * route and gets `400 "banner required"` for it; do not copy that.
1030
+ *
1031
+ * ## Shared rules for both uploads
1032
+ *
1033
+ * - **10 MiB** ceiling ({@link ARTIST_IMAGE_MAX_BYTES}), checked before
1034
+ * anything else; over it is `400 "file too big (max 10485760B)"`. The SDK
1035
+ * checks first when the size is known, which it is for a `Blob` and for a
1036
+ * picker that reported one.
1037
+ * - JPEG, PNG, WebP and GIF are mapped straight to an extension; anything
1038
+ * else falls back to the filename's extension and is a
1039
+ * `400 "unsupported image type <type>"` only when that is empty too. So
1040
+ * always send a `filename` with a real extension.
1041
+ * - The bytes go through the music storage quota. Over budget is
1042
+ * `400 "Music storage quota exceeded"` - check `music_storage_bytes` in
1043
+ * `oms.quotas` first if you want to say something better than that.
1044
+ * - An oversized or malformed IMAGE (a decompression bomb) is rejected by
1045
+ * the image pipeline as a 400 as well, with the processor's own message.
1046
+ * - Uploading purges the legacy `compressed_*` companion, because nothing
1047
+ * regenerates it for artists and a leftover copy would keep rendering the
1048
+ * OLD picture. Expect `compressed_banner_media_id` to be null afterwards.
1049
+ * - The response is the `:extended` view rendered STANDALONE, so
1050
+ * `fallback_artwork_media_id` comes back null even for an artist that has
1051
+ * one. Merge the response into what you already had; do not replace it.
1052
+ * - Addressed like {@link get}, not like {@link update}: id, slug or
1053
+ * canonical name all resolve, and the 404 message is `"Artist not found"`.
1054
+ *
1055
+ * ## The three clients
1056
+ *
1057
+ * React Native passes the picked `{ uri, name, type }` straight through; the
1058
+ * transport appends it verbatim, which is the only thing that works there.
1059
+ * Browser and Bun pass a {@link FileInput} carrying a `Blob`/`Uint8Array` -
1060
+ * a bare `NativeFile` on those runtimes is rejected loudly rather than
1061
+ * stringified into an empty part.
1062
+ *
1063
+ * @throws {OmsError} `invalid_request` when a known size exceeds the ceiling.
1064
+ * @throws {OmsApiError} 400 for size, type, quota or image-decode failures;
1065
+ * 401 when the artist is not yours; 404 `"Artist not found"`.
1066
+ */
1067
+ uploadBanner(id: ArtistId, banner: FileInput | NativeFile, options?: RequestOptions): Promise<ArtistExtended>;
1068
+ }