@rocksky/sdk 0.14.0 → 0.14.2

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.
@@ -0,0 +1,83 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import { RockskyClient } from "./client.js";
4
+
5
+ const TOKEN = "test-token";
6
+
7
+ /**
8
+ * Swap in a fetch that records the request instead of making one. The client
9
+ * builds its fetch handler in the constructor, so install this first.
10
+ */
11
+ function captureFetch(): {
12
+ calls: Request[];
13
+ only: () => Request;
14
+ restore: () => void;
15
+ } {
16
+ const calls: Request[] = [];
17
+ const real = globalThis.fetch;
18
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
19
+ calls.push(new Request(input as RequestInfo, init));
20
+ return new Response(JSON.stringify({ status: "ok" }), {
21
+ status: 200,
22
+ headers: { "content-type": "application/json" },
23
+ });
24
+ }) as typeof globalThis.fetch;
25
+ return {
26
+ calls,
27
+ /** The single request that was made; fails loudly if there wasn't exactly one. */
28
+ only: () => {
29
+ expect(calls).toHaveLength(1);
30
+ return calls[0] as Request;
31
+ },
32
+ restore: () => {
33
+ globalThis.fetch = real;
34
+ },
35
+ };
36
+ }
37
+
38
+ let restore: (() => void) | null = null;
39
+ afterEach(() => {
40
+ restore?.();
41
+ restore = null;
42
+ });
43
+
44
+ describe("authenticated request headers", () => {
45
+ test("a JSON procedure keeps its content-type alongside the bearer token", async () => {
46
+ // Regression: the auth wrapper used to merge headers with an object spread.
47
+ // atcute passes a Headers instance, which has no own enumerable properties,
48
+ // so the spread produced `{}` and dropped the `application/json` content-type
49
+ // atcute sets for a JSON body. The AppView then saw text/plain and replied
50
+ // `InvalidRequest: Wrong request encoding (Content-Type): text/plain`.
51
+ const cap = captureFetch();
52
+ restore = cap.restore;
53
+
54
+ const client = new RockskyClient("https://appview.test", TOKEN);
55
+ await client.library().createPlaylist("Late night drive");
56
+
57
+ const req = cap.only();
58
+ expect(req.method).toBe("POST");
59
+ expect(req.headers.get("content-type")).toBe("application/json");
60
+ expect(req.headers.get("authorization")).toBe(`Bearer ${TOKEN}`);
61
+ expect(await req.json()).toEqual({ name: "Late night drive" });
62
+ });
63
+
64
+ test("a query still carries the bearer token", async () => {
65
+ const cap = captureFetch();
66
+ restore = cap.restore;
67
+
68
+ const client = new RockskyClient("https://appview.test", TOKEN);
69
+ await client.library().getPlaylists();
70
+
71
+ expect(cap.only().headers.get("authorization")).toBe(`Bearer ${TOKEN}`);
72
+ });
73
+
74
+ test("no token means no authorization header", async () => {
75
+ const cap = captureFetch();
76
+ restore = cap.restore;
77
+
78
+ const client = new RockskyClient("https://appview.test");
79
+ await client.get("app.rocksky.actor.getProfile", { did: "did:plc:test" });
80
+
81
+ expect(cap.only().headers.get("authorization")).toBeNull();
82
+ });
83
+ });
package/src/client.ts CHANGED
@@ -10,6 +10,8 @@ import type {
10
10
  AlbumViewDetailed,
11
11
  ArtistViewBasic,
12
12
  ArtistViewDetailed,
13
+ ChartsDecadeViewBasic,
14
+ ChartsScrobblerViewBasic,
13
15
  ChartsView,
14
16
  CreateScrobbleInput,
15
17
  FollowAccountOutput,
@@ -53,6 +55,9 @@ import type {
53
55
  StatsGlobalStatsView,
54
56
  StatsView,
55
57
  StatsWrappedView,
58
+ PlaylistCreatePlaylistOutput,
59
+ PlaylistUpdatePlaylistOutput,
60
+ AddSongsOutput,
56
61
  UnfollowAccountOutput,
57
62
  UpdateSeenOutput,
58
63
  } from "./generated/types.js";
@@ -113,11 +118,16 @@ export class RockskyClient {
113
118
  let handler = simpleFetchHandler({ service: appview });
114
119
  if (token) {
115
120
  const inner = handler;
116
- handler = ((pathname: string, init?: RequestInit) =>
117
- inner(pathname, {
118
- ...init,
119
- headers: { ...(init?.headers as Record<string, string>), authorization: `Bearer ${token}` },
120
- })) as typeof handler;
121
+ handler = ((pathname: string, init?: RequestInit) => {
122
+ // `new Headers(init.headers)`, not a spread: atcute hands us a Headers
123
+ // instance, and a Headers has no own enumerable properties — spreading
124
+ // it yields `{}` and silently drops everything already set. That threw
125
+ // away the `content-type: application/json` atcute adds for a JSON
126
+ // body, so the AppView saw text/plain and rejected the request.
127
+ const headers = new Headers(init?.headers);
128
+ headers.set("authorization", `Bearer ${token}`);
129
+ return inner(pathname, { ...init, headers });
130
+ }) as typeof handler;
121
131
  }
122
132
  this.rpc = new Client({ handler });
123
133
  }
@@ -234,26 +244,67 @@ export class RockskyClient {
234
244
  return this.topArtistsInterval(limit, offset, Interval.allTime());
235
245
  }
236
246
 
237
- /** The top tracks chart over a typed {@link DateInterval}. */
238
- async topTracksInterval(limit: number, offset: number, interval: DateInterval): Promise<SongViewBasic[]> {
247
+ /** The top tracks chart over a typed {@link DateInterval}. Pass `did` to scope
248
+ * it to one actor instead of the platform-wide ranking. */
249
+ async topTracksInterval(
250
+ limit: number,
251
+ offset: number,
252
+ interval: DateInterval,
253
+ did?: string,
254
+ ): Promise<SongViewBasic[]> {
239
255
  const out = await this.query<{ tracks?: SongViewBasic[] }>("app.rocksky.charts.getTopTracks", {
240
256
  limit,
241
257
  offset,
258
+ did,
242
259
  ...interval,
243
260
  });
244
261
  return out.tracks ?? [];
245
262
  }
246
263
 
247
- /** The top artists chart over a typed {@link DateInterval}. */
248
- async topArtistsInterval(limit: number, offset: number, interval: DateInterval): Promise<ArtistViewBasic[]> {
264
+ /** The top artists chart over a typed {@link DateInterval}. Pass `did` to scope
265
+ * it to one actor instead of the platform-wide ranking. */
266
+ async topArtistsInterval(
267
+ limit: number,
268
+ offset: number,
269
+ interval: DateInterval,
270
+ did?: string,
271
+ ): Promise<ArtistViewBasic[]> {
249
272
  const out = await this.query<{ artists?: ArtistViewBasic[] }>("app.rocksky.charts.getTopArtists", {
250
273
  limit,
251
274
  offset,
275
+ did,
252
276
  ...interval,
253
277
  });
254
278
  return out.artists ?? [];
255
279
  }
256
280
 
281
+ /** Scrobbles grouped by the release decade of the music, over a typed
282
+ * {@link DateInterval}. Pass `did` to scope it to one actor. */
283
+ async decades(
284
+ interval: DateInterval = {},
285
+ did?: string,
286
+ ): Promise<ChartsDecadeViewBasic[]> {
287
+ const out = await this.query<{ decades?: ChartsDecadeViewBasic[] }>(
288
+ "app.rocksky.charts.getDecades",
289
+ { did, ...interval },
290
+ );
291
+ return out.decades ?? [];
292
+ }
293
+
294
+ /** The listeners who scrobbled the most, over a typed {@link DateInterval}.
295
+ * Pass `Interval.allTime()` for the all-time leaderboard. */
296
+ async topScrobblers(
297
+ limit = 20,
298
+ offset = 0,
299
+ interval: DateInterval = {},
300
+ ): Promise<ChartsScrobblerViewBasic[]> {
301
+ const out = await this.query<{ scrobblers?: ChartsScrobblerViewBasic[] }>(
302
+ "app.rocksky.charts.getTopScrobblers",
303
+ { limit, offset, ...interval },
304
+ );
305
+ return out.scrobblers ?? [];
306
+ }
307
+
257
308
  /** The album catalog, optionally filtered by `genre` and/or an RSQL
258
309
  * {@link Filter} expression (see {@link AlbumFields}). */
259
310
  async catalogAlbums(
@@ -406,9 +457,11 @@ export class RockskyClient {
406
457
  return this.query("app.rocksky.artist.getArtist", { uri });
407
458
  }
408
459
  /** Resolve full canonical metadata for a bare title + artist
409
- * (`app.rocksky.song.matchSong`); optionally anchor with `mbId` / `isrc`. */
410
- matchSong(title: string, artist: string, mbId?: string, isrc?: string): Promise<SongViewDetailed> {
411
- return this.query("app.rocksky.song.matchSong", { title, artist, mbId, isrc });
460
+ * (`app.rocksky.song.matchSong`); optionally anchor with `mbId` / `isrc`.
461
+ * `album` steers the match toward that release (case-insensitive) so a
462
+ * remaster/live/single edition doesn't shadow the intended album. */
463
+ matchSong(title: string, artist: string, mbId?: string, isrc?: string, album?: string): Promise<SongViewDetailed> {
464
+ return this.query("app.rocksky.song.matchSong", { title, artist, mbId, isrc, album });
412
465
  }
413
466
  /** A single song by at:// `uri` (or by `mbid` / `isrc` / `spotifyId`). */
414
467
  song(opts: {
@@ -508,14 +561,71 @@ export class RockskyClient {
508
561
  spotifyCurrentlyPlaying(actor: string): Promise<PlayerCurrentlyPlayingViewDetailed> {
509
562
  return this.query("app.rocksky.spotify.getCurrentlyPlaying", { actor });
510
563
  }
511
- /** The playlist catalog. */
512
- playlists(limit = 50, offset = 0): Promise<PlaylistGetPlaylistsOutput> {
513
- return this.query("app.rocksky.playlist.getPlaylists", { limit, offset });
564
+ /** The playlist catalog, optionally filtered by an RSQL {@link Filter}
565
+ * expression (see {@link PlaylistFields}). */
566
+ playlists(
567
+ limit = 50,
568
+ offset = 0,
569
+ filter?: string | Filter,
570
+ ): Promise<PlaylistGetPlaylistsOutput> {
571
+ return this.query("app.rocksky.playlist.getPlaylists", {
572
+ limit,
573
+ offset,
574
+ filter: filter?.toString(),
575
+ });
514
576
  }
515
577
  /** A single playlist with its items. */
516
578
  playlist(uri: string): Promise<PlaylistViewDetailed> {
517
579
  return this.query("app.rocksky.playlist.getPlaylist", { uri });
518
580
  }
581
+
582
+ /**
583
+ * Create a playlist (`app.rocksky.playlist.createPlaylist`). Auth required.
584
+ * Publishes an app.rocksky.playlist record to the caller's repo; the AppView
585
+ * only lists it once the commit has been ingested.
586
+ */
587
+ createPlaylist(input: {
588
+ name: string;
589
+ description?: string;
590
+ pictureUrl?: string;
591
+ }): Promise<PlaylistCreatePlaylistOutput> {
592
+ return this.post("app.rocksky.playlist.createPlaylist", { params: input });
593
+ }
594
+
595
+ /** Rename or re-describe a playlist (`app.rocksky.playlist.updatePlaylist`). Owner only. */
596
+ updatePlaylist(input: {
597
+ uri: string;
598
+ name?: string;
599
+ description?: string;
600
+ pictureUrl?: string;
601
+ }): Promise<PlaylistUpdatePlaylistOutput> {
602
+ return this.post("app.rocksky.playlist.updatePlaylist", { params: input });
603
+ }
604
+
605
+ /**
606
+ * Add songs to a playlist (`app.rocksky.playlist.addSongs`). Owner only.
607
+ * `songs` are app.rocksky.song AT-URIs; returns the created entry URIs.
608
+ */
609
+ addSongs(uri: string, songs: string[]): Promise<AddSongsOutput> {
610
+ return this.post("app.rocksky.playlist.addSongs", {
611
+ params: { uri, songs },
612
+ });
613
+ }
614
+
615
+ /** Delete a playlist and the caller's own entries (`app.rocksky.playlist.removePlaylist`). Owner only. */
616
+ removePlaylist(uri: string): Promise<void> {
617
+ return this.post("app.rocksky.playlist.removePlaylist", { params: { uri } });
618
+ }
619
+
620
+ /**
621
+ * Remove a song from a playlist (`app.rocksky.playlist.removeTrack`). An
622
+ * entry can only be retracted by the repo that published it.
623
+ */
624
+ removeTrack(uri: string, songUri: string): Promise<void> {
625
+ return this.post("app.rocksky.playlist.removeTrack", {
626
+ params: { uri, songUri },
627
+ });
628
+ }
519
629
  /** Shouts on an album. */
520
630
  albumShouts(uri: string, limit = 50, offset = 0): Promise<GetAlbumShoutsOutput> {
521
631
  return this.query("app.rocksky.shout.getAlbumShouts", { uri, limit, offset });
package/src/filter.ts CHANGED
@@ -168,6 +168,26 @@ export const ArtistFields = {
168
168
  createdAt: "createdAt",
169
169
  } as const;
170
170
 
171
+ /** Filterable fields of app.rocksky.playlist.getPlaylists (`track.*` selectors match the playlist's contents). */
172
+ export const PlaylistFields = {
173
+ name: "name",
174
+ title: "title",
175
+ description: "description",
176
+ uri: "uri",
177
+ spotifyLink: "spotifyLink",
178
+ tidalLink: "tidalLink",
179
+ appleMusicLink: "appleMusicLink",
180
+ createdAt: "createdAt",
181
+ updatedAt: "updatedAt",
182
+ curatorDid: "curatorDid",
183
+ curatorHandle: "curatorHandle",
184
+ curatorName: "curatorName",
185
+ trackTitle: "track.title",
186
+ trackArtist: "track.artist",
187
+ trackAlbum: "track.album",
188
+ trackAlbumArtist: "track.albumArtist",
189
+ } as const;
190
+
171
191
  /** Filterable fields of app.rocksky.scrobble.getScrobbles (dotted selectors reach the joined track/user/artist). */
172
192
  export const ScrobbleFields = {
173
193
  uri: "uri",
@@ -126,6 +126,18 @@ export interface AddItemsToQueueParams {
126
126
  shuffle?: boolean;
127
127
  }
128
128
 
129
+ export interface AddSongsOutput {
130
+ /** AT-URIs of the created app.rocksky.playlist.song records */
131
+ uris: AtUri[];
132
+ }
133
+
134
+ export interface AddSongsParams {
135
+ /** The URI of the playlist to add the songs to */
136
+ uri: AtUri;
137
+ /** AT-URIs of the app.rocksky.song records to add */
138
+ songs: AtUri[];
139
+ }
140
+
129
141
  export interface AlbumGetAlbumParams {
130
142
  /** The URI of the album to retrieve. */
131
143
  uri: AtUri;
@@ -355,6 +367,34 @@ export interface ArtistViewDetailed {
355
367
  tags?: string[];
356
368
  }
357
369
 
370
+ export interface ChartsDecadeViewBasic {
371
+ /** The first year of the decade, e.g. 1990. */
372
+ decade?: number;
373
+ /** The number of scrobbles of music released in this decade. */
374
+ scrobbles?: number;
375
+ /** The number of distinct albums scrobbled from this decade. */
376
+ uniqueAlbums?: number;
377
+ }
378
+
379
+ export interface ChartsScrobblerViewBasic {
380
+ /** The unique identifier of the actor. */
381
+ id?: string;
382
+ /** The DID of the actor. */
383
+ did?: string;
384
+ /** The handle of the actor. */
385
+ handle?: string;
386
+ /** The display name of the actor. */
387
+ displayName?: string;
388
+ /** The URL of the actor's avatar image. */
389
+ avatar?: Uri;
390
+ /** The number of scrobbles in the requested window. */
391
+ scrobbles?: number;
392
+ /** The number of distinct artists scrobbled in the window. */
393
+ uniqueArtists?: number;
394
+ /** The number of distinct tracks scrobbled in the window. */
395
+ uniqueTracks?: number;
396
+ }
397
+
358
398
  export interface ChartsScrobbleViewBasic {
359
399
  /** The date of the scrobble. */
360
400
  date?: DateTime;
@@ -755,10 +795,12 @@ export interface GetActorPlaylistsOutput {
755
795
  export interface GetActorPlaylistsParams {
756
796
  /** The DID or handle of the actor */
757
797
  did: AtIdentifier;
758
- /** The maximum number of albums to return */
798
+ /** The maximum number of playlists to return */
759
799
  limit?: number;
760
800
  /** The offset for pagination */
761
801
  offset?: number;
802
+ /** RSQL filter expression, e.g. `name=="Road trip*";track.artist=="Daft Punk"`. Supports ==, !=, <, <=, >, >=, =in=, =out=, and `;`/`and`, `,`/`or` combinators, `*` wildcards in string values. Filterable fields: name, title, description, uri, spotifyLink, tidalLink, appleMusicLink, createdAt, updatedAt. The `track.title`, `track.artist`, `track.album` and `track.albumArtist` selectors match the playlist's contents, returning playlists that contain a matching track; several `track.*` terms joined with `;` must all be satisfied by the same track. */
803
+ filter?: string;
762
804
  }
763
805
 
764
806
  export interface GetActorScrobblesOutput {
@@ -966,6 +1008,19 @@ export interface GetCoverArtUrlParams {
966
1008
  size?: number;
967
1009
  }
968
1010
 
1011
+ export interface GetDecadesOutput {
1012
+ decades?: ChartsDecadeViewBasic[];
1013
+ }
1014
+
1015
+ export interface GetDecadesParams {
1016
+ /** The DID or handle of the actor to scope the chart to */
1017
+ did?: AtIdentifier;
1018
+ /** The start date to count scrobbles from (ISO 8601 format) */
1019
+ startDate?: DateTime;
1020
+ /** The end date to count scrobbles to (ISO 8601 format) */
1021
+ endDate?: DateTime;
1022
+ }
1023
+
969
1024
  export interface GetDownloadUrlOutput {
970
1025
  /** The resolved media or cover-art URL. */
971
1026
  url: Uri;
@@ -1368,6 +1423,8 @@ export interface GetTopArtistsOutput {
1368
1423
  }
1369
1424
 
1370
1425
  export interface GetTopArtistsParams {
1426
+ /** The DID or handle of the actor to scope the chart to */
1427
+ did?: AtIdentifier;
1371
1428
  /** The maximum number of artists to return */
1372
1429
  limit?: number;
1373
1430
  /** The offset for pagination */
@@ -1378,6 +1435,21 @@ export interface GetTopArtistsParams {
1378
1435
  endDate?: DateTime;
1379
1436
  }
1380
1437
 
1438
+ export interface GetTopScrobblersOutput {
1439
+ scrobblers?: ChartsScrobblerViewBasic[];
1440
+ }
1441
+
1442
+ export interface GetTopScrobblersParams {
1443
+ /** The maximum number of scrobblers to return */
1444
+ limit?: number;
1445
+ /** The offset for pagination */
1446
+ offset?: number;
1447
+ /** The start date to count scrobbles from (ISO 8601 format) */
1448
+ startDate?: DateTime;
1449
+ /** The end date to count scrobbles to (ISO 8601 format) */
1450
+ endDate?: DateTime;
1451
+ }
1452
+
1381
1453
  export interface GetTopSongsOutput {
1382
1454
 
1383
1455
  }
@@ -1394,6 +1466,8 @@ export interface GetTopTracksOutput {
1394
1466
  }
1395
1467
 
1396
1468
  export interface GetTopTracksParams {
1469
+ /** The DID or handle of the actor to scope the chart to */
1470
+ did?: AtIdentifier;
1397
1471
  /** The maximum number of tracks to return */
1398
1472
  limit?: number;
1399
1473
  /** The offset for pagination */
@@ -1565,6 +1639,23 @@ export interface LibrarySearchParams {
1565
1639
  songOffset?: number;
1566
1640
  }
1567
1641
 
1642
+ export interface LibraryUpdatePlaylistInput {
1643
+ /** The playlist id to update. */
1644
+ playlistId: string;
1645
+ /** New playlist name. */
1646
+ name?: string;
1647
+ /** New playlist comment. */
1648
+ comment?: string;
1649
+ /** A song id to add to the playlist. */
1650
+ songIdToAdd?: string;
1651
+ /** A track index to remove from the playlist. */
1652
+ songIndexToRemove?: number;
1653
+ }
1654
+
1655
+ export interface LibraryUpdatePlaylistOutput {
1656
+
1657
+ }
1658
+
1568
1659
  export interface LikeRecord {
1569
1660
  /** The date when the like was created. */
1570
1661
  createdAt: DateTime;
@@ -1599,6 +1690,8 @@ export interface MatchSongParams {
1599
1690
  title: string;
1600
1691
  /** The artist of the song to retrieve */
1601
1692
  artist: string;
1693
+ /** Optional album title — candidates whose album matches it (case-insensitive) are preferred, so remaster/live/single editions don't shadow the intended release */
1694
+ album?: string;
1602
1695
  /** Optional MusicBrainz recording ID to anchor the match */
1603
1696
  mbId?: string;
1604
1697
  /** Optional International Standard Recording Code (ISRC) to anchor the match */
@@ -1610,6 +1703,8 @@ export interface MirrorSourceView {
1610
1703
  provider: string;
1611
1704
  /** Whether scrobbles from this source are being mirrored into Rocksky. */
1612
1705
  enabled: boolean;
1706
+ /** Whether Rocksky scrobbles are mirrored out to this source. Enabled unless the user turned it off. teal.fm only. */
1707
+ pushEnabled?: boolean;
1613
1708
  /** Username on the external service (Last.fm / ListenBrainz). Null for Teal.fm. */
1614
1709
  externalUsername?: string;
1615
1710
  /** True when an API key is stored. Last.fm/ListenBrainz only; always false for Teal.fm. */
@@ -1723,16 +1818,27 @@ export interface PlayFileParams {
1723
1818
  fileId: string;
1724
1819
  }
1725
1820
 
1821
+ export interface PlaylistCreatePlaylistOutput {
1822
+ /** The AT-URI of the created app.rocksky.playlist record. */
1823
+ uri: AtUri;
1824
+ /** The CID of the created app.rocksky.playlist record. */
1825
+ cid: string;
1826
+ }
1827
+
1726
1828
  export interface PlaylistCreatePlaylistParams {
1727
1829
  /** The name of the playlist */
1728
1830
  name: string;
1729
1831
  /** A brief description of the playlist */
1730
1832
  description?: string;
1833
+ /** The URL of the cover image for the playlist */
1834
+ pictureUrl?: Uri;
1731
1835
  }
1732
1836
 
1733
1837
  export interface PlaylistGetPlaylistParams {
1734
1838
  /** The URI of the playlist to retrieve. */
1735
1839
  uri: AtUri;
1840
+ /** RSQL filter expression applied to the playlist's tracks, 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, addedAt */
1841
+ filter?: string;
1736
1842
  }
1737
1843
 
1738
1844
  export interface PlaylistGetPlaylistsOutput {
@@ -1744,15 +1850,8 @@ export interface PlaylistGetPlaylistsParams {
1744
1850
  limit?: number;
1745
1851
  /** The offset for pagination, used to skip a number of playlists. */
1746
1852
  offset?: number;
1747
- }
1748
-
1749
- export interface PlaylistItemRecord {
1750
- subject: StrongRef;
1751
- /** The date the playlist was created. */
1752
- createdAt: DateTime;
1753
- track: SongViewBasic;
1754
- /** The order of the item in the playlist. */
1755
- order: number;
1853
+ /** RSQL filter expression, e.g. `name=="Road trip*";track.artist=="Daft Punk"`. Supports ==, !=, <, <=, >, >=, =in=, =out=, and `;`/`and`, `,`/`or` combinators, `*` wildcards in string values. Filterable fields: name, title, description, uri, spotifyLink, tidalLink, appleMusicLink, createdAt, updatedAt, curatorDid, curatorHandle, curatorName. The `track.title`, `track.artist`, `track.album` and `track.albumArtist` selectors match the playlist's contents, returning playlists that contain a matching track; several `track.*` terms joined with `;` must all be satisfied by the same track. */
1854
+ filter?: string;
1756
1855
  }
1757
1856
 
1758
1857
  export interface PlaylistRecord {
@@ -1776,6 +1875,45 @@ export interface PlaylistRecord {
1776
1875
  appleMusicLink?: string;
1777
1876
  }
1778
1877
 
1878
+ export interface PlaylistSongRecord {
1879
+ /** Strong reference (AT-URI + CID) to the parent app.rocksky.playlist record. */
1880
+ playlist: StrongRef;
1881
+ /** Strong reference (AT-URI + CID) to the app.rocksky.song record this entry points at. */
1882
+ song: StrongRef;
1883
+ /** The title of the song. */
1884
+ title: string;
1885
+ /** The artist of the song. */
1886
+ artist: string;
1887
+ /** The album the song belongs to. */
1888
+ album: string;
1889
+ /** The album artist of the song. */
1890
+ albumArtist: string;
1891
+ /** The duration of the song in milliseconds. */
1892
+ duration: number;
1893
+ /** The URL of the album art of the song. */
1894
+ albumArtUrl?: Uri;
1895
+ /** The date and time the song was added to the playlist. */
1896
+ addedAt: DateTime;
1897
+ }
1898
+
1899
+ export interface PlaylistUpdatePlaylistOutput {
1900
+ /** The AT-URI of the updated app.rocksky.playlist record. */
1901
+ uri: AtUri;
1902
+ /** The CID of the updated app.rocksky.playlist record. */
1903
+ cid: string;
1904
+ }
1905
+
1906
+ export interface PlaylistUpdatePlaylistParams {
1907
+ /** The URI of the playlist to update */
1908
+ uri: AtUri;
1909
+ /** The new name of the playlist */
1910
+ name?: string;
1911
+ /** The new description of the playlist */
1912
+ description?: string;
1913
+ /** The new cover image URL for the playlist */
1914
+ pictureUrl?: Uri;
1915
+ }
1916
+
1779
1917
  /** Basic view of a playlist, including its metadata */
1780
1918
  export interface PlaylistViewBasic {
1781
1919
  /** The unique identifier of the playlist. */
@@ -1800,6 +1938,8 @@ export interface PlaylistViewBasic {
1800
1938
  createdAt?: DateTime;
1801
1939
  /** The number of tracks in the playlist. */
1802
1940
  trackCount?: number;
1941
+ /** Album-art URLs of up to four of the playlist's tracks, for rendering a cover mosaic when the playlist has no picture of its own. */
1942
+ trackArts?: Uri[];
1803
1943
  }
1804
1944
 
1805
1945
  /** Detailed view of a playlist, including its tracks and metadata */
@@ -1858,6 +1998,8 @@ export interface PutMirrorSourceInput {
1858
1998
  provider: string;
1859
1999
  /** Enable or disable mirroring for this provider. */
1860
2000
  enabled?: boolean;
2001
+ /** Enable or disable mirroring Rocksky scrobbles out to this provider. teal.fm only. */
2002
+ pushEnabled?: boolean;
1861
2003
  /** External username (Last.fm / ListenBrainz). Required when enabling those providers. Ignored for Teal.fm. */
1862
2004
  externalUsername?: string;
1863
2005
  /** API key / token to be encrypted at rest. Omit to leave the existing key unchanged. Pass an empty string to clear it. */
@@ -1929,8 +2071,10 @@ export interface RemoveShoutParams {
1929
2071
  export interface RemoveTrackParams {
1930
2072
  /** The URI of the playlist to remove the track from */
1931
2073
  uri: AtUri;
1932
- /** The position of the track to remove in the playlist */
1933
- position: number;
2074
+ /** The URI of the app.rocksky.song record to remove. Removes every copy of it; pass `index` instead to remove one. */
2075
+ songUri?: AtUri;
2076
+ /** 0-based position of the entry to remove, in the order getPlaylist returns. */
2077
+ index?: number;
1934
2078
  }
1935
2079
 
1936
2080
  export interface ReplyShoutInput {
@@ -2166,6 +2310,8 @@ export interface ScrobbleViewDetailed {
2166
2310
  /** The SHA256 hash of the scrobble data. */
2167
2311
  sha256?: string;
2168
2312
  liked?: boolean;
2313
+ /** The URI of the track (song) this scrobble is of. */
2314
+ trackUri?: string;
2169
2315
  likesCount?: number;
2170
2316
  /** The number of listeners */
2171
2317
  listeners?: number;
@@ -2405,6 +2551,10 @@ export interface SongViewBasic {
2405
2551
  discNumber?: number;
2406
2552
  /** The number of times the song has been played. */
2407
2553
  playCount?: number;
2554
+ /** The number of users who have loved this song. */
2555
+ likesCount?: number;
2556
+ /** Whether the authenticated user has loved this song. False when unauthenticated. */
2557
+ liked?: boolean;
2408
2558
  /** The number of unique listeners who have played the song. */
2409
2559
  uniqueListeners?: number;
2410
2560
  /** The URI of the album the song belongs to. */
@@ -2445,6 +2595,10 @@ export interface SongViewDetailed {
2445
2595
  discNumber?: number;
2446
2596
  /** The number of times the song has been played. */
2447
2597
  playCount?: number;
2598
+ /** The number of users who have loved this song. */
2599
+ likesCount?: number;
2600
+ /** Whether the authenticated user has loved this song. False when unauthenticated. */
2601
+ liked?: boolean;
2448
2602
  /** The number of unique listeners who have played the song. */
2449
2603
  uniqueListeners?: number;
2450
2604
  /** The URI of the album the song belongs to. */
@@ -2714,23 +2868,6 @@ export interface UpdateNowPlayingOutput {
2714
2868
 
2715
2869
  }
2716
2870
 
2717
- export interface UpdatePlaylistInput {
2718
- /** The playlist id to update. */
2719
- playlistId: string;
2720
- /** New playlist name. */
2721
- name?: string;
2722
- /** New playlist comment. */
2723
- comment?: string;
2724
- /** A song id to add to the playlist. */
2725
- songIdToAdd?: string;
2726
- /** A track index to remove from the playlist. */
2727
- songIndexToRemove?: number;
2728
- }
2729
-
2730
- export interface UpdatePlaylistOutput {
2731
-
2732
- }
2733
-
2734
2871
  export interface UpdateSeenInput {
2735
2872
  /** The ids of the notifications to mark as viewed. Omit to mark all. */
2736
2873
  ids?: string[];
@@ -2770,8 +2907,10 @@ export interface Endpoints {
2770
2907
  "app.rocksky.artist.getArtistRecentListeners": GetArtistRecentListenersOutput;
2771
2908
  "app.rocksky.artist.getArtists": ArtistGetArtistsOutput;
2772
2909
  "app.rocksky.artist.getArtistTracks": GetArtistTracksOutput;
2910
+ "app.rocksky.charts.getDecades": GetDecadesOutput;
2773
2911
  "app.rocksky.charts.getScrobblesChart": ChartsView;
2774
2912
  "app.rocksky.charts.getTopArtists": GetTopArtistsOutput;
2913
+ "app.rocksky.charts.getTopScrobblers": GetTopScrobblersOutput;
2775
2914
  "app.rocksky.charts.getTopTracks": GetTopTracksOutput;
2776
2915
  "app.rocksky.dropbox.downloadFile": void;
2777
2916
  "app.rocksky.dropbox.getFiles": DropboxFileListView;
@@ -2835,7 +2974,7 @@ export interface Endpoints {
2835
2974
  "app.rocksky.library.startScan": StartScanOutput;
2836
2975
  "app.rocksky.library.unstar": UnstarOutput;
2837
2976
  "app.rocksky.library.updateNowPlaying": UpdateNowPlayingOutput;
2838
- "app.rocksky.library.updatePlaylist": UpdatePlaylistOutput;
2977
+ "app.rocksky.library.updatePlaylist": LibraryUpdatePlaylistOutput;
2839
2978
  "app.rocksky.like.dislikeShout": ShoutView;
2840
2979
  "app.rocksky.like.dislikeSong": SongViewDetailed;
2841
2980
  "app.rocksky.like.likeShout": ShoutView;
@@ -2856,7 +2995,8 @@ export interface Endpoints {
2856
2995
  "app.rocksky.player.playFile": void;
2857
2996
  "app.rocksky.player.previous": void;
2858
2997
  "app.rocksky.player.seek": void;
2859
- "app.rocksky.playlist.createPlaylist": void;
2998
+ "app.rocksky.playlist.addSongs": AddSongsOutput;
2999
+ "app.rocksky.playlist.createPlaylist": PlaylistCreatePlaylistOutput;
2860
3000
  "app.rocksky.playlist.getPlaylist": PlaylistViewDetailed;
2861
3001
  "app.rocksky.playlist.getPlaylists": PlaylistGetPlaylistsOutput;
2862
3002
  "app.rocksky.playlist.insertDirectory": void;
@@ -2864,6 +3004,7 @@ export interface Endpoints {
2864
3004
  "app.rocksky.playlist.removePlaylist": void;
2865
3005
  "app.rocksky.playlist.removeTrack": void;
2866
3006
  "app.rocksky.playlist.startPlaylist": void;
3007
+ "app.rocksky.playlist.updatePlaylist": PlaylistUpdatePlaylistOutput;
2867
3008
  "app.rocksky.rockbox.getAudioSettings": RockboxSettingsView;
2868
3009
  "app.rocksky.rockbox.putAudioSettings": RockboxSettingsView;
2869
3010
  "app.rocksky.scrobble.createScrobble": ScrobbleViewBasic;