@omelhorsite/sdk 0.2.0 → 0.3.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/dist/index.js +4939 -552
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +4 -1
- package/dist/types/resources/account.d.ts +66 -3
- package/dist/types/resources/admin.d.ts +1837 -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 +847 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content.d.ts +2970 -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 +62 -21
- package/dist/types/resources/library.d.ts +1435 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies.d.ts +1186 -0
- package/dist/types/resources/music/artists.d.ts +1066 -0
- package/dist/types/resources/music/imports.d.ts +940 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1026 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1183 -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 +45 -4
- package/dist/types/resources/social.d.ts +1330 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +88 -22
- package/dist/types/resources/tickets.d.ts +82 -3
- 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,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;
|