@rocksky/sdk 0.11.0 → 0.13.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/src/filter.ts ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Fluent builder for RSQL filter expressions, accepted by the `filter`
3
+ * parameter of the catalog and scrobble-feed queries
4
+ * (app.rocksky.song.getSongs, app.rocksky.artist.getArtists,
5
+ * app.rocksky.album.getAlbums, app.rocksky.scrobble.getScrobbles).
6
+ *
7
+ * ```ts
8
+ * import { Filter } from "@rocksky/sdk";
9
+ *
10
+ * const filter = Filter.eq("artist", "Daft Punk")
11
+ * .and(Filter.gt("duration", 200_000))
12
+ * .or(Filter.in("genre", ["house", "electro"]));
13
+ *
14
+ * await client.catalogSongs(50, 0, undefined, filter);
15
+ * // artist=="Daft Punk";duration=gt=200000,genre=in=(house,electro)
16
+ * ```
17
+ *
18
+ * String values are quoted and escaped automatically when they contain
19
+ * characters RSQL reserves; `*` wildcards pass through unquoted so
20
+ * `Filter.eq("artist", "Daft*")` performs a case-insensitive match.
21
+ */
22
+ export type FilterValue = string | number | boolean;
23
+
24
+ /** Characters that never need quoting in an RSQL value (`*` kept bare so wildcards work). */
25
+ const SAFE_VALUE = /^[A-Za-z0-9_.:@*+-]+$/;
26
+
27
+ const renderValue = (value: FilterValue): string => {
28
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
29
+ if (value.length > 0 && SAFE_VALUE.test(value)) return value;
30
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
31
+ };
32
+
33
+ type NodeKind = "comparison" | "and" | "or";
34
+
35
+ export class Filter {
36
+ private constructor(
37
+ private readonly expr: string,
38
+ private readonly kind: NodeKind,
39
+ ) {}
40
+
41
+ private static comparison(field: string, op: string, value: FilterValue): Filter {
42
+ return new Filter(`${field}${op}${renderValue(value)}`, "comparison");
43
+ }
44
+
45
+ private static list(field: string, op: string, values: FilterValue[]): Filter {
46
+ if (values.length === 0) {
47
+ throw new Error(`Filter.${op === "=in=" ? "in" : "out"}("${field}", ...) needs at least one value`);
48
+ }
49
+ return new Filter(`${field}${op}(${values.map(renderValue).join(",")})`, "comparison");
50
+ }
51
+
52
+ /** `field==value` — equals; `*` in string values is a wildcard. */
53
+ static eq(field: string, value: FilterValue): Filter {
54
+ return Filter.comparison(field, "==", value);
55
+ }
56
+
57
+ /** `field!=value` — not equals. */
58
+ static ne(field: string, value: FilterValue): Filter {
59
+ return Filter.comparison(field, "!=", value);
60
+ }
61
+
62
+ /** `field=gt=value` — greater than. */
63
+ static gt(field: string, value: FilterValue): Filter {
64
+ return Filter.comparison(field, "=gt=", value);
65
+ }
66
+
67
+ /** `field=ge=value` — greater than or equal. */
68
+ static ge(field: string, value: FilterValue): Filter {
69
+ return Filter.comparison(field, "=ge=", value);
70
+ }
71
+
72
+ /** `field=lt=value` — less than. */
73
+ static lt(field: string, value: FilterValue): Filter {
74
+ return Filter.comparison(field, "=lt=", value);
75
+ }
76
+
77
+ /** `field=le=value` — less than or equal. */
78
+ static le(field: string, value: FilterValue): Filter {
79
+ return Filter.comparison(field, "=le=", value);
80
+ }
81
+
82
+ /** `field=in=(a,b)` — matches any of the values. */
83
+ static in(field: string, values: FilterValue[]): Filter {
84
+ return Filter.list(field, "=in=", values);
85
+ }
86
+
87
+ /** `field=out=(a,b)` — matches none of the values. */
88
+ static out(field: string, values: FilterValue[]): Filter {
89
+ return Filter.list(field, "=out=", values);
90
+ }
91
+
92
+ /** `field==null` — the field is NULL. */
93
+ static isNull(field: string): Filter {
94
+ return new Filter(`${field}==null`, "comparison");
95
+ }
96
+
97
+ /** `field!=null` — the field is not NULL. */
98
+ static isNotNull(field: string): Filter {
99
+ return new Filter(`${field}!=null`, "comparison");
100
+ }
101
+
102
+ /** Both sides must match (`;`). An `or` operand is parenthesized to keep RSQL precedence. */
103
+ and(other: Filter): Filter {
104
+ return new Filter(`${this.renderIn("and")};${other.renderIn("and")}`, "and");
105
+ }
106
+
107
+ /** Either side may match (`,`). */
108
+ or(other: Filter): Filter {
109
+ return new Filter(`${this.expr},${other.expr}`, "or");
110
+ }
111
+
112
+ private renderIn(parent: "and"): string {
113
+ return this.kind === "or" ? `(${this.expr})` : this.expr;
114
+ }
115
+
116
+ /** The RSQL expression string to send as the `filter` query param. */
117
+ build(): string {
118
+ return this.expr;
119
+ }
120
+
121
+ toString(): string {
122
+ return this.expr;
123
+ }
124
+ }
125
+
126
+ /** Filterable fields of app.rocksky.song.getSongs. */
127
+ export const SongFields = {
128
+ title: "title",
129
+ artist: "artist",
130
+ album: "album",
131
+ albumArtist: "albumArtist",
132
+ genre: "genre",
133
+ composer: "composer",
134
+ label: "label",
135
+ duration: "duration",
136
+ trackNumber: "trackNumber",
137
+ discNumber: "discNumber",
138
+ mbId: "mbId",
139
+ isrc: "isrc",
140
+ sha256: "sha256",
141
+ uri: "uri",
142
+ albumUri: "albumUri",
143
+ artistUri: "artistUri",
144
+ createdAt: "createdAt",
145
+ } as const;
146
+
147
+ /** Filterable fields of app.rocksky.album.getAlbums. */
148
+ export const AlbumFields = {
149
+ title: "title",
150
+ artist: "artist",
151
+ year: "year",
152
+ releaseDate: "releaseDate",
153
+ sha256: "sha256",
154
+ uri: "uri",
155
+ artistUri: "artistUri",
156
+ createdAt: "createdAt",
157
+ } as const;
158
+
159
+ /** Filterable fields of app.rocksky.artist.getArtists. */
160
+ export const ArtistFields = {
161
+ name: "name",
162
+ genres: "genres",
163
+ bornIn: "bornIn",
164
+ born: "born",
165
+ died: "died",
166
+ sha256: "sha256",
167
+ uri: "uri",
168
+ createdAt: "createdAt",
169
+ } as const;
170
+
171
+ /** Filterable fields of app.rocksky.scrobble.getScrobbles (dotted selectors reach the joined track/user/artist). */
172
+ export const ScrobbleFields = {
173
+ uri: "uri",
174
+ date: "date",
175
+ timestamp: "timestamp",
176
+ title: "title",
177
+ artist: "artist",
178
+ album: "album",
179
+ trackTitle: "track.title",
180
+ trackArtist: "track.artist",
181
+ trackAlbum: "track.album",
182
+ trackAlbumArtist: "track.albumArtist",
183
+ trackGenre: "track.genre",
184
+ trackDuration: "track.duration",
185
+ trackIsrc: "track.isrc",
186
+ trackMbId: "track.mbId",
187
+ userDid: "user.did",
188
+ userHandle: "user.handle",
189
+ userDisplayName: "user.displayName",
190
+ artistName: "artist.name",
191
+ artistGenres: "artist.genres",
192
+ } as const;
@@ -243,6 +243,8 @@ export interface ArtistGetArtistsParams {
243
243
  names?: string;
244
244
  /** The genre to filter artists by */
245
245
  genre?: string;
246
+ /** RSQL filter expression, e.g. `name==Daft*;genres=in=(house,electro)`. Supports ==, !=, <, <=, >, >=, =in=, =out=, and `;`/`and`, `,`/`or` combinators, `*` wildcards in string values. Filterable fields: name, genres, bornIn, born, died, sha256, uri, createdAt */
247
+ filter?: string;
246
248
  }
