@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,1132 @@
1
+ /**
2
+ * The `music.social` namespace: jams, the music half of a public profile, the
3
+ * music storage meter, the chat assistant and the DJ.
4
+ *
5
+ * Five endpoint families with one thing in common: not one of them is a CRUD
6
+ * resource. Every one is a bespoke controller with a hand-written hash for a
7
+ * body, so nothing here goes through Blueprinter, nothing here carries the
8
+ * `id`/`created_at`/`updated_at` base fields as a matter of course, and none of
9
+ * it accepts the list DSL (`search[...]`, `exact_search[...]`,
10
+ * `modifiers[page]`). What a route sends is exactly what its `ok!({ ... })`
11
+ * literal says, which is why every interface below is documented against the
12
+ * controller rather than against a blueprint view. There is no paging anywhere
13
+ * in this file either: every listing is a whole bare array with a server-side
14
+ * cap, so no method returns a `Paginated`.
15
+ *
16
+ * ## Only two of the five exist in the web frontend
17
+ *
18
+ * `/jams*` and `/users/:id/music_profile` are used by both clients and are the
19
+ * best-tested surface here. **`/music/storage`, `/music_assistant*` and
20
+ * `/music_dj*` are called only by the native app** - the web frontend has no
21
+ * service for any of them. They are in the SDK because the app is one of the
22
+ * three clients it serves, and because the assistant and the DJ are perfectly
23
+ * usable from the CLI and the MCP server. Nothing about them needs a phone; the
24
+ * note is about where the field experience comes from, so treat their shapes as
25
+ * less battle-hardened than the jam ones.
26
+ *
27
+ * ## A jam is half HTTP and half cable, and this file is the HTTP half
28
+ *
29
+ * Every method below performs an ACTION and returns what that action produced.
30
+ * None of them tells you what the jam is doing right now. The host's current
31
+ * song and position, the queue members are watching, the running skip tally,
32
+ * "somebody joined", "the jam ended" - all of that arrives over ActionCable, on
33
+ * two streams this SDK does not open and has no code for:
34
+ *
35
+ * - `jam:<jam_id>` (see {@link jamStreamName}), carrying `snapshot`,
36
+ * `state_changed`, `position_tick`, `members_changed`, `jam_updated`,
37
+ * `song_proposed`, `skip_votes`, `skipped` and `ended`. It is RECEIVE-ONLY:
38
+ * `JamChannel` declares no client actions at all, which is why every mutation
39
+ * in a jam is one of the HTTP calls below.
40
+ * - `playback:user:<host_id>`, the host's own playback stream, where
41
+ * {@link MusicJamsNamespace.propose} and a passing
42
+ * {@link MusicJamsNamespace.skipVote} deposit a `command` for the host's
43
+ * player to execute.
44
+ *
45
+ * Two consequences to design around:
46
+ *
47
+ * 1. **Join over HTTP first, subscribe second.** `JamChannel` authorizes on
48
+ * membership, so a subscription opened before `POST /jams/:id/join` has
49
+ * returned is answered with `reject_subscription`. A rejection later in the
50
+ * session means the jam ended or you were dropped - clear local state, do
51
+ * not retry-loop.
52
+ * 2. **A `200` from `propose` means "the message was sent", not "the song is
53
+ * queued".** The song enters the jam when the HOST's client acts on the
54
+ * `jam_add_song` command; a host whose app is backgrounded and disconnected
55
+ * never acts on it, and nothing reports that back. The same is true of a
56
+ * `skipVote` that returns `skipped: true`.
57
+ *
58
+ * ## Rate limits
59
+ *
60
+ * Not one of these paths has a `Rack::Attack` rule of its own, so all of them
61
+ * sit under the general authenticated ceiling of **600 requests a minute**,
62
+ * keyed by the literal `Authorization` header. The single extra ceiling is
63
+ * application-level and lives in `MusicDjController`: forty generations per
64
+ * user per hour shared between `/music_dj` and `/music_dj/batch`, counted in
65
+ * the Rails cache. It is counted per REQUEST, before the work and before the
66
+ * refusal, which is why {@link MusicDjNamespace} turns retrying off by default -
67
+ * see {@link MUSIC_DJ_HOURLY_CAP}.
68
+ *
69
+ * ## An OAuth access token cannot reach any of this
70
+ *
71
+ * `Authentication#enforce_oauth_scope!` denies by default: a Doorkeeper token
72
+ * reaches an action only when its controller declared an `oauth_scope` for it.
73
+ * None of these five controllers declares one, so a CLI or MCP host holding an
74
+ * OAuth token gets `403 {"error":"insufficient_scope"}` on every route in this
75
+ * file, whatever scopes it was granted. Use a session token (`POST /sessions`)
76
+ * or, in the browser, the session cookie.
77
+ *
78
+ * ## Errors are bare JSON strings
79
+ *
80
+ * `"Jam not found"`, `"Only friends of a jam member can join"`, `"Song not
81
+ * found"`. There is no `{ error: ... }` object and no envelope; the SDK's
82
+ * `OmsError` carries the string in `message`. One status is worth calling out
83
+ * before it costs somebody a session: **a refused join or a refused host action
84
+ * is `401`, not `403`** - `JamsController` uses `unauthorized!` for
85
+ * authorization failures. A host-wide "on 401, log the user out" interceptor
86
+ * will sign somebody out for tapping Join on the wrong jam. Test the message,
87
+ * or scope the interceptor to the auth routes.
88
+ */
89
+ import { Resource } from "../../http";
90
+ import type { Id, RequestOptions, Timestamp } from "../../types";
91
+ import type { Song, SongId } from "./songs";
92
+ /**
93
+ * Primary key of a jam. An INTEGER (`jams` has a default `bigint` id), like
94
+ * songs, playlists and artists - and unlike `host_id` and the member ids
95
+ * sitting beside it in the very same payload, which are user ids and therefore
96
+ * strings. `MusicListeningSnapshot.jam_id` is the same integer, so a feed row
97
+ * can be matched straight against {@link Jam.id}.
98
+ */
99
+ export type JamId = number;
100
+ /** Who may feed the queue. */
101
+ export type JamQueueMode = "everyone" | "host";
102
+ /** What it takes for a skip to pass. */
103
+ export type JamSkipMode = "majority" | "host" | "anyone";
104
+ /** Everything `Jam::QUEUE_MODES` accepts. Anything else is a `400`. */
105
+ export declare const JAM_QUEUE_MODES: readonly ["everyone", "host"];
106
+ /** Everything `Jam::SKIP_MODES` accepts. Anything else is a `400`. */
107
+ export declare const JAM_SKIP_MODES: readonly ["majority", "host", "anyone"];
108
+ /**
109
+ * How many upcoming entries the cable's state payload carries
110
+ * (`Jams::Serializer::UPCOMING_LIMIT`). Nothing in this file returns them - it
111
+ * is here so a client sizing its "up next" list uses the server's number rather
112
+ * than a guess.
113
+ */
114
+ export declare const JAM_UPCOMING_LIMIT = 10;
115
+ /**
116
+ * How long a presigned `*_url` in any cross-user payload stays valid, in
117
+ * milliseconds (`MediaUrls::EXPIRY`, six hours).
118
+ *
119
+ * The server caches each signature for five hours, so a URL you receive has at
120
+ * least an hour of life left and the SAME string is handed out again across
121
+ * broadcasts - deliberately, so a follower does not treat every pause/resume
122
+ * as a new source and rebuffer. Compare songs by `id`, never by URL.
123
+ */
124
+ export declare const MUSIC_PRESIGNED_URL_TTL_MS: number;
125
+ /** One participant, as the jam payload nests them. */
126
+ export interface JamMember {
127
+ /** The USER's id, a string. Never a `jam_members` row id - that is not sent. */
128
+ readonly id: Id;
129
+ readonly handle: string;
130
+ readonly name: string;
131
+ /** True for exactly one member: the one whose id equals `jam.host_id`. */
132
+ readonly is_host: boolean;
133
+ /** When this member joined - the join row's `created_at`. */
134
+ readonly joined_at: Timestamp;
135
+ }
136
+ /**
137
+ * A jam, as `Jams::Serializer.jam_hash` renders it.
138
+ *
139
+ * This is NOT a Blueprinter view. There is no `updated_at` here even though the
140
+ * table has one, and `members` is a bespoke five-key hash rather than a user
141
+ * view, so none of the base-record guarantees the rest of the API makes apply.
142
+ *
143
+ * `members` comes back ordered by join time, which normally puts the host
144
+ * first - normally, not always. Find the host with {@link jamHost} rather than
145
+ * by position.
146
+ */
147
+ export interface Jam {
148
+ readonly id: JamId;
149
+ /** Owner of the jam. A user id, so a STRING beside this record's integer id. */
150
+ readonly host_id: Id;
151
+ /** Defaults to `"everyone"` at creation. */
152
+ readonly queue_mode: JamQueueMode;
153
+ /** Defaults to `"majority"` at creation. */
154
+ readonly skip_mode: JamSkipMode;
155
+ readonly created_at: Timestamp;
156
+ /**
157
+ * `null` while the jam is live.
158
+ *
159
+ * Every route here scopes to `Jam.active` (`ended_at IS NULL`), so an ended
160
+ * jam answers `404` rather than a payload with a timestamp in this field. You
161
+ * will only ever see a non-null value on a copy you were already holding, or
162
+ * on one that arrived over the cable.
163
+ */
164
+ readonly ended_at: Timestamp | null;
165
+ readonly members: JamMember[];
166
+ }
167
+ /**
168
+ * `GET /jams`: the caller's own jam, plus the jams they are allowed to join.
169
+ *
170
+ * `joinable` is not "every live jam". It is the live jams containing at least
171
+ * one ACCEPTED FRIEND of the caller - which is the exact predicate
172
+ * {@link MusicJamsNamespace.join} authorizes against, so the list can never
173
+ * offer a jam that would then refuse you. The caller's own jam is excluded.
174
+ */
175
+ export interface JamsIndex {
176
+ /** The live jam the caller belongs to, or `null`. At most one, ever. */
177
+ readonly current: Jam | null;
178
+ /** Live jams with a friend in them, minus {@link JamsIndex.current}. */
179
+ readonly joinable: Jam[];
180
+ }
181
+ /**
182
+ * The rules a host may change. Both fields optional; sending an empty object is
183
+ * an accepted no-op that still broadcasts `jam_updated` to everyone.
184
+ */
185
+ export interface UpdateJamRulesInput {
186
+ readonly queue_mode?: JamQueueMode;
187
+ readonly skip_mode?: JamSkipMode;
188
+ }
189
+ /** `POST /jams/:id/skip_vote` in full. */
190
+ export interface JamSkipVoteResult {
191
+ /**
192
+ * True when this vote carried the skip and the host's player was told to
193
+ * advance. Also true, always and immediately, when the voter is the host.
194
+ */
195
+ readonly skipped: boolean;
196
+ /** Distinct voters for the CURRENT song, this one included. */
197
+ readonly count: number;
198
+ /** Votes required: `1` under `"anyone"`, `floor(members / 2) + 1` otherwise. */
199
+ readonly needed: number;
200
+ }
201
+ /**
202
+ * A song as the cross-user payloads render it
203
+ * (`Listening::Snapshot.song_hash`): the seven fields somebody who does NOT own
204
+ * the track is allowed to see.
205
+ *
206
+ * ## `id` is a number, and every other client says otherwise
207
+ *
208
+ * `oms-music/src/domain/song.ts` types `SnapshotSong.id` as `string`, the web
209
+ * frontend's `ListeningSong.id` is a `string`, and `docs/api-social-jams.md`
210
+ * writes `id: string; // Song id (stringly numeric)` and repeats it in gotcha
211
+ * 1 ("song ids are strings"). **All three are wrong about this payload.**
212
+ * `songs` has a default `bigint` primary key and `Listening::Snapshot` emits
213
+ * `id: song.id` with no cast, so it arrives as a JSON NUMBER exactly like
214
+ * `Song.id` does everywhere else in the REST API. The mistake is invisible
215
+ * while the field is only rendered or used as a cache key; it bites the first
216
+ * time somebody writes `snapshot.song.id === song.id`, which is always false.
217
+ * Song ids become strings on the CABLE (`position_tick.song_id`), which is
218
+ * where that belief comes from.
219
+ */
220
+ export interface MusicListeningSong {
221
+ /** Integer song id. See the note above about three clients typing it wrong. */
222
+ readonly id: number;
223
+ readonly title: string;
224
+ readonly album: string | null;
225
+ /** Whole seconds. */
226
+ readonly duration: number;
227
+ /** The OWNER's user id, a string. Not the viewer's. */
228
+ readonly owner_id: Id;
229
+ /** Pre-joined credits, `", "`-separated. An empty string when there are none. */
230
+ readonly artist_names: string;
231
+ /**
232
+ * A PRESIGNED absolute URL, already usable, and short-lived
233
+ * ({@link MUSIC_PRESIGNED_URL_TTL_MS}).
234
+ *
235
+ * The viewer does not own the underlying attachment, so `/media/:id/data`
236
+ * would refuse it - use this string verbatim and never try to re-derive one
237
+ * from the id. `null` is a real outcome: `MediaUrls.for_attachment` swallows
238
+ * a presign failure and returns nothing rather than raising.
239
+ */
240
+ readonly artwork_url: string | null;
241
+ }
242
+ /**
243
+ * What a friend may see about somebody's playback
244
+ * (`Listening::Snapshot.for_user`), and the shape the friends feed pushes over
245
+ * `listening:user:<id>` with a `type: "listening_update"` key merged in AT THE
246
+ * TOP LEVEL, not nested.
247
+ */
248
+ export interface MusicListeningSnapshot {
249
+ readonly user: {
250
+ readonly id: Id;
251
+ readonly handle: string;
252
+ readonly name: string;
253
+ };
254
+ /**
255
+ * `null` when nothing is playing OR when the user turned `share_listening`
256
+ * off. The two are deliberately indistinguishable.
257
+ */
258
+ readonly song: MusicListeningSong | null;
259
+ /** `true` when there is no playback state at all, not only when paused. */
260
+ readonly paused: boolean;
261
+ /** Whether a playback device was seen in the last 75 seconds. */
262
+ readonly online: boolean;
263
+ /**
264
+ * The live jam they are in, or `null`. Survives `share_listening: false`
265
+ * along with `online`, `paused` and `updated_at`: a jam is an explicit social
266
+ * act, listening is passive, and only the passive half is hidden.
267
+ */
268
+ readonly jam_id: JamId | null;
269
+ /** When the playback row last changed, or `null` when there is none. */
270
+ readonly updated_at: Timestamp | null;
271
+ }
272
+ /** One row of {@link MusicProfileVisible.top_artists}. */
273
+ export interface MusicProfileArtist {
274
+ /** Integer artist id, scoped to the PROFILE OWNER's library, never the viewer's. */
275
+ readonly id: number;
276
+ readonly name: string;
277
+ readonly slug: string;
278
+ /** Cached Deezer picture set. Absolute public URLs; all set, or all null. */
279
+ readonly picture: string | null;
280
+ readonly picture_medium: string | null;
281
+ readonly picture_big: string | null;
282
+ readonly picture_xl: string | null;
283
+ /** Last.fm's image. Almost always `null` - see the artists namespace. */
284
+ readonly external_image_url: string | null;
285
+ /** Presigned URL for the owner's uploaded avatar, or `null`. Use verbatim. */
286
+ readonly image_url: string | null;
287
+ /** Plays in the last 30 days. */
288
+ readonly play_count: number;
289
+ }
290
+ /**
291
+ * A profile the viewer is allowed to see.
292
+ *
293
+ * All six keys are always present on a visible profile - `MusicProfiles::Builder`
294
+ * builds one literal with no conditionals - which is why they are required here
295
+ * even though both existing clients type every field as optional. See
296
+ * {@link MusicProfile} for why they had to.
297
+ */
298
+ export interface MusicProfileVisible {
299
+ readonly visible: true;
300
+ /** A full listening snapshot of the OWNER, same shape as a feed row. */
301
+ readonly now_playing: MusicListeningSnapshot;
302
+ /** Up to 8, over the last 30 days, most played first. */
303
+ readonly top_artists: MusicProfileArtist[];
304
+ /** Up to 10, over the last 30 days, most played first. */
305
+ readonly top_songs: (MusicListeningSong & {
306
+ readonly play_count: number;
307
+ })[];
308
+ /** Up to 10 distinct songs, most recently played first. */
309
+ readonly recent: (MusicListeningSong & {
310
+ readonly last_played_at: Timestamp;
311
+ })[];
312
+ /** Play events in the last 30 days. */
313
+ readonly plays_30d: number;
314
+ }
315
+ /**
316
+ * A profile the viewer may not see: `{ "visible": false }` and nothing else, at
317
+ * status `200`.
318
+ *
319
+ * This is not an error and must not be handled as one. It is what a signed-in
320
+ * stranger gets, what a friend of somebody with `share_listening` off gets, and
321
+ * what the client contract asks you to render as nothing - a private profile is
322
+ * deliberately indistinguishable from an empty one. A real `404` ("User not
323
+ * found.") means the user does not exist; a `401` means you sent no credential,
324
+ * because `music_profile` is not on the unauthenticated allowlist.
325
+ */
326
+ export interface MusicProfileHidden {
327
+ readonly visible: false;
328
+ }
329
+ /**
330
+ * `GET /users/:idOrHandle/music_profile`.
331
+ *
332
+ * A discriminated union rather than the "everything optional" object both
333
+ * existing clients use (`MusicProfile` in `oms-music/src/domain/social.ts` and
334
+ * in the frontend's `SocialMusicService.ts`, both with `visible: boolean` and
335
+ * five `?` fields). Those types are not wrong about the wire - they simply
336
+ * cannot say that the five fields are present together or absent together, so
337
+ * every read site needs a `?.` that TypeScript can never discharge. Narrow once
338
+ * with {@link isMusicProfileVisible} and the rest is non-optional.
339
+ */
340
+ export type MusicProfile = MusicProfileVisible | MusicProfileHidden;
341
+ /**
342
+ * `GET /music/storage`: bytes of music media stored, against the account's
343
+ * ceiling.
344
+ *
345
+ * ## `limit_bytes` IS NULLABLE, and it did not use to be
346
+ *
347
+ * The ceiling was `users.music_storage_limit_bytes`, a `NOT NULL` column, so
348
+ * `limit_bytes` was always a number - which is why the native app types it
349
+ * `limit_bytes: number` and has no `unlimited` field at all
350
+ * (`oms-music/src/api/endpoints/musicStorage.ts`). Since the quota catalogue
351
+ * landed (`Quotas::CATALOG`, migration `20260826120000`) an administrator can
352
+ * mark an account unlimited by storing a `QuotaOverride` row with a `NULL`
353
+ * value; the limit then resolves to `Float::INFINITY` and `Quotas.limit_json`
354
+ * serialises it as `null`, because JSON has no infinity.
355
+ *
356
+ * So read {@link MusicStorageUsage.unlimited} FIRST. A client that divides
357
+ * `used_bytes` by `limit_bytes` renders `NaN%`, and one that reads `null` as
358
+ * `0` shows a permanently full bar to exactly the accounts that were given no
359
+ * ceiling at all. {@link musicStorageRemaining} and
360
+ * {@link musicStorageAffords} answer both questions without the arithmetic.
361
+ *
362
+ * An ordinary account is unaffected: with no override row the limit is the
363
+ * catalogue default of 100 GiB - the same number the old column defaulted to,
364
+ * and `quotas_test.rb` pins the two together so they cannot drift apart.
365
+ *
366
+ * ## The same number is also in `oms.quotas`
367
+ *
368
+ * `music_storage_bytes` is one of the six resources `GET /quotas` reports, and
369
+ * both routes call `Quotas.limit_for` and `Music::Quota.usage`, so they cannot
370
+ * disagree. Use `oms.quotas.list()` when you want every ceiling at once; use
371
+ * this when you want only this one, or when you are talking to a build older
372
+ * than the catalogue.
373
+ */
374
+ export interface MusicStorageUsage {
375
+ /**
376
+ * Sum of the DISTINCT blobs reachable from the account's songs, artists and
377
+ * playlists. Computed live on every call - a blob shared by two records
378
+ * counts once, exactly as it costs once in object storage - so it can never
379
+ * drift the way the storage tree's cached counters did. It is also not free:
380
+ * read it on a settings screen, not on a timer.
381
+ */
382
+ readonly used_bytes: number;
383
+ /** The ceiling in bytes, or `null` when {@link unlimited} is `true`. */
384
+ readonly limit_bytes: number | null;
385
+ /** `true` when an administrator removed this account's ceiling. */
386
+ readonly unlimited: boolean;
387
+ }
388
+ /** Primary key of a persisted assistant chat. An INTEGER. */
389
+ export type MusicAssistantChatId = number;
390
+ /** Turns of history the server feeds the model (`Responder::MAX_HISTORY`). */
391
+ export declare const MUSIC_ASSISTANT_HISTORY_TURNS = 20;
392
+ /** Messages a chat keeps before the oldest fall off (`AssistantChat::MAX_MESSAGES`). */
393
+ export declare const MUSIC_ASSISTANT_CHAT_MAX_MESSAGES = 200;
394
+ /** Player actions one answer may carry (`Responder::MAX_ACTIONS`). */
395
+ export declare const MUSIC_ASSISTANT_MAX_ACTIONS = 10;
396
+ /**
397
+ * Largest request body `MusicAssistantController` accepts, in bytes. Over it is
398
+ * `413 "Request too big"`, decided from `Content-Length` before any parsing.
399
+ */
400
+ export declare const MUSIC_ASSISTANT_MAX_BODY_BYTES: number;
401
+ /**
402
+ * How long a chat may sit idle before it seals itself, in milliseconds
403
+ * (`AssistantChat::READ_ONLY_AFTER`, two days).
404
+ *
405
+ * It is COMPUTED, never persisted - no job runs, `read_only?` is a comparison
406
+ * against the clock - so a summary you cached yesterday can report
407
+ * `read_only: false` for a chat that is now sealed. The `POST` is where you
408
+ * find out, with a `423`.
409
+ */
410
+ export declare const MUSIC_ASSISTANT_READ_ONLY_AFTER_MS: number;
411
+ /**
412
+ * Default deadline for one assistant generation, in milliseconds.
413
+ *
414
+ * The client's global default is 60 s and a cold model behind OpenRouter
415
+ * routinely beats that while still being on its way to an answer. The app uses
416
+ * 90 s for the same reason; pass `timeoutMs` to override.
417
+ */
418
+ export declare const MUSIC_ASSISTANT_TIMEOUT_MS = 90000;
419
+ /** One turn of a conversation. */
420
+ export interface MusicAssistantMessage {
421
+ readonly role: "user" | "assistant";
422
+ readonly content: string;
423
+ /** Only on messages read back from a stored chat; never on ones you send. */
424
+ readonly created_at?: Timestamp;
425
+ }
426
+ /** A stored session, without its transcript. */
427
+ export interface MusicAssistantChatSummary {
428
+ readonly id: MusicAssistantChatId;
429
+ /** The first 60 squished characters of the first user message. */
430
+ readonly title: string;
431
+ readonly last_message_at: Timestamp;
432
+ /** See {@link MUSIC_ASSISTANT_READ_ONLY_AFTER_MS}: computed, so it can go stale. */
433
+ readonly read_only: boolean;
434
+ }
435
+ /** A stored session with its transcript. */
436
+ export interface MusicAssistantChatDetail extends MusicAssistantChatSummary {
437
+ /** Oldest first. Capped at {@link MUSIC_ASSISTANT_CHAT_MAX_MESSAGES}. */
438
+ readonly messages: MusicAssistantMessage[];
439
+ }
440
+ /**
441
+ * Snapshot of the caller's player, so the model can answer "pause this" and
442
+ * "who sings this".
443
+ *
444
+ * The server applies a strict whitelist (`MusicAssistantController#player_context`);
445
+ * a key that is not one of these nine never reaches the prompt, silently. Every
446
+ * field is optional here because the whitelist permits rather than requires.
447
+ */
448
+ export interface MusicAssistantPlayerContext {
449
+ readonly song_id?: number | null;
450
+ readonly title?: string | null;
451
+ readonly artist?: string | null;
452
+ readonly playing?: boolean;
453
+ /** `0` to `1`. */
454
+ readonly volume?: number;
455
+ readonly shuffle?: boolean;
456
+ /** `"none"`, `"one"` or `"all"`. */
457
+ readonly loop_mode?: string;
458
+ /** Playback rate, `0.5` to `1.5`. */
459
+ readonly rate?: number;
460
+ readonly queue_length?: number;
461
+ }
462
+ /** A playlist the assistant created or changed during the turn. */
463
+ export interface MusicAssistantPlaylistRef {
464
+ readonly id: number;
465
+ readonly name: string;
466
+ readonly song_count: number;
467
+ }
468
+ /**
469
+ * A player command the server validated and the client executes LOCALLY.
470
+ *
471
+ * Nothing here runs on the server. The songs in `play` and `queue` arrive fully
472
+ * serialised in the `GET /songs` shape and have already been through
473
+ * `viewable_by`, so a client queues them without a second request and without
474
+ * re-checking anything. Values are clamped server-side before they get here:
475
+ * `set_volume` to `[0, 1]`, `set_rate` to `[0.5, 1.5]`, `sleep_timer.minutes`
476
+ * to `[1, 600]`. An action the sanitiser did not recognise is DROPPED rather
477
+ * than passed through, so an unknown `action` string should not appear - handle
478
+ * one by ignoring it anyway, because this list grows.
479
+ */
480
+ export type MusicAssistantAction = {
481
+ readonly action: "play";
482
+ readonly songs: Song[];
483
+ readonly shuffle: boolean;
484
+ } | {
485
+ readonly action: "queue";
486
+ readonly songs: Song[];
487
+ readonly mode: "next" | "last";
488
+ } | {
489
+ readonly action: "pause";
490
+ } | {
491
+ readonly action: "resume";
492
+ } | {
493
+ readonly action: "skip";
494
+ } | {
495
+ readonly action: "previous";
496
+ } | {
497
+ readonly action: "set_shuffle";
498
+ readonly on: boolean;
499
+ } | {
500
+ readonly action: "set_loop";
501
+ readonly mode: "none" | "one" | "all";
502
+ } | {
503
+ readonly action: "set_volume";
504
+ readonly value: number;
505
+ } | {
506
+ readonly action: "set_rate";
507
+ readonly value: number;
508
+ } | {
509
+ readonly action: "sleep_timer";
510
+ readonly minutes: number;
511
+ } | {
512
+ readonly action: "sleep_timer";
513
+ readonly end_of_song: true;
514
+ } | {
515
+ readonly action: "sleep_timer";
516
+ readonly off: true;
517
+ } | {
518
+ readonly action: "open";
519
+ readonly target: "playlist";
520
+ readonly playlist_id: number;
521
+ } | {
522
+ readonly action: "open";
523
+ readonly target: "artist";
524
+ readonly artist: string;
525
+ } | {
526
+ readonly action: "open";
527
+ readonly target: "album";
528
+ readonly artist: string | null;
529
+ readonly album: string;
530
+ } | {
531
+ readonly action: "open";
532
+ readonly target: "liked";
533
+ } | {
534
+ readonly action: "open";
535
+ readonly target: "settings";
536
+ };
537
+ /**
538
+ * What `POST /music_assistant` answers.
539
+ *
540
+ * `playlist` and `actions` are OMITTED when the turn produced none - the
541
+ * controller only writes the keys it has - so test with `in` or a truthiness
542
+ * check rather than against `null`.
543
+ */
544
+ export interface MusicAssistantAnswer {
545
+ /** Always present. Falls back to an apology when the model made no sense. */
546
+ readonly reply: string;
547
+ /** One card per answer at most, for the last playlist a tool touched. */
548
+ readonly playlist?: MusicAssistantPlaylistRef;
549
+ /** Up to {@link MUSIC_ASSISTANT_MAX_ACTIONS}, in order. Execute, do not re-resolve. */
550
+ readonly actions?: MusicAssistantAction[];
551
+ /**
552
+ * The chat the exchange was stored in - NEW on the first message of a
553
+ * session, so keep whatever comes back. Present only in the persisted mode;
554
+ * {@link MusicAssistantNamespace.ask} never returns one.
555
+ */
556
+ readonly chat_id?: MusicAssistantChatId;
557
+ }
558
+ /** Everything {@link MusicAssistantNamespace.send} takes. */
559
+ export interface SendMusicAssistantMessageInput {
560
+ /** The new message, and only the new one. The history is the server's. */
561
+ readonly message: string;
562
+ /** Omit to open a new chat. The answer carries the id it created. */
563
+ readonly chatId?: MusicAssistantChatId;
564
+ /** Optional player snapshot. Strictly whitelisted server-side. */
565
+ readonly player?: MusicAssistantPlayerContext;
566
+ }
567
+ /**
568
+ * Generations per user per hour, shared by `/music_dj` and `/music_dj/batch`
569
+ * (`MusicDjController::HOURLY_CAP`).
570
+ *
571
+ * Enforced in the controller with a cache counter rather than by
572
+ * `Rack::Attack`, and the counter is incremented by EVERY request - including
573
+ * the ones it then refuses with `429`. A retry loop therefore drives the count
574
+ * further past the cap and can never recover inside the hour, which is why
575
+ * {@link MusicDjNamespace} passes `retry: false` unless you override it.
576
+ */
577
+ export declare const MUSIC_DJ_HOURLY_CAP = 40;
578
+ /**
579
+ * Default deadline for one DJ generation, in milliseconds. A script from the
580
+ * free LLM plus a couple of seconds of local text-to-speech takes well past the
581
+ * client's 60 s default; the app uses 120 s.
582
+ */
583
+ export declare const MUSIC_DJ_TIMEOUT_MS = 120000;
584
+ /** Songs one `/music_dj/batch` set plans at most (`BatchPlanner::BATCH_SIZE`). */
585
+ export declare const MUSIC_DJ_BATCH_SIZE = 4;
586
+ /** `POST /music_dj`: one spoken link between two tracks. */
587
+ export interface MusicDjInterstitial {
588
+ /** The script as text, at most 320 characters. Worth showing while audio loads. */
589
+ readonly text: string;
590
+ /**
591
+ * The same script spoken, base64, NOT a data URL. Decode with
592
+ * {@link musicDjAudioBytes} or wrap with {@link musicDjAudioDataUrl}.
593
+ */
594
+ readonly audio_base64: string;
595
+ /** Container of the decoded bytes. `"wav"` today, and typed wide on purpose. */
596
+ readonly format: string;
597
+ }
598
+ /** `POST /music_dj/batch`: a whole set - what to play next, and the words for it. */
599
+ export interface MusicDjBatch extends MusicDjInterstitial {
600
+ /**
601
+ * The planned tracks, in play order, in the full `GET /songs` shape and
602
+ * already through `viewable_by`. Between 1 and
603
+ * {@link MUSIC_DJ_BATCH_SIZE}: the planner raises rather than answer with an
604
+ * empty set, so this is never `[]`.
605
+ */
606
+ readonly songs: Song[];
607
+ }
608
+ /** Everything `POST /music_dj/batch` accepts. All of it optional. */
609
+ export interface MusicDjBatchInput {
610
+ /** A free-text steer ("something calmer"). Truncated to 300 characters. */
611
+ readonly request?: string;
612
+ /**
613
+ * Recently played ids. Only the last 60 are read, and the planner SUBTRACTS
614
+ * them from its own picks - so a list that covers the whole library leaves
615
+ * nothing playable and the call fails with a `502`.
616
+ */
617
+ readonly recentSongIds?: SongId[];
618
+ /** Ids the listener skipped, as negative signal. Only the last 20 are read. */
619
+ readonly skippedSongIds?: SongId[];
620
+ /** Which set of the session this is, so the script can vary its opening. */
621
+ readonly batchIndex?: number;
622
+ }
623
+ /**
624
+ * Jams over HTTP. The realtime half lives on the cable and is not in this SDK -
625
+ * see the module note, and {@link jamStreamName} for the stream to subscribe to.
626
+ */
627
+ export declare class MusicJamsNamespace extends Resource {
628
+ /**
629
+ * `GET /jams` - the caller's live jam plus the ones they may join.
630
+ *
631
+ * Cheap and not cached anywhere, but also not a subscription: it is what you
632
+ * call on app start to rediscover a jam you were already in, and when opening
633
+ * a "join a jam" list. Everything that happens afterwards arrives on the
634
+ * cable, so polling this is the wrong shape.
635
+ *
636
+ * Missing keys are normalised (`current: null`, `joinable: []`) so a caller
637
+ * never has to guard the two separately.
638
+ *
639
+ * General ceiling: 600/min.
640
+ */
641
+ list(options?: RequestOptions): Promise<JamsIndex>;
642
+ /**
643
+ * The caller's live jam, or `null`. Convenience over {@link list} for the
644
+ * app-start "am I still in a jam" question; costs the same one request.
645
+ */
646
+ current(options?: RequestOptions): Promise<Jam | null>;
647
+ /**
648
+ * `POST /jams` - opens a jam hosted by the caller, who becomes its first
649
+ * member. `201`.
650
+ *
651
+ * Two side effects worth knowing before you call it:
652
+ *
653
+ * - **It silently leaves whatever jam you were in, and ENDS it if you were
654
+ * hosting.** One jam at a time is enforced server-side, with no
655
+ * confirmation and no error - the previous jam's members just receive
656
+ * `ended`.
657
+ * - The caller's friends-feed row is re-broadcast so it gains the jam badge.
658
+ *
659
+ * A jam with no active host device is a silent jam: the whole relay rides the
660
+ * host's playback publishes, so proposals and skip votes answer `400 "The
661
+ * host is not playing right now"` until the host's client claims the active
662
+ * device (`claim_active` with `mode: "steal"` on `PlaybackChannel`) and plays
663
+ * something. Do that immediately after this returns.
664
+ *
665
+ * General ceiling: 600/min.
666
+ */
667
+ create(options?: RequestOptions): Promise<Jam>;
668
+ /**
669
+ * `POST /jams/:id/join` - joins a live jam. `200`, body is the jam.
670
+ *
671
+ * Authorization is "an accepted friend of ANY current member", not of the
672
+ * host, and it is the same predicate {@link list} filters `joinable` with -
673
+ * so a jam that came out of that list will not refuse you unless its
674
+ * membership changed in between. Joining a jam you are already in is a no-op
675
+ * success rather than an error.
676
+ *
677
+ * Like {@link create}, this silently leaves (or ends) your previous jam.
678
+ *
679
+ * Then, and only then, subscribe to {@link jamStreamName}: the channel
680
+ * rejects non-members.
681
+ *
682
+ * @throws {OmsError} `404 "Jam not found"` - also what an ENDED jam answers.
683
+ * @throws {OmsError} `401 "Only friends of a jam member can join"`. Note the
684
+ * status: this is an authorization failure wearing a `401`, so do not let a
685
+ * global "401 means log out" interceptor see it.
686
+ */
687
+ join(jamId: JamId, options?: RequestOptions): Promise<Jam>;
688
+ /**
689
+ * `POST /jams/:id/leave` - leaves a jam. `200` with a `null` body.
690
+ *
691
+ * **If the caller is the HOST this ends the jam for everybody.** There is no
692
+ * host handoff anywhere in this feature; the remaining members receive
693
+ * `ended` and the jam row is closed. For a member it deletes the membership
694
+ * row and broadcasts `members_changed`.
695
+ *
696
+ * @throws {OmsError} `404 "Jam not found"` when the jam is over, or when the
697
+ * caller was not a member of it - the two are not distinguished.
698
+ */
699
+ leave(jamId: JamId, options?: RequestOptions): Promise<void>;
700
+ /**
701
+ * `DELETE /jams/:id` - the host ends the jam for everyone. `200` with a
702
+ * `null` body, NOT the `204` the SDK's other destroys answer with.
703
+ *
704
+ * Identical in effect to a host calling {@link leave}, which is what the web
705
+ * UI's "End jam" button actually does. Both set `ended_at`, broadcast
706
+ * `ended`, and re-broadcast every member's feed row so their jam badges drop.
707
+ *
708
+ * @throws {OmsError} `401 "Only the host can end a jam"` - an authorization
709
+ * failure with an authentication status. See {@link join}.
710
+ */
711
+ end(jamId: JamId, options?: RequestOptions): Promise<void>;
712
+ /**
713
+ * `PATCH /jams/:id` - the host changes the rules. `200`, body is the jam.
714
+ *
715
+ * `queue_mode` gates {@link propose}, `skip_mode` gates {@link skipVote}. Any
716
+ * subset is accepted, an empty object included (a no-op that still fans
717
+ * `jam_updated` out to everyone). An invalid value is a `400` carrying the
718
+ * validation message; use {@link JAM_QUEUE_MODES} and {@link JAM_SKIP_MODES}
719
+ * rather than a literal.
720
+ *
721
+ * @throws {OmsError} `401 "Only the host can change the rules"`.
722
+ */
723
+ updateRules(jamId: JamId, rules: UpdateJamRulesInput, options?: RequestOptions): Promise<Jam>;
724
+ /**
725
+ * `POST /jams/:id/invite` - notifies a friend that the jam exists. `200` with
726
+ * a `null` body.
727
+ *
728
+ * An invitation is PURELY a notification (`kind: "jam_invite"`, delivered on
729
+ * the invitee's notifications channel). It creates no state, grants no
730
+ * access, expires never, and has no accept endpoint: the invitee joins
731
+ * through {@link join} like anybody else, which they could already do because
732
+ * being a friend of the inviter - a member - is the whole authorization rule.
733
+ * So an invite is a nudge, and revoking one is not a thing.
734
+ *
735
+ * The caller must be a member; the target must be an accepted friend of the
736
+ * CALLER (not of the host) and not already in the jam.
737
+ *
738
+ * @throws {OmsError} `404 "Jam not found"`, `404 "User not found"`,
739
+ * `400 "You can only invite your friends"`, `400 "Already in the jam"`.
740
+ */
741
+ invite(jamId: JamId, userId: Id, options?: RequestOptions): Promise<void>;
742
+ /**
743
+ * `POST /jams/:id/propose` - offers one of YOUR OWN songs as an upcoming
744
+ * pick. `200` with a `null` body.
745
+ *
746
+ * ## The `200` is weaker than it looks
747
+ *
748
+ * Nothing is written to the database here. The server records the song in a
749
+ * 24-hour cache allowlist (so the host's playback state is allowed to
750
+ * reference a song the host does not own), then broadcasts a `jam_add_song`
751
+ * command onto the HOST's playback stream carrying a fully presigned payload.
752
+ * The song joins the queue when the host's CLIENT handles that command and
753
+ * republishes its state. A host whose app is backgrounded, disconnected, or
754
+ * simply older than the feature never handles it, and no error comes back to
755
+ * you. Watch the jam stream for the `state_changed` that follows, rather than
756
+ * treating the `200` as confirmation.
757
+ *
758
+ * The song must be the CALLER's. Proposing the host's song, or a third
759
+ * user's, is `404 "Song not found"` - the lookup is scoped to `user_id`, so a
760
+ * song you can see but do not own does not exist for this route.
761
+ *
762
+ * @throws {OmsError} `404 "Jam not found"` (also when you are not a member).
763
+ * @throws {OmsError} `400 "The host picks the music in this jam"` when
764
+ * `queue_mode` is `"host"` and you are not the host.
765
+ * @throws {OmsError} `404 "Song not found"` - you do not own it, or it has no
766
+ * media attached.
767
+ * @throws {OmsError} `400 "The host is not playing right now"` when the host
768
+ * has no active playback device.
769
+ */
770
+ propose(jamId: JamId, songId: SongId, options?: RequestOptions): Promise<void>;
771
+ /**
772
+ * `POST /jams/:id/skip_vote` - votes to skip whatever is playing.
773
+ *
774
+ * Votes are a set of user ids in a 15-minute cache entry keyed by jam AND by
775
+ * the CURRENT song, so voting twice is idempotent and **a track change resets
776
+ * the tally silently** - no message says so. Reset any local counter whenever
777
+ * the song id in the jam state changes.
778
+ *
779
+ * The threshold is `1` under `"anyone"` and `floor(members / 2) + 1` under
780
+ * `"majority"` ({@link jamSkipVotesNeeded} computes it from a jam you already
781
+ * hold). A vote from the HOST always passes immediately, whatever the mode
782
+ * and whatever the count.
783
+ *
784
+ * When it passes, the server sends `next` to the host's active device and
785
+ * broadcasts `skipped`; the actual skip then depends on the host's client
786
+ * acting, exactly as in {@link propose}.
787
+ *
788
+ * @throws {OmsError} `404 "Jam not found"` (also when you are not a member).
789
+ * @throws {OmsError} `400 "Only the host can skip in this jam"` under
790
+ * `skip_mode: "host"`.
791
+ * @throws {OmsError} `400 "Nothing is playing"` when the host has no current
792
+ * song or no active device.
793
+ */
794
+ skipVote(jamId: JamId, options?: RequestOptions): Promise<JamSkipVoteResult>;
795
+ }
796
+ /** The music section of a user's public profile. */
797
+ export declare class MusicProfilesNamespace extends Resource {
798
+ /**
799
+ * `GET /users/:idOrHandle/music_profile` - now playing, 30-day tops, recent
800
+ * plays and a play count.
801
+ *
802
+ * The path segment accepts EITHER a user id or a handle; the server tries the
803
+ * id first and then the lowercased handle, so pass whatever you have. It is
804
+ * percent-encoded here, which matters for a handle more than for an id.
805
+ *
806
+ * **A `200` does not mean there is a profile.** A viewer who is not the owner
807
+ * and not an accepted friend with `share_listening` on gets
808
+ * `{ "visible": false }` at status `200`, on purpose: a private profile has to
809
+ * look identical to an empty one. Narrow with {@link isMusicProfileVisible}.
810
+ *
811
+ * Authentication is required even though it looks like a public read - the
812
+ * action is not on `UsersController`'s unauthenticated allowlist, so an
813
+ * anonymous call is a `401`, not a hidden profile.
814
+ *
815
+ * Every `*_url` in the answer is presigned and short-lived
816
+ * ({@link MUSIC_PRESIGNED_URL_TTL_MS}); use them verbatim, do not store them,
817
+ * and pick an artist image with {@link musicProfileArtistImage}.
818
+ *
819
+ * General ceiling: 600/min. Note this is a `/users/` path, NOT one of the
820
+ * `/artists/` family, so the tighter 60/min music bucket does not apply.
821
+ *
822
+ * @throws {OmsError} `404 "User not found."` when nobody matches.
823
+ */
824
+ get(idOrHandle: string, options?: RequestOptions): Promise<MusicProfile>;
825
+ }
826
+ /**
827
+ * The music storage meter. **Native app only** - the web frontend has no caller
828
+ * for this route.
829
+ */
830
+ export declare class MusicStorageNamespace extends Resource {
831
+ /**
832
+ * `GET /music/storage` - bytes of music media stored, against the ceiling.
833
+ *
834
+ * Read {@link MusicStorageUsage.unlimited} before doing arithmetic with
835
+ * `limit_bytes`, which is `null` for an unlimited account. See the interface
836
+ * for the whole story.
837
+ *
838
+ * `used_bytes` is a live `SUM` over the account's distinct blobs, so it is
839
+ * never stale and never free. A settings screen, not a poll.
840
+ *
841
+ * General ceiling: 600/min - this route is NOT covered by the 30/min bucket
842
+ * on `GET /quotas`, even though the two report the same number.
843
+ */
844
+ get(options?: RequestOptions): Promise<MusicStorageUsage>;
845
+ }
846
+ /**
847
+ * Stored assistant sessions: list, reopen, delete. **Native app only.**
848
+ *
849
+ * Reading and deleting live here; WRITING does not. A message is appended by
850
+ * {@link MusicAssistantNamespace.send}, because the only path that may add to a
851
+ * chat is the one that talks to the model.
852
+ */
853
+ export declare class MusicAssistantChatsNamespace extends Resource {
854
+ /**
855
+ * `GET /music_assistant/chats` - the caller's sessions, newest activity
856
+ * first, without their messages.
857
+ *
858
+ * A bare array with no paging and no filters: `AssistantChat` is not a CRUD
859
+ * resource, so `modifiers[page]` and `search[...]` are not read (and, unlike
860
+ * a real index, not rejected either - they are simply ignored). The list
861
+ * grows without bound; nothing prunes old chats.
862
+ *
863
+ * General ceiling: 600/min.
864
+ */
865
+ list(options?: RequestOptions): Promise<MusicAssistantChatSummary[]>;
866
+ /**
867
+ * `GET /music_assistant/chats/:id` - one session with its full transcript.
868
+ *
869
+ * @throws {OmsError} `404 "Chat not found"` for a chat that does not exist
870
+ * AND for one belonging to somebody else. The scope is applied before the
871
+ * lookup precisely so the two are indistinguishable; never expect a `403`.
872
+ */
873
+ get(chatId: MusicAssistantChatId, options?: RequestOptions): Promise<MusicAssistantChatDetail>;
874
+ /**
875
+ * `DELETE /music_assistant/chats/:id` - permanent, `204`, no body.
876
+ *
877
+ * Unlike the rest of this file, this one really is a `204`. A repeat delete
878
+ * is a `404`, which is why the transport does not replay a `DELETE` after a
879
+ * torn connection: it would turn a success into an error.
880
+ *
881
+ * @throws {OmsError} `404 "Chat not found"`, including for somebody else's.
882
+ */
883
+ delete(chatId: MusicAssistantChatId, options?: RequestOptions): Promise<void>;
884
+ }
885
+ /**
886
+ * "O Melhor Assistente": a chat that can search the library, build playlists
887
+ * and drive the player. **Native app only** among the shipped clients, but
888
+ * nothing about it is mobile-specific.
889
+ */
890
+ export declare class MusicAssistantNamespace extends Resource {
891
+ /** Stored sessions: list, reopen, delete. */
892
+ readonly chats: MusicAssistantChatsNamespace;
893
+ constructor(http: ConstructorParameters<typeof Resource>[0]);
894
+ /**
895
+ * `POST /music_assistant` in its PERSISTED mode - send one message, get one
896
+ * answer, and let the server keep the conversation.
897
+ *
898
+ * ## Send the new message only
899
+ *
900
+ * The history is the SERVER's. Omit `chatId` for a new session and the answer
901
+ * carries the `chat_id` that was created; pass it back on every later turn.
902
+ * Do not send a transcript - the model is fed the stored history (the last
903
+ * {@link MUSIC_ASSISTANT_HISTORY_TURNS} turns of it) and anything you resend
904
+ * is simply a second copy of a message.
905
+ *
906
+ * ## Nothing is stored until the model answers
907
+ *
908
+ * The controller runs the generation FIRST and only then creates the chat and
909
+ * appends both messages. So a `502` leaves the chat exactly as it was - no
910
+ * dangling user message, no empty chat on a failed first turn, and a resend
911
+ * that cannot duplicate. That is also why a failure gives you no `chat_id` to
912
+ * continue from.
913
+ *
914
+ * ## Two failure modes that are not network errors
915
+ *
916
+ * - `423 "Este chat é só de leitura."` - the chat has been idle for two days
917
+ * and is sealed. Open a new one (call again with no `chatId`); there is no
918
+ * unseal. The `read_only` flag is computed, so a cached summary can say
919
+ * `false` and this still fire.
920
+ * - `502` - OpenRouter refused or fell over. The reply is not partial, it is
921
+ * absent.
922
+ *
923
+ * A body over {@link MUSIC_ASSISTANT_MAX_BODY_BYTES} is `413 "Request too
924
+ * big"`, decided from `Content-Length` before parsing.
925
+ *
926
+ * The deadline defaults to {@link MUSIC_ASSISTANT_TIMEOUT_MS} rather than the
927
+ * client's 60 s, because a generation regularly outlives 60 s while still
928
+ * being on its way. Note that the transport does not replay a `POST`, so a
929
+ * timeout here is genuinely unknown ground: the server may well have
930
+ * finished and stored the turn. Reload with {@link MusicAssistantChatsNamespace.get}
931
+ * before resending.
932
+ *
933
+ * General ceiling: 600/min, and one generation holds a Puma thread for its
934
+ * whole duration - do not fan these out.
935
+ */
936
+ send(input: SendMusicAssistantMessageInput, options?: RequestOptions): Promise<MusicAssistantAnswer>;
937
+ /**
938
+ * `POST /music_assistant` in its STATELESS mode - you own the history, the
939
+ * server stores nothing.
940
+ *
941
+ * This is the older contract, kept alive for the app builds already in
942
+ * people's hands, and it is the right one for a CLI or an MCP host that has
943
+ * no place to keep a `chat_id` between invocations. The whole transcript
944
+ * rides in the request every time, so it grows, and the body ceiling
945
+ * ({@link MUSIC_ASSISTANT_MAX_BODY_BYTES}) is a real limit rather than a
946
+ * theoretical one. Only the last {@link MUSIC_ASSISTANT_HISTORY_TURNS}
947
+ * messages reach the model whatever you send.
948
+ *
949
+ * The answer never carries a `chat_id`, and the two modes do not mix: a
950
+ * request with both `messages` and `message` takes the persisted branch and
951
+ * ignores `messages` entirely.
952
+ *
953
+ * The side effects are NOT stateless. A `tools` turn writes real playlists
954
+ * into the library, so this is not a "read-only" mode - only a "no
955
+ * transcript" one.
956
+ *
957
+ * @throws {OmsError} `400 "messages required"` when the array is empty.
958
+ */
959
+ ask(messages: MusicAssistantMessage[], player?: MusicAssistantPlayerContext, options?: RequestOptions): Promise<MusicAssistantAnswer>;
960
+ }
961
+ /**
962
+ * "O Melhor DJ": a written-and-spoken link between tracks, and a whole planned
963
+ * set. **Native app only** among the shipped clients.
964
+ *
965
+ * Both methods pass `retry: false` by default. That is not caution about
966
+ * duplicates - the transport does not replay a `POST` anyway - it is about the
967
+ * one thing it DOES replay: a `429`. The hourly cap here is a cache counter the
968
+ * controller increments on every request including the refused ones, so waiting
969
+ * out a `Retry-After` and asking again pushes the count further past the cap
970
+ * and cannot succeed inside the hour. Pass `retry: {}` to opt back in if you
971
+ * are sure the `429` came from `Rack::Attack` instead.
972
+ */
973
+ export declare class MusicDjNamespace extends Resource {
974
+ /**
975
+ * `POST /music_dj` - the DJ introduces the next track.
976
+ *
977
+ * Returns the script AND the spoken audio in one answer, base64 in the JSON
978
+ * body rather than as a URL, because the clip is small and ephemeral and
979
+ * nothing stores it. Decode with {@link musicDjAudioBytes}, or hand
980
+ * {@link musicDjAudioDataUrl} to a player that takes a URI.
981
+ *
982
+ * Both ids are resolved through `viewable_by`, so a followed playlist's track
983
+ * works and a stranger's does not. `previousSongId` is genuinely optional and
984
+ * is what lets the script say goodbye to the outgoing track.
985
+ *
986
+ * Generation is a free-tier LLM call followed by local text-to-speech, so it
987
+ * takes seconds; the deadline defaults to {@link MUSIC_DJ_TIMEOUT_MS}. The
988
+ * intended pattern is to ask for the clip while the current track is still
989
+ * playing and drop it on the boundary, ideally over the outgoing
990
+ * instrumental.
991
+ *
992
+ * @throws {OmsError} `401 "Session required"` when unauthenticated.
993
+ * @throws {OmsError} `404 "Song not found"` for either id.
994
+ * @throws {OmsError} `429 "DJ limit reached, try again later"` past
995
+ * {@link MUSIC_DJ_HOURLY_CAP}.
996
+ * @throws {OmsError} `502 "DJ is unavailable right now"` when the script
997
+ * failed, `503 "DJ voice is unavailable right now"` when the voice did.
998
+ * The split is deliberate: a `503` means the words exist but nothing can
999
+ * say them.
1000
+ */
1001
+ interstitial(input: {
1002
+ readonly nextSongId: SongId;
1003
+ readonly previousSongId?: SongId | null;
1004
+ }, options?: RequestOptions): Promise<MusicDjInterstitial>;
1005
+ /**
1006
+ * `POST /music_dj/batch` - a whole set: what to play next AND the words
1007
+ * introducing it, from one model call.
1008
+ *
1009
+ * The intended cadence is a real station's: take the set, play it, and come
1010
+ * back when about two tracks remain. Every field is optional, so
1011
+ * `batch({})` is a valid cold start.
1012
+ *
1013
+ * `recentSongIds` is a filter, not just a hint - the planner subtracts those
1014
+ * ids from its own picks. Send a list covering the whole library and the
1015
+ * planner has nothing left, which surfaces as `502 "DJ is unavailable right
1016
+ * now"` rather than as an empty set. Keep it to a genuine recent window; only
1017
+ * the last 60 are read anyway.
1018
+ *
1019
+ * `songs` comes back in play order, in the full `GET /songs` shape, already
1020
+ * scoped to what the caller may play. It is never empty.
1021
+ *
1022
+ * Shares {@link MUSIC_DJ_HOURLY_CAP} with {@link interstitial}. Same
1023
+ * timeouts, same error shapes.
1024
+ */
1025
+ batch(input?: MusicDjBatchInput, options?: RequestOptions): Promise<MusicDjBatch>;
1026
+ }
1027
+ /**
1028
+ * The `music.social` entry point, holding the five families as sub-namespaces.
1029
+ *
1030
+ * Each is also exported on its own, so a host that would rather mount
1031
+ * `oms.music.jams` or `oms.assistant` can do that instead of reaching through
1032
+ * this class.
1033
+ */
1034
+ export declare class MusicSocialNamespace extends Resource {
1035
+ /** Shared listening sessions. HTTP half only - the rest is on the cable. */
1036
+ readonly jams: MusicJamsNamespace;
1037
+ /** The music card on somebody's profile. */
1038
+ readonly profiles: MusicProfilesNamespace;
1039
+ /** Music bytes stored against the account's ceiling. Native app only. */
1040
+ readonly storage: MusicStorageNamespace;
1041
+ /** The chat assistant, with its stored sessions. Native app only. */
1042
+ readonly assistant: MusicAssistantNamespace;
1043
+ /** Scripted and spoken links between tracks. Native app only. */
1044
+ readonly dj: MusicDjNamespace;
1045
+ constructor(http: ConstructorParameters<typeof Resource>[0]);
1046
+ }
1047
+ /**
1048
+ * The ActionCable stream a jam broadcasts on (`Jam.stream_for`).
1049
+ *
1050
+ * This SDK does not open it - it has no cable client - but the name is part of
1051
+ * the contract and hard-coding `` `jam:${id}` `` in three clients is how it
1052
+ * drifts. Subscribe with the identifier
1053
+ * `{"channel":"JamChannel","id":<jam id>}` AFTER
1054
+ * {@link MusicJamsNamespace.join} has returned, and treat the subscription as
1055
+ * receive-only: `JamChannel` has no client actions.
1056
+ */
1057
+ export declare function jamStreamName(jamId: JamId): string;
1058
+ /**
1059
+ * The host's member row, or `undefined` when the payload is inconsistent.
1060
+ *
1061
+ * Reads `host_id` rather than trusting `is_host` or the array order. Both are
1062
+ * correct today; only `host_id` is the source of truth.
1063
+ */
1064
+ export declare function jamHost(jam: Jam): JamMember | undefined;
1065
+ /** True when `userId` hosts this jam. The gate on rules, ending, and skipping. */
1066
+ export declare function isJamHost(jam: Jam, userId: Id): boolean;
1067
+ /** That user's member row, or `undefined` when they are not in the jam. */
1068
+ export declare function jamMember(jam: Jam, userId: Id): JamMember | undefined;
1069
+ /**
1070
+ * How many votes a skip needs right now: `1` under `"anyone"`,
1071
+ * `floor(members / 2) + 1` otherwise.
1072
+ *
1073
+ * The same arithmetic the server does, so a client can render "2 of 3" before
1074
+ * anybody votes rather than waiting for the first result to learn the
1075
+ * threshold. It moves as people join and leave, and `"host"` returns the
1076
+ * majority number even though no vote can pass under it - the host skips in
1077
+ * their own player instead.
1078
+ */
1079
+ export declare function jamSkipVotesNeeded(jam: Jam): number;
1080
+ /** Narrows a {@link MusicProfile} to the variant that has any content. */
1081
+ export declare function isMusicProfileVisible(profile: MusicProfile): profile is MusicProfileVisible;
1082
+ /**
1083
+ * Picks the best available image for a profile's top artist, in the order both
1084
+ * shipped clients use: the owner's upload, then the large Deezer sizes, then
1085
+ * the small ones, then Last.fm. `undefined` when there is nothing, which is
1086
+ * common - render initials.
1087
+ *
1088
+ * Note that `picture_big` deliberately comes before `picture_xl`: `xl` is a
1089
+ * 1000px square and these render at avatar size.
1090
+ */
1091
+ export declare function musicProfileArtistImage(artist: MusicProfileArtist): string | undefined;
1092
+ /**
1093
+ * Bytes still available, or `null` when the account is unlimited.
1094
+ *
1095
+ * `null` means "no ceiling", never "zero" - the distinction the raw
1096
+ * `limit_bytes` makes so easy to lose. Clamped at zero, because an account
1097
+ * whose limit was lowered below its usage is over, not negative.
1098
+ */
1099
+ export declare function musicStorageRemaining(usage: MusicStorageUsage): number | null;
1100
+ /**
1101
+ * Whether `bytes` more would fit. Always true for an unlimited account.
1102
+ *
1103
+ * Worth calling before an upload: the music quota is checked server-side under
1104
+ * an advisory lock at attach time, and failing there means the bytes have
1105
+ * already crossed the network.
1106
+ */
1107
+ export declare function musicStorageAffords(usage: MusicStorageUsage, bytes: number): boolean;
1108
+ /**
1109
+ * Decodes a DJ clip's `audio_base64` into bytes.
1110
+ *
1111
+ * Uses the platform's `atob` when there is one and falls back to a arithmetic
1112
+ * decode when there is not, because this has to work in all three clients and
1113
+ * `atob` is the kind of global that is present in a browser and in Bun, arrived
1114
+ * in React Native only recently, and is not guaranteed in a Worker-class
1115
+ * isolate. No `Buffer`, no `node:*`.
1116
+ *
1117
+ * Padding, whitespace and newlines are tolerated; anything outside the base64
1118
+ * alphabet is skipped rather than throwing, because a clip that decodes to
1119
+ * slightly short audio is a better failure than one that throws inside a
1120
+ * playback callback.
1121
+ */
1122
+ export declare function musicDjAudioBytes(clip: Pick<MusicDjInterstitial, "audio_base64">): Uint8Array;
1123
+ /**
1124
+ * Wraps a DJ clip as a `data:` URI, for a player that takes a URI rather than
1125
+ * bytes - which is most of them, `expo-audio` included.
1126
+ *
1127
+ * The string is roughly a third larger than the audio, and the audio is already
1128
+ * in memory, so this is cheap in every sense that matters at this size. It does
1129
+ * NOT work as an `<a download>` target inside a published artifact, and it is
1130
+ * not a URL anything can fetch twice - it is the bytes, spelled differently.
1131
+ */
1132
+ export declare function musicDjAudioDataUrl(clip: MusicDjInterstitial): string;