@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/README.md +23 -0
- package/dist/client.d.ts +13 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/filter.d.ts +122 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.test.d.ts +2 -0
- package/dist/filter.test.d.ts.map +1 -0
- package/dist/generated/types.d.ts +20 -0
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -27
- package/dist/remote-player.d.ts +5 -0
- package/dist/remote-player.d.ts.map +1 -1
- package/dist/remote.js +6 -2
- package/package.json +1 -1
- package/src/client.ts +38 -8
- package/src/filter.test.ts +94 -0
- package/src/filter.ts +192 -0
- package/src/generated/types.ts +21 -0
- package/src/index.ts +8 -0
- package/src/remote-controller.ts +2 -0
- package/src/remote-player.ts +7 -0
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;
|
package/src/generated/types.ts
CHANGED
|
@@ -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";
|
package/src/remote-controller.ts
CHANGED
|
@@ -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,
|
package/src/remote-player.ts
CHANGED
|
@@ -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
|
});
|