247
249
 
248
250
  export interface ArtistListenerViewBasic {
@@ -847,6 +849,8 @@ export interface GetAlbumsParams {
847
849
  offset?: number;
848
850
  /** The genre to filter artists by */
849
851
  genre?: string;
852
+ /** RSQL filter expression, e.g. `artist=="Daft Punk";year=ge=2000`. Supports ==, !=, <, <=, >, >=, =in=, =out=, and `;`/`and`, `,`/`or` combinators, `*` wildcards in string values. Filterable fields: title, artist, year, releaseDate, sha256, uri, artistUri, createdAt */
853
+ filter?: string;
850
854
  }
851
855
 
852
856
  export interface GetAlbumTracksOutput {
@@ -1243,6 +1247,8 @@ export interface GetScrobblesParams {
1243
1247
  limit?: number;
1244
1248
  /** The offset for pagination */
1245
1249
  offset?: number;
1250
+ /** RSQL filter expression, e.g. `track.artist=="Daft Punk";date=ge=2025-01-01`. Supports ==, !=, <, <=, >, >=, =in=, =out=, and `;`/`and`, `,`/`or` combinators, `*` wildcards in string values. Filterable fields: uri, date, timestamp, title, artist, album, track.title, track.artist, track.album, track.albumArtist, track.genre, track.duration, track.isrc, track.mbId, user.did, user.handle, user.displayName, artist.name, artist.genres */
1251
+ filter?: string;
1246
1252
  }
1247
1253
 
1248
1254
  export interface GetShoutRepliesOutput {
@@ -1312,6 +1318,8 @@ export interface GetSongsParams {
1312
1318
  isrc?: string;
1313
1319
  /** Filter songs by Spotify track ID (resolved internally to the Spotify track URL) */
1314
1320
  spotifyId?: string;
1321
+ /** RSQL filter expression, e.g. `artist=="Daft Punk";duration=gt=200000`. Supports ==, !=, <, <=, >, >=, =in=, =out=, and `;`/`and`, `,`/`or` combinators, `*` wildcards in string values. Filterable fields: title, artist, album, albumArtist, genre, composer, label, duration, trackNumber, discNumber, mbId, isrc, sha256, uri, albumUri, artistUri, createdAt */
1322
+ filter?: string;
1315
1323
  }
1316
1324
 
1317
1325
  export interface GetStarredOutput {
@@ -1626,6 +1634,18 @@ export interface NotificationActor {
1626
1634
  avatar?: Uri;
1627
1635
  }
1628
1636
 
1637
+ /** The song, album, or scrobble a notification relates to, for rich display. */
1638
+ export interface NotificationSubjectView {
1639
+ /** The at-uri of the subject. */
1640
+ uri: string;
1641
+ /** The title of the track or album. */
1642
+ title?: string;
1643
+ /** The artist of the track or album. */
1644
+ artist?: string;
1645
+ /** The album art image URL. */
1646
+ albumArt?: Uri;
1647
+ }
1648
+
1629
1649
  export interface NotificationView {
1630
1650
  /** The unique identifier of the notification. */
1631
1651
  id: string;
@@ -1642,6 +1662,7 @@ export interface NotificationView {
1642
1662
  /** The content of the related shout, if any. */
1643
1663
  shoutContent?: string;
1644
1664
  actor?: NotificationActor;
1665
+ subject?: NotificationSubjectView;
1645
1666
  }
1646
1667
 
1647
1668
  export interface PingOutput {
package/src/index.ts CHANGED
@@ -41,6 +41,14 @@ export {
41
41
  type RemoteDevice,
42
42
  type RemoteStatus,
43
43
  } from "./remote-controller.js";
44
+ export {
45
+ Filter,
46
+ SongFields,
47
+ AlbumFields,
48
+ ArtistFields,
49
+ ScrobbleFields,
50
+ type FilterValue,
51
+ } from "./filter.js";
44
52
  export { songHash, albumHash, artistHash } from "./hash.js";
45
53
  export { RockskyError } from "./errors.js";
46
54
  export type * from "./generated/types.js";
@@ -332,6 +332,8 @@ function trackFromJson(d: Json): RemoteNowPlaying {
332
332
  durationMs: d.duration_ms ?? d.length,
333
333
  elapsedMs: d.elapsed,
334
334
  isPlaying: d.is_playing,
335
+ codec: d.codec,
336
+ sampleRate: d.sample_rate,
335
337
  // Server-enriched fields (present on the broadcast a controller receives).
336
338
  songUri: d.song_uri,
337
339
  albumUri: d.album_uri,
@@ -36,6 +36,11 @@ export interface RemoteNowPlaying {
36
36
  /** Current position, ms. */
37
37
  elapsedMs?: number;
38
38
  isPlaying?: boolean;
39
+ /** Audio codec / container of the playing file (e.g. "mp3", "flac"),
40
+ * when the player knows it. Shown as a format badge by controller UIs. */
41
+ codec?: string;
42
+ /** Audio sample rate in Hz (e.g. 44100), when the player knows it. */
43
+ sampleRate?: number;
39
44
  // The following are filled in by the server on the broadcast a controller
40
45
  // receives (a player leaves them unset — the server resolves them from the
41
46
  // library). They let a controller UI deep-link and show like state.
@@ -186,6 +191,8 @@ export class RemotePlayer {
186
191
  duration_ms: track.durationMs ?? 0,
187
192
  album_art: track.albumArt,
188
193
  is_playing: track.isPlaying ?? true,
194
+ codec: track.codec,
195
+ sample_rate: track.sampleRate,
189
196
  device_name: this.opts.name,
190
197
  },
191
198
  });