@omelhorsite/sdk 0.12.2 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11076,6 +11076,12 @@ class AuthSessionsNamespace extends Resource {
11076
11076
  async changeEmailStart(newEmail, options = {}) {
11077
11077
  return this.http.post("/users/update_email_start", { email: newEmail }, options);
11078
11078
  }
11079
+ async verifyEmailStart(options = {}) {
11080
+ return this.http.post("/users/verify_email_start", undefined, options);
11081
+ }
11082
+ async verifyEmailComplete(code, options = {}) {
11083
+ return this.http.post("/users/verify_email_end", { code }, options);
11084
+ }
11079
11085
  async changeEmailComplete(input, options = {}) {
11080
11086
  return this.http.post("/users/update_email_end", {
11081
11087
  email: input.email,
@@ -13515,9 +13521,8 @@ var MUSIC_ASSISTANT_MAX_ACTIONS = 10;
13515
13521
  var MUSIC_ASSISTANT_MAX_BODY_BYTES = 256 * 1024;
13516
13522
  var MUSIC_ASSISTANT_READ_ONLY_AFTER_MS = 2 * 24 * 60 * 60 * 1000;
13517
13523
  var MUSIC_ASSISTANT_TIMEOUT_MS = 90000;
13518
- var MUSIC_DJ_HOURLY_CAP = 40;
13524
+ var MUSIC_DJ_HOURLY_CAP = 90;
13519
13525
  var MUSIC_DJ_TIMEOUT_MS = 120000;
13520
- var MUSIC_DJ_BATCH_SIZE = 4;
13521
13526
 
13522
13527
  class MusicJamsNamespace extends Resource {
13523
13528
  async list(options = {}) {
@@ -13608,33 +13613,31 @@ class MusicAssistantNamespace extends Resource {
13608
13613
  }
13609
13614
 
13610
13615
  class MusicDjNamespace extends Resource {
13611
- interstitial(input, options = {}) {
13612
- const body = { next_song_id: input.nextSongId };
13613
- if (input.previousSongId !== undefined && input.previousSongId !== null) {
13614
- body["previous_song_id"] = input.previousSongId;
13615
- }
13616
- return this.http.post("/music_dj", body, {
13617
- ...options,
13618
- timeoutMs: options.timeoutMs ?? MUSIC_DJ_TIMEOUT_MS,
13619
- retry: options.retry ?? false
13620
- });
13621
- }
13622
- batch(input = {}, options = {}) {
13616
+ next(input = {}, options = {}) {
13623
13617
  const body = {};
13618
+ if (input.sessionId !== undefined)
13619
+ body["session_id"] = input.sessionId;
13624
13620
  if (input.request !== undefined)
13625
13621
  body["request"] = input.request;
13626
- if (input.recentSongIds !== undefined)
13627
- body["recent_song_ids"] = input.recentSongIds;
13628
- if (input.skippedSongIds !== undefined)
13629
- body["skipped_song_ids"] = input.skippedSongIds;
13630
- if (input.batchIndex !== undefined)
13631
- body["batch_index"] = input.batchIndex;
13632
- return this.http.post("/music_dj/batch", body, {
13622
+ if (input.skippedSongId !== undefined)
13623
+ body["skipped_song_id"] = input.skippedSongId;
13624
+ if (input.speak !== undefined)
13625
+ body["speak"] = input.speak;
13626
+ if (input.restart !== undefined)
13627
+ body["restart"] = input.restart;
13628
+ return this.http.post("/music_dj/next", body, {
13633
13629
  ...options,
13634
13630
  timeoutMs: options.timeoutMs ?? MUSIC_DJ_TIMEOUT_MS,
13635
13631
  retry: options.retry ?? false
13636
13632
  });
13637
13633
  }
13634
+ async session(options = {}) {
13635
+ const body = await this.http.get("/music_dj/session", options);
13636
+ return body.session ?? null;
13637
+ }
13638
+ async end(options = {}) {
13639
+ await this.http.delete("/music_dj/session", options);
13640
+ }
13638
13641
  }
13639
13642
 
13640
13643
  class MusicSocialNamespace extends Resource {
@@ -13737,7 +13740,8 @@ var SONG_FILTER_COLUMNS = Object.freeze([
13737
13740
  "album",
13738
13741
  "position",
13739
13742
  "year",
13740
- "artist"
13743
+ "artist",
13744
+ "language"
13741
13745
  ]);
13742
13746
  var SONG_ORDER_COLUMNS = Object.freeze([
13743
13747
  "id",
@@ -13868,6 +13872,13 @@ class MusicSongsNamespace extends Resource {
13868
13872
  async deleteSeparation(id, options = {}) {
13869
13873
  await this.http.delete(`/songs/${encodeURIComponent(String(id))}/separation`, options);
13870
13874
  }
13875
+ async matchCandidates(id, params = {}, options = {}) {
13876
+ const body = await this.http.get(`/songs/${encodeURIComponent(String(id))}/match_candidates`, { ...options, query: { limit: params.limit } });
13877
+ return body.items ?? [];
13878
+ }
13879
+ async rematch(id, input = {}, options = {}) {
13880
+ return this.http.post(`/songs/${encodeURIComponent(String(id))}/rematch`, input.sourceUrl === undefined ? {} : { source_url: input.sourceUrl }, options);
13881
+ }
13871
13882
  async listLiked(params = {}, options = {}) {
13872
13883
  const before = params.before instanceof Date ? params.before.toISOString() : params.before;
13873
13884
  const rows = await this.http.get("/liked_songs", {
@@ -13925,7 +13936,13 @@ class MusicSongsNamespace extends Resource {
13925
13936
  return listQuery(params, at, {
13926
13937
  order: at === undefined ? undefined : "created_at:asc",
13927
13938
  search: { title: params.title },
13928
- exactSearch: { album: params.album, year: params.year, id: params.ids, artist: params.artist },
13939
+ exactSearch: {
13940
+ album: params.album,
13941
+ year: params.year,
13942
+ id: params.ids,
13943
+ artist: params.artist,
13944
+ language: params.language
13945
+ },
13929
13946
  top: params.artistRole === undefined ? {} : { artist_role: params.artistRole }
13930
13947
  });
13931
13948
  }
@@ -16270,7 +16287,6 @@ export {
16270
16287
  MUSIC_PRESIGNED_URL_TTL_MS,
16271
16288
  MUSIC_DJ_TIMEOUT_MS,
16272
16289
  MUSIC_DJ_HOURLY_CAP,
16273
- MUSIC_DJ_BATCH_SIZE,
16274
16290
  MUSIC_ASSISTANT_TIMEOUT_MS,
16275
16291
  MUSIC_ASSISTANT_READ_ONLY_AFTER_MS,
16276
16292
  MUSIC_ASSISTANT_MAX_BODY_BYTES,
@@ -30,6 +30,13 @@ export interface User extends BaseRecord {
30
30
  readonly country_code?: string | null;
31
31
  /** `en`, `pt` or `lv`: the language of the emails sent to this account. Own account only. */
32
32
  readonly language?: string | null;
33
+ /**
34
+ * Whether the address on the account was proven by an emailed code: the same
35
+ * fact the id token's `email_verified` carries. Own account only. `false`
36
+ * on accounts older than the emailed sign-up code until they confirm once
37
+ * with `sessions.verifyEmailStart` / `verifyEmailComplete`.
38
+ */
39
+ readonly email_verified?: boolean;
33
40
  readonly email_is_public?: boolean;
34
41
  readonly gender_is_public?: boolean;
35
42
  readonly library_public?: boolean;
@@ -692,6 +692,26 @@ export declare class AuthSessionsNamespace extends Resource {
692
692
  * rejected at write time - already taken, malformed. Both codes are
693
693
  * consumed by then and the flow restarts at {@link changeEmailStart}.
694
694
  */
695
+ /**
696
+ * `POST /users/verify_email_start` - emails a code to the address already on
697
+ * the account, so an account that never proved its mailbox (one older than
698
+ * the emailed sign-up code) can. Requires a live session; an account that is
699
+ * already verified answers `200` without sending anything.
700
+ *
701
+ * Same shared `*_start` throttle as the other code-issuing calls.
702
+ *
703
+ * @throws {OmsAuthError} 401 without a live session.
704
+ */
705
+ verifyEmailStart(options?: RequestOptions): Promise<string>;
706
+ /**
707
+ * `POST /users/verify_email_end` - presents the code; answers `200` with the
708
+ * updated {@link User}, `email_verified` now `true`.
709
+ *
710
+ * @throws {OmsAuthError} 401 without a live session.
711
+ * @throws {OmsApiError} 404 `"Invalid Verification"` when the code is wrong,
712
+ * expired or burned.
713
+ */
714
+ verifyEmailComplete(code: string, options?: RequestOptions): Promise<User>;
695
715
  changeEmailComplete(input: ChangeEmailInput, options?: RequestOptions): Promise<User>;
696
716
  /**
697
717
  * `POST /users/destroy_start` - emails a deletion code to the address on the
@@ -501,57 +501,99 @@ export interface SendMusicAssistantMessageInput {
501
501
  readonly player?: MusicAssistantPlayerContext;
502
502
  }
503
503
  /**
504
- * Generations per user per hour, shared by `/music_dj` and `/music_dj/batch`.
504
+ * Turns per user per hour on `POST /music_dj/next`.
505
505
  *
506
- * The counter is incremented by EVERY request - including the ones it then
507
- * refuses with `429`. A retry loop therefore drives the count further past the
508
- * cap and can never recover inside the hour, which is why
509
- * {@link MusicDjNamespace} passes `retry: false` unless you override it.
506
+ * A turn is one song, so an hour of listening spends about 17 of them and the
507
+ * rest of the budget absorbs skipping. The counter is incremented by EVERY
508
+ * request - including the ones it then refuses with `429` - so a retry loop
509
+ * drives the count further past the cap and can never recover inside the hour,
510
+ * which is why {@link MusicDjNamespace} passes `retry: false` unless you
511
+ * override it.
510
512
  */
511
- export declare const MUSIC_DJ_HOURLY_CAP = 40;
513
+ export declare const MUSIC_DJ_HOURLY_CAP = 90;
512
514
  /**
513
- * Default deadline for one DJ generation, in milliseconds. Writing the script
514
- * and speaking it takes well past the client's 60 s default.
515
+ * Default deadline for one DJ turn, in milliseconds. Choosing the song and
516
+ * speaking it takes well past the client's 60 s default.
515
517
  */
516
518
  export declare const MUSIC_DJ_TIMEOUT_MS = 120000;
517
- /** Songs one `/music_dj/batch` set plans at most. */
518
- export declare const MUSIC_DJ_BATCH_SIZE = 4;
519
- /** `POST /music_dj`: one spoken link between two tracks. */
520
- export interface MusicDjInterstitial {
521
- /** The script as text, at most 320 characters. Worth showing while audio loads. */
522
- readonly text: string;
519
+ /** One entry in a DJ session's transcript. */
520
+ export interface MusicDjTurn {
521
+ /** `"dj"` when he played or spoke, `"listener"` when they asked for something. */
522
+ readonly role: "dj" | "listener";
523
523
  /**
524
- * The same script spoken, base64, NOT a data URL. Decode with
525
- * {@link musicDjAudioBytes} or wrap with {@link musicDjAudioDataUrl}.
524
+ * What was said. `null` on a DJ turn where he stayed quiet - he speaks on
525
+ * the first song, whenever the listener asks for something, and every third
526
+ * song after that, so most turns carry a song and no words.
526
527
  */
527
- readonly audio_base64: string;
528
- /** Container of the decoded bytes. `"wav"` today, and typed wide on purpose. */
529
- readonly format: string;
528
+ readonly text: string | null;
529
+ /** The song that turn played, when it played one. */
530
+ readonly song_id: number | null;
531
+ readonly created_at: Timestamp;
530
532
  }
531
- /** `POST /music_dj/batch`: a whole set - what to play next, and the words for it. */
532
- export interface MusicDjBatch extends MusicDjInterstitial {
533
+ /** `GET /music_dj/session`: the session still on air, if there is one. */
534
+ export interface MusicDjSession {
535
+ readonly id: number;
536
+ /** The listener's standing request. It holds until they ask for something else. */
537
+ readonly request: string | null;
538
+ readonly turns: MusicDjTurn[];
533
539
  /**
534
- * The planned tracks, in play order, in the full `GET /songs` shape and
535
- * already scoped to what the caller may play. Between 1 and
536
- * {@link MUSIC_DJ_BATCH_SIZE}: the server fails rather than answer with an
537
- * empty set, so this is never `[]`.
540
+ * Every song the transcript refers to, in the full `GET /songs` shape, so a
541
+ * client can rebuild the session without a lookup per turn. Unordered: match
542
+ * them to {@link MusicDjTurn.song_id}.
538
543
  */
539
544
  readonly songs: Song[];
540
545
  }
541
- /** Everything `POST /music_dj/batch` accepts. All of it optional. */
542
- export interface MusicDjBatchInput {
543
- /** A free-text steer ("something calmer"). Truncated to 300 characters. */
546
+ /** `POST /music_dj/next`: the next song, and the words for it when there are any. */
547
+ export interface MusicDjNext {
548
+ readonly session_id: number;
549
+ /** What to play now. Never one the session already played. */
550
+ readonly song: Song;
551
+ /** Two to five words naming this stretch of the show, for a screen. */
552
+ readonly theme: string | null;
553
+ /**
554
+ * The spoken link, present ONLY on the turns where he speaks. Its absence is
555
+ * the normal case, not an error: play the song straight away.
556
+ */
557
+ readonly text?: string;
558
+ /**
559
+ * The same words spoken, base64, NOT a data URL. Decode with
560
+ * {@link musicDjAudioBytes} or wrap with {@link musicDjAudioDataUrl}.
561
+ *
562
+ * Absent when he stayed quiet AND when the voice service failed while he had
563
+ * something to say - in that second case `text` is there without it, and the
564
+ * song still plays. Never block playback on this field.
565
+ */
566
+ readonly audio_base64?: string;
567
+ /** Container of the decoded bytes. `"wav"` today, and typed wide on purpose. */
568
+ readonly format?: string;
569
+ }
570
+ /** Everything `POST /music_dj/next` accepts. All of it optional. */
571
+ export interface MusicDjNextInput {
572
+ /**
573
+ * Continue this session. Omit to continue the caller's own session, which
574
+ * the server finds by itself - a session goes stale after two hours of
575
+ * silence and the next turn starts a fresh one.
576
+ */
577
+ readonly sessionId?: number;
578
+ /**
579
+ * A steer ("something calmer", "fados"), truncated to 300 characters.
580
+ *
581
+ * STANDING ORDERS, not a one-off: it holds for every following turn until
582
+ * another request replaces it, and it makes him speak on this turn even if
583
+ * it was not his turn to.
584
+ */
544
585
  readonly request?: string;
586
+ /** The song the listener just skipped, as a direction to leave. */
587
+ readonly skippedSongId?: SongId;
545
588
  /**
546
- * Recently played ids. Only the last 60 are read, and the planner SUBTRACTS
547
- * them from its own picks - so a list that covers the whole library leaves
548
- * nothing playable and the call fails with a `502`.
589
+ * Make him speak on this turn even if it was not his turn to.
590
+ *
591
+ * This is the DJ button: the listener wants to hear him now. It changes
592
+ * nothing about which song comes next.
549
593
  */
550
- readonly recentSongIds?: SongId[];
551
- /** Ids the listener skipped, as negative signal. Only the last 20 are read. */
552
- readonly skippedSongIds?: SongId[];
553
- /** Which set of the session this is, so the script can vary its opening. */
554
- readonly batchIndex?: number;
594
+ readonly speak?: boolean;
595
+ /** Start a new session even though one is still on air. */
596
+ readonly restart?: boolean;
555
597
  }
556
598
  /**
557
599
  * Jams over HTTP. The realtime half lives on the WebSocket stream and is not in
@@ -887,12 +929,16 @@ export declare class MusicAssistantNamespace extends Resource {
887
929
  ask(messages: MusicAssistantMessage[], player?: MusicAssistantPlayerContext, options?: RequestOptions): Promise<MusicAssistantAnswer>;
888
930
  }
889
931
  /**
890
- * "O Melhor DJ": a written-and-spoken link between tracks, and a whole planned
891
- * set.
932
+ * "O Melhor DJ": a radio session for one listener, one song at a time.
933
+ *
934
+ * The server holds the session - what played, what was skipped, what he said,
935
+ * what the listener asked for - so a client only ever says what just happened
936
+ * and gets the next song back. Ask for one when the current song ends, when
937
+ * the listener skips, or when they want something else.
892
938
  *
893
- * Both methods pass `retry: false` by default. That is not caution about
939
+ * {@link next} passes `retry: false` by default. That is not caution about
894
940
  * duplicates - the transport does not replay a `POST` anyway - it is about the
895
- * one thing it DOES replay: a `429`. The hourly cap here counts every request
941
+ * one thing it DOES replay: a `429`. The hourly cap counts every request
896
942
  * including the refused ones, so waiting out a `Retry-After` and asking again
897
943
  * pushes the count further past the cap and cannot succeed inside the hour.
898
944
  * Pass `retry: {}` to opt back in if you are sure the `429` came from the
@@ -900,57 +946,46 @@ export declare class MusicAssistantNamespace extends Resource {
900
946
  */
901
947
  export declare class MusicDjNamespace extends Resource {
902
948
  /**
903
- * `POST /music_dj` - the DJ introduces the next track.
949
+ * `POST /music_dj/next` - the next song, chosen with the whole session in
950
+ * view.
904
951
  *
905
- * Returns the script AND the spoken audio in one answer, base64 in the JSON
906
- * body rather than as a URL, because the clip is small and ephemeral and
907
- * nothing stores it. Decode with {@link musicDjAudioBytes}, or hand
908
- * {@link musicDjAudioDataUrl} to a player that takes a URI.
952
+ * `next({})` is a valid cold start: with no session on air the server opens
953
+ * one. The answer always carries a `song`; `text` and `audio_base64` come
954
+ * only on the turns where he speaks (the first song, after a request, and
955
+ * every third song), so treat their absence as normal and play the song.
909
956
  *
910
- * Both ids are resolved against what the caller may play, so a followed
911
- * playlist's track works and a stranger's does not. `previousSongId` is genuinely optional and
912
- * is what lets the script say goodbye to the outgoing track.
957
+ * A song the session already played is never returned, which is what lets a
958
+ * client keep the whole session in its queue and walk back through it.
913
959
  *
914
- * Generation takes seconds; the deadline defaults to
915
- * {@link MUSIC_DJ_TIMEOUT_MS}. The
916
- * intended pattern is to ask for the clip while the current track is still
917
- * playing and drop it on the boundary, ideally over the outgoing
918
- * instrumental.
960
+ * Choosing takes seconds and speaking takes more; the deadline defaults to
961
+ * {@link MUSIC_DJ_TIMEOUT_MS}. Ask for the next song while the current one
962
+ * is still playing, or the listener hears the gap.
919
963
  *
920
964
  * @throws {OmsError} `401 "Session required"` when unauthenticated.
921
- * @throws {OmsError} `404 "Song not found"` for either id.
922
965
  * @throws {OmsError} `429 "DJ limit reached, try again later"` past
923
966
  * {@link MUSIC_DJ_HOURLY_CAP}.
924
- * @throws {OmsError} `502 "DJ is unavailable right now"` when the script
925
- * failed, `503 "DJ voice is unavailable right now"` when the voice did.
926
- * The split is deliberate: a `503` means the words exist but nothing can
927
- * say them.
967
+ * @throws {OmsError} `502 "DJ is unavailable right now"` when no song could
968
+ * be chosen - including the honest case where the library has nothing left
969
+ * that the session has not already played.
928
970
  */
929
- interstitial(input: {
930
- readonly nextSongId: SongId;
931
- readonly previousSongId?: SongId | null;
932
- }, options?: RequestOptions): Promise<MusicDjInterstitial>;
971
+ next(input?: MusicDjNextInput, options?: RequestOptions): Promise<MusicDjNext>;
933
972
  /**
934
- * `POST /music_dj/batch` - a whole set: what to play next AND the words
935
- * introducing it, from one model call.
973
+ * `GET /music_dj/session` - the session still on air, or `null`.
936
974
  *
937
- * The intended cadence is a real station's: take the set, play it, and come
938
- * back when about two tracks remain. Every field is optional, so
939
- * `batch({})` is a valid cold start.
940
- *
941
- * `recentSongIds` is a filter, not just a hint - the planner subtracts those
942
- * ids from its own picks. Send a list covering the whole library and the
943
- * planner has nothing left, which surfaces as `502 "DJ is unavailable right
944
- * now"` rather than as an empty set. Keep it to a genuine recent window; only
945
- * the last 60 are read anyway.
946
- *
947
- * `songs` comes back in play order, in the full `GET /songs` shape, already
948
- * scoped to what the caller may play. It is never empty.
975
+ * `null` is the ordinary answer for someone who has not listened today: a
976
+ * session goes stale after two hours of silence and stops being served here.
977
+ * Use it to rebuild the transcript when a screen opens, not to decide
978
+ * whether {@link next} may be called - it always may.
979
+ */
980
+ session(options?: RequestOptions): Promise<MusicDjSession | null>;
981
+ /**
982
+ * `DELETE /music_dj/session` - end the session on air.
949
983
  *
950
- * Shares {@link MUSIC_DJ_HOURLY_CAP} with {@link interstitial}. Same
951
- * timeouts, same error shapes.
984
+ * Idempotent: with nothing on air it still answers `204`. The next
985
+ * {@link next} starts a session with no memory of this one, which is the
986
+ * point - it is how a listener says "forget what I asked for".
952
987
  */
953
- batch(input?: MusicDjBatchInput, options?: RequestOptions): Promise<MusicDjBatch>;
988
+ end(options?: RequestOptions): Promise<void>;
954
989
  }
955
990
  /**
956
991
  * The `music.social` entry point, holding the five families as sub-namespaces.
@@ -1045,7 +1080,9 @@ export declare function musicStorageAffords(usage: MusicStorageUsage, bytes: num
1045
1080
  * slightly short audio is a better failure than one that throws inside a
1046
1081
  * playback callback.
1047
1082
  */
1048
- export declare function musicDjAudioBytes(clip: Pick<MusicDjInterstitial, "audio_base64">): Uint8Array;
1083
+ export declare function musicDjAudioBytes(clip: {
1084
+ readonly audio_base64: string;
1085
+ }): Uint8Array;
1049
1086
  /**
1050
1087
  * Wraps a DJ clip as a `data:` URI, for a player that takes a URI rather than
1051
1088
  * bytes - which is most of them.
@@ -1054,4 +1091,7 @@ export declare function musicDjAudioBytes(clip: Pick<MusicDjInterstitial, "audio
1054
1091
  * in memory, so this is cheap in every sense that matters at this size. It is
1055
1092
  * not a URL anything can fetch twice - it is the bytes, spelled differently.
1056
1093
  */
1057
- export declare function musicDjAudioDataUrl(clip: MusicDjInterstitial): string;
1094
+ export declare function musicDjAudioDataUrl(clip: {
1095
+ readonly audio_base64: string;
1096
+ readonly format?: string;
1097
+ }): string;
@@ -60,6 +60,7 @@ import { Resource } from "../../http";
60
60
  import type { ListParams } from "../../listing";
61
61
  import { type BaseRecord, type FileInput, type FileOutput, type Id, type NativeFile, type Paginated, type RequestOptions, type Timestamp } from "../../types";
62
62
  import type { VocalSeparation } from "../tools/vocalSeparation";
63
+ import type { SongImport } from "./imports";
63
64
  /**
64
65
  * Primary key of a song. An **integer**, unlike most ids in this API.
65
66
  *
@@ -113,7 +114,7 @@ export interface SongArtistCredit extends Omit<BaseRecord, "id"> {
113
114
  readonly compressed_image_fs_node_id?: Id | null;
114
115
  }
115
116
  /** Where a track came from. */
116
- export type SongSourceKind = "upload" | "yt_dlp" | "spotify_sync";
117
+ export type SongSourceKind = "upload" | "yt_dlp" | "spotify_sync" | "rematch";
117
118
  /**
118
119
  * A track in the library.
119
120
  *
@@ -142,6 +143,35 @@ export interface Song extends Omit<BaseRecord, "id"> {
142
143
  readonly source_id: string | null;
143
144
  /** Recording identifier, when the importer resolved one. */
144
145
  readonly isrc: string | null;
146
+ /**
147
+ * ISO 639-1 code of the language SUNG on the track (`"es"`, `"pt"`, `"ja"`),
148
+ * or `null`.
149
+ *
150
+ * Derived, not declared: the server runs a detector over the stored lyrics,
151
+ * and falls back to unambiguous Last.fm tags (`"j-pop"`, `"fado"`) when
152
+ * there are none. So `null` means "not known yet" - a song whose lyrics were
153
+ * never fetched, or an instrumental - and NEVER "no language". Treat it as
154
+ * unknown rather than quietly dropping it from a language view.
155
+ */
156
+ readonly language: string | null;
157
+ /**
158
+ * Lowercased Last.fm tags for the track, most-voted first, at most six.
159
+ *
160
+ * Crowd tags, not a controlled vocabulary: expect `"rock"` and
161
+ * `"female vocalists"` in the same list, and expect `[]` for anything
162
+ * obscure. The server drops the junk ones ("seen live", "albums i own") and
163
+ * falls back to the artist's tags when the track has none of its own, which
164
+ * is why two songs by one artist often carry identical lists.
165
+ */
166
+ readonly tags: string[];
167
+ /**
168
+ * Tempo in beats per minute, or `null` when it is not known.
169
+ *
170
+ * Comes from the recording's own registry entry, matched by ISRC, so a
171
+ * track with no ISRC never gets one and a remix is never given the
172
+ * original's tempo. `null` is common and means nothing about the music.
173
+ */
174
+ readonly bpm: number | null;
145
175
  readonly original_filename: string | null;
146
176
  readonly audio_codec: string | null;
147
177
  readonly audio_bitrate_kbps: number | null;
@@ -254,6 +284,48 @@ export interface SongSeparationStatus {
254
284
  */
255
285
  readonly job: VocalSeparation | null;
256
286
  }
287
+ /** Providers a track can be matched against. */
288
+ export type SongMatchSource = "youtube" | "soundcloud" | "bandcamp";
289
+ /**
290
+ * One take the matcher found for a track, scored the way the import scores it.
291
+ *
292
+ * A candidate with `score: null` was REJECTED and `reject_reason` says why
293
+ * ("title says 'cover', query did not"); it is returned anyway, because the
294
+ * reason is often the whole answer to "why did I get this recording". Rejected
295
+ * candidates can still be passed to {@link MusicSongsNamespace.rematch}: a
296
+ * deliberate choice is not second-guessed.
297
+ */
298
+ export interface SongMatchCandidate {
299
+ readonly source: SongMatchSource;
300
+ readonly title: string | null;
301
+ readonly uploader: string | null;
302
+ /** The page to import from. Feed it back as `sourceUrl` to take this one. */
303
+ readonly url: string | null;
304
+ /** Absent for providers whose search does not carry a runtime. */
305
+ readonly duration_s: number | null;
306
+ readonly thumbnail: string | null;
307
+ readonly source_id: string | null;
308
+ /** Higher is a better match; `null` means rejected. */
309
+ readonly score: number | null;
310
+ readonly reject_reason: string | null;
311
+ /** Where the provider's own search ranked it, before scoring. */
312
+ readonly rank: number;
313
+ /** True for the source the song's audio already came from. */
314
+ readonly current: boolean;
315
+ }
316
+ /** Arguments for {@link MusicSongsNamespace.matchCandidates}. */
317
+ export interface ListSongMatchCandidatesParams {
318
+ /** Candidates per provider. Clamped to 1..20; defaults to 8. */
319
+ readonly limit?: number;
320
+ }
321
+ /** Arguments for {@link MusicSongsNamespace.rematch}. */
322
+ export interface RematchSongInput {
323
+ /**
324
+ * The page to download from, normally a {@link SongMatchCandidate.url}.
325
+ * Omit to re-run the matcher over the track's original search terms.
326
+ */
327
+ readonly sourceUrl?: string;
328
+ }
257
329
  /** A liked track. The join row, with the whole song inlined. */
258
330
  export interface LikedSong extends Omit<BaseRecord, "id"> {
259
331
  /** Integer primary key of the like itself. NOT the song id. */
@@ -402,7 +474,7 @@ export interface MusicExternalSearchResult {
402
474
  * `artist` is on the list but is not a column - see
403
475
  * {@link ListSongsParams.artist}.
404
476
  */
405
- export declare const SONG_FILTER_COLUMNS: readonly ["id", "created_at", "updated_at", "title", "album", "position", "year", "artist"];
477
+ export declare const SONG_FILTER_COLUMNS: readonly ["id", "created_at", "updated_at", "title", "album", "position", "year", "artist", "language"];
406
478
  /** Columns the backend will accept in `modifiers[order]`. */
407
479
  export declare const SONG_ORDER_COLUMNS: readonly string[];
408
480
  /** Filters shared by `GET /songs` and `GET /songs/albums`. */
@@ -460,6 +532,15 @@ export interface SongFilters {
460
532
  * excluded even if they also appear as a feature.
461
533
  */
462
534
  readonly artistRole?: SongArtistRole;
535
+ /**
536
+ * Exact language code, or a list of them, sent as `exact_search[language]`.
537
+ *
538
+ * Matches {@link Song.language}, with the same caveat: rows the server could
539
+ * not classify hold `null` and are NOT returned by any code you ask for.
540
+ * Pass `null` to get exactly those - the unclassified pile - which is a
541
+ * different question from "songs with no words".
542
+ */
543
+ readonly language?: string | string[] | null;
463
544
  }
464
545
  /** Arguments for {@link MusicSongsNamespace.list}. */
465
546
  export interface ListSongsParams extends SongFilters, ListParams<(typeof SONG_FILTER_COLUMNS)[number]> {
@@ -901,6 +982,49 @@ export declare class MusicSongsNamespace extends Resource {
901
982
  * @throws {OmsAuthError} 401 when the song is not yours.
902
983
  */
903
984
  deleteSeparation(id: SongId, options?: RequestOptions): Promise<void>;
985
+ /**
986
+ * `GET /songs/:id/match_candidates` - every take the matcher can find for
987
+ * this track, across all three providers, scored and ordered best first.
988
+ *
989
+ * This is the answer to "this is the wrong recording". A track imported by
990
+ * artist and title is matched, not looked up, and the classic wrong result is
991
+ * a cover: same runtime, the original artist's name in its title, and nothing
992
+ * about it that a duration check can catch. The pool shows what the matcher
993
+ * chose between, with the rejects kept and labelled.
994
+ *
995
+ * Candidates are searched live on three providers, so this is SLOW - seconds,
996
+ * not milliseconds - and worth its own timeout. Nothing is cached: two calls
997
+ * a minute apart can return different pools.
998
+ *
999
+ * Scores are comparable within one response and meaningless across two.
1000
+ *
1001
+ * @throws {OmsApiError} 404 for a song that is not yours, 502 when the
1002
+ * downloader is unreachable.
1003
+ */
1004
+ matchCandidates(id: SongId, params?: ListSongMatchCandidatesParams, options?: RequestOptions): Promise<SongMatchCandidate[]>;
1005
+ /**
1006
+ * `POST /songs/:id/rematch` - re-download this track onto the same record.
1007
+ *
1008
+ * The song row survives: its id, its credits, and every playlist entry, like
1009
+ * and play event that points at it. Only the audio is replaced, along with
1010
+ * the fields that describe the file (runtime, codec, source). Any vocal
1011
+ * separation is thrown away, because the stems came out of audio the song no
1012
+ * longer has.
1013
+ *
1014
+ * With a `sourceUrl` the pick is taken as given - no duration or cover check
1015
+ * stands between you and the recording you named. Without one the matcher
1016
+ * re-runs over the track's original search terms, which is the useful move
1017
+ * after a bad match rather than a repeat of it.
1018
+ *
1019
+ * Returns the import to poll through `music.imports`; the swap lands when it
1020
+ * reaches `complete`.
1021
+ *
1022
+ * @throws {OmsAuthError} 401 when the song is not yours.
1023
+ * @throws {OmsApiError} 400 for a `sourceUrl` that is not http(s), or when no
1024
+ * `sourceUrl` is given and the track has no search terms to re-run (an
1025
+ * uploaded file, for one - there is nothing to match it against).
1026
+ */
1027
+ rematch(id: SongId, input?: RematchSongInput, options?: RequestOptions): Promise<SongImport>;
904
1028
  /**
905
1029
  * `GET /liked_songs` - the caller's likes, newest first, with each song
906
1030
  * inlined in full.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omelhorsite/sdk",
3
- "version": "0.12.2",
3
+ "version": "0.15.1",
4
4
  "description": "TypeScript SDK for the omelhorsite API. Isolate-safe: no node builtins, no environment access, no stdout.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",