@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.
- package/README.md +112 -130
- package/dist/index.js +4800 -610
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +5 -1
- package/dist/types/internal/attachments.d.ts +17 -0
- package/dist/types/internal/helpers.d.ts +9 -0
- package/dist/types/listing.d.ts +90 -0
- package/dist/types/resources/account.d.ts +71 -5
- package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
- package/dist/types/resources/admin/chests.d.ts +51 -0
- package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
- package/dist/types/resources/admin/identities.d.ts +99 -0
- package/dist/types/resources/admin/index.d.ts +77 -0
- package/dist/types/resources/admin/jobs.d.ts +97 -0
- package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
- package/dist/types/resources/admin/notepads.d.ts +49 -0
- package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
- package/dist/types/resources/admin/quotas.d.ts +114 -0
- package/dist/types/resources/admin/shortLinks.d.ts +201 -0
- package/dist/types/resources/admin/types.d.ts +222 -0
- package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
- package/dist/types/resources/admin.d.ts +1841 -0
- package/dist/types/resources/auth/index.d.ts +39 -0
- package/dist/types/resources/auth/passkeys.d.ts +652 -0
- package/dist/types/resources/auth/sessions.d.ts +844 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content/analysis.d.ts +62 -0
- package/dist/types/resources/content/blogs.d.ts +471 -0
- package/dist/types/resources/content/feedbacks.d.ts +263 -0
- package/dist/types/resources/content/index.d.ts +52 -0
- package/dist/types/resources/content/intel/articles.d.ts +230 -0
- package/dist/types/resources/content/intel/config.d.ts +135 -0
- package/dist/types/resources/content/intel/index.d.ts +53 -0
- package/dist/types/resources/content/intel/items.d.ts +91 -0
- package/dist/types/resources/content/intel/reports.d.ts +108 -0
- package/dist/types/resources/content/intel/scripts.d.ts +152 -0
- package/dist/types/resources/content/intel/sources.d.ts +236 -0
- package/dist/types/resources/content/intel/stats.d.ts +105 -0
- package/dist/types/resources/content/intel/types.d.ts +86 -0
- package/dist/types/resources/content/jokes.d.ts +93 -0
- package/dist/types/resources/content/notifications.d.ts +147 -0
- package/dist/types/resources/content/serviceUsages.d.ts +73 -0
- package/dist/types/resources/content/servicesStatus.d.ts +194 -0
- package/dist/types/resources/content/siteConfig.d.ts +43 -0
- package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
- package/dist/types/resources/content.d.ts +2775 -0
- package/dist/types/resources/dynamicQrs.d.ts +39 -3
- package/dist/types/resources/forms.d.ts +176 -35
- package/dist/types/resources/index.d.ts +19 -8
- package/dist/types/resources/ipLookup.d.ts +20 -4
- package/dist/types/resources/jobs.d.ts +69 -25
- package/dist/types/resources/library/annotations.d.ts +162 -0
- package/dist/types/resources/library/books.d.ts +534 -0
- package/dist/types/resources/library/chat.d.ts +259 -0
- package/dist/types/resources/library/index.d.ts +39 -0
- package/dist/types/resources/library/shelves.d.ts +237 -0
- package/dist/types/resources/library/types.d.ts +120 -0
- package/dist/types/resources/library.d.ts +1433 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies/addons.d.ts +388 -0
- package/dist/types/resources/movies/collections.d.ts +352 -0
- package/dist/types/resources/movies/index.d.ts +29 -0
- package/dist/types/resources/movies/types.d.ts +50 -0
- package/dist/types/resources/movies/watchProgress.d.ts +277 -0
- package/dist/types/resources/movies.d.ts +1160 -0
- package/dist/types/resources/music/artists.d.ts +1068 -0
- package/dist/types/resources/music/imports.d.ts +941 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1027 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1167 -0
- package/dist/types/resources/notepads.d.ts +4 -1
- package/dist/types/resources/quotas.d.ts +7 -1
- package/dist/types/resources/realtime.d.ts +855 -0
- package/dist/types/resources/shortLinks.d.ts +49 -5
- package/dist/types/resources/social/groupChats.d.ts +458 -0
- package/dist/types/resources/social/index.d.ts +19 -0
- package/dist/types/resources/social/messages.d.ts +424 -0
- package/dist/types/resources/social/relationships.d.ts +322 -0
- package/dist/types/resources/social/types.d.ts +37 -0
- package/dist/types/resources/social.d.ts +1348 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +111 -27
- package/dist/types/resources/tickets.d.ts +101 -13
- package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
- package/dist/types/resources/tools/captions.d.ts +448 -21
- package/dist/types/resources/tools/downloader.d.ts +21 -0
- package/dist/types/resources/tools/index.d.ts +57 -15
- package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
- package/dist/types/resources/tools/transcription.d.ts +35 -13
- package/dist/types/resources/tools/upscale.d.ts +23 -3
- package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
- package/dist/types/types.d.ts +249 -17
- package/package.json +2 -1
|
@@ -0,0 +1,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
|
+
}
|