@rocksky/sdk 0.13.0 → 0.14.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/README.md +4 -2
- package/dist/client.d.ts +49 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/dedup.js +215 -0
- package/dist/errors.d.ts +5 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/generated/types.d.ts +83 -27
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -209
- package/dist/library.d.ts +101 -11
- package/dist/library.d.ts.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/package.json +8 -2
- package/src/agent.ts +3 -3
- package/src/client.test.ts +83 -0
- package/src/client.ts +114 -7
- package/src/errors.ts +7 -2
- package/src/generated/types.ts +92 -31
- package/src/hash.ts +4 -2
- package/src/index.ts +11 -2
- package/src/library.ts +117 -21
- package/src/remote.ts +4 -5
|
@@ -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
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
ArtistViewBasic,
|
|
12
12
|
ArtistViewDetailed,
|
|
13
13
|
ChartsView,
|
|
14
|
+
CreateScrobbleInput,
|
|
15
|
+
FollowAccountOutput,
|
|
14
16
|
FeedGeneratorsView,
|
|
15
17
|
FeedRecommendationsView,
|
|
16
18
|
FeedRecommendedAlbumsView,
|
|
@@ -37,10 +39,13 @@ import type {
|
|
|
37
39
|
GetTrackShoutsOutput,
|
|
38
40
|
GetUnreadCountOutput,
|
|
39
41
|
ListNotificationsOutput,
|
|
42
|
+
MirrorSourceView,
|
|
40
43
|
PlayerCurrentlyPlayingViewDetailed,
|
|
41
44
|
PlayerPlaybackQueueViewDetailed,
|
|
42
45
|
PlaylistGetPlaylistsOutput,
|
|
43
46
|
PlaylistViewDetailed,
|
|
47
|
+
PutAudioSettingsInput,
|
|
48
|
+
PutMirrorSourceInput,
|
|
44
49
|
RockboxSettingsView,
|
|
45
50
|
ScrobbleViewBasic,
|
|
46
51
|
SongViewBasic,
|
|
@@ -48,6 +53,10 @@ import type {
|
|
|
48
53
|
StatsGlobalStatsView,
|
|
49
54
|
StatsView,
|
|
50
55
|
StatsWrappedView,
|
|
56
|
+
PlaylistCreatePlaylistOutput,
|
|
57
|
+
PlaylistUpdatePlaylistOutput,
|
|
58
|
+
AddSongsOutput,
|
|
59
|
+
UnfollowAccountOutput,
|
|
51
60
|
UpdateSeenOutput,
|
|
52
61
|
} from "./generated/types.js";
|
|
53
62
|
|
|
@@ -107,11 +116,16 @@ export class RockskyClient {
|
|
|
107
116
|
let handler = simpleFetchHandler({ service: appview });
|
|
108
117
|
if (token) {
|
|
109
118
|
const inner = handler;
|
|
110
|
-
handler = ((pathname: string, init?: RequestInit) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
handler = ((pathname: string, init?: RequestInit) => {
|
|
120
|
+
// `new Headers(init.headers)`, not a spread: atcute hands us a Headers
|
|
121
|
+
// instance, and a Headers has no own enumerable properties — spreading
|
|
122
|
+
// it yields `{}` and silently drops everything already set. That threw
|
|
123
|
+
// away the `content-type: application/json` atcute adds for a JSON
|
|
124
|
+
// body, so the AppView saw text/plain and rejected the request.
|
|
125
|
+
const headers = new Headers(init?.headers);
|
|
126
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
127
|
+
return inner(pathname, { ...init, headers });
|
|
128
|
+
}) as typeof handler;
|
|
115
129
|
}
|
|
116
130
|
this.rpc = new Client({ handler });
|
|
117
131
|
}
|
|
@@ -136,7 +150,7 @@ export class RockskyClient {
|
|
|
136
150
|
if (v !== undefined && v !== "") clean[k] = v;
|
|
137
151
|
}
|
|
138
152
|
const res = await this.rpc.get(nsid as never, { params: clean } as never);
|
|
139
|
-
if (!res.ok) throw new RockskyError(res.data);
|
|
153
|
+
if (!res.ok) throw new RockskyError(res.data, res.status);
|
|
140
154
|
return res.data as T;
|
|
141
155
|
}
|
|
142
156
|
|
|
@@ -161,6 +175,24 @@ export class RockskyClient {
|
|
|
161
175
|
return this.query(nsid, params);
|
|
162
176
|
}
|
|
163
177
|
|
|
178
|
+
/** Call any AppView procedure by nsid. `params` ride the query string (some
|
|
179
|
+
* procedures take their arguments there), `body` is the JSON input — omitted
|
|
180
|
+
* entirely when `undefined`. Escape hatch for procedures without a wrapper. */
|
|
181
|
+
async post<T = unknown>(
|
|
182
|
+
nsid: string,
|
|
183
|
+
opts: { params?: Record<string, unknown>; body?: unknown } = {},
|
|
184
|
+
): Promise<T> {
|
|
185
|
+
const clean: Record<string, unknown> = {};
|
|
186
|
+
for (const [k, v] of Object.entries(opts.params ?? {})) {
|
|
187
|
+
if (v !== undefined && v !== "") clean[k] = v;
|
|
188
|
+
}
|
|
189
|
+
const call: Record<string, unknown> = { params: clean };
|
|
190
|
+
if (opts.body !== undefined) call.input = opts.body;
|
|
191
|
+
const res = await this.rpc.post(nsid as never, call as never);
|
|
192
|
+
if (!res.ok) throw new RockskyError(res.data, res.status);
|
|
193
|
+
return res.data as T;
|
|
194
|
+
}
|
|
195
|
+
|
|
164
196
|
/** An actor's most-played songs. */
|
|
165
197
|
async songs(actor: string, limit = 50, offset = 0): Promise<SongViewBasic[]> {
|
|
166
198
|
const out = await this.query<{ tracks?: SongViewBasic[] }>("app.rocksky.actor.getActorSongs", {
|
|
@@ -492,6 +524,54 @@ export class RockskyClient {
|
|
|
492
524
|
playlist(uri: string): Promise<PlaylistViewDetailed> {
|
|
493
525
|
return this.query("app.rocksky.playlist.getPlaylist", { uri });
|
|
494
526
|
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Create a playlist (`app.rocksky.playlist.createPlaylist`). Auth required.
|
|
530
|
+
* Publishes an app.rocksky.playlist record to the caller's repo; the AppView
|
|
531
|
+
* only lists it once the commit has been ingested.
|
|
532
|
+
*/
|
|
533
|
+
createPlaylist(input: {
|
|
534
|
+
name: string;
|
|
535
|
+
description?: string;
|
|
536
|
+
pictureUrl?: string;
|
|
537
|
+
}): Promise<PlaylistCreatePlaylistOutput> {
|
|
538
|
+
return this.post("app.rocksky.playlist.createPlaylist", { params: input });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Rename or re-describe a playlist (`app.rocksky.playlist.updatePlaylist`). Owner only. */
|
|
542
|
+
updatePlaylist(input: {
|
|
543
|
+
uri: string;
|
|
544
|
+
name?: string;
|
|
545
|
+
description?: string;
|
|
546
|
+
pictureUrl?: string;
|
|
547
|
+
}): Promise<PlaylistUpdatePlaylistOutput> {
|
|
548
|
+
return this.post("app.rocksky.playlist.updatePlaylist", { params: input });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Add songs to a playlist (`app.rocksky.playlist.addSongs`). Owner only.
|
|
553
|
+
* `songs` are app.rocksky.song AT-URIs; returns the created entry URIs.
|
|
554
|
+
*/
|
|
555
|
+
addSongs(uri: string, songs: string[]): Promise<AddSongsOutput> {
|
|
556
|
+
return this.post("app.rocksky.playlist.addSongs", {
|
|
557
|
+
params: { uri, songs },
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Delete a playlist and the caller's own entries (`app.rocksky.playlist.removePlaylist`). Owner only. */
|
|
562
|
+
removePlaylist(uri: string): Promise<void> {
|
|
563
|
+
return this.post("app.rocksky.playlist.removePlaylist", { params: { uri } });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Remove a song from a playlist (`app.rocksky.playlist.removeTrack`). An
|
|
568
|
+
* entry can only be retracted by the repo that published it.
|
|
569
|
+
*/
|
|
570
|
+
removeTrack(uri: string, songUri: string): Promise<void> {
|
|
571
|
+
return this.post("app.rocksky.playlist.removeTrack", {
|
|
572
|
+
params: { uri, songUri },
|
|
573
|
+
});
|
|
574
|
+
}
|
|
495
575
|
/** Shouts on an album. */
|
|
496
576
|
albumShouts(uri: string, limit = 50, offset = 0): Promise<GetAlbumShoutsOutput> {
|
|
497
577
|
return this.query("app.rocksky.shout.getAlbumShouts", { uri, limit, offset });
|
|
@@ -539,7 +619,34 @@ export class RockskyClient {
|
|
|
539
619
|
const res = await this.rpc.post("app.rocksky.notification.updateSeen" as never, {
|
|
540
620
|
input: (ids && ids.length ? { ids } : {}) as never,
|
|
541
621
|
} as never);
|
|
542
|
-
if (!res.ok) throw new RockskyError(res.data);
|
|
622
|
+
if (!res.ok) throw new RockskyError(res.data, res.status);
|
|
543
623
|
return res.data as UpdateSeenOutput;
|
|
544
624
|
}
|
|
625
|
+
|
|
626
|
+
/** Follow an account by DID or handle (`app.rocksky.graph.followAccount`). Auth required. */
|
|
627
|
+
followAccount(account: string): Promise<FollowAccountOutput> {
|
|
628
|
+
return this.post("app.rocksky.graph.followAccount", { params: { account } });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Unfollow an account by DID or handle (`app.rocksky.graph.unfollowAccount`). Auth required. */
|
|
632
|
+
unfollowAccount(account: string): Promise<UnfollowAccountOutput> {
|
|
633
|
+
return this.post("app.rocksky.graph.unfollowAccount", { params: { account } });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Submit a scrobble through the AppView (`app.rocksky.scrobble.createScrobble`).
|
|
637
|
+
* Auth required. For direct-to-PDS scrobbling use {@link Agent} instead. */
|
|
638
|
+
createScrobble(input: CreateScrobbleInput): Promise<ScrobbleViewBasic> {
|
|
639
|
+
return this.post("app.rocksky.scrobble.createScrobble", { body: input });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Create or update a mirror source (`app.rocksky.mirror.putMirrorSource`). Auth required. */
|
|
643
|
+
putMirrorSource(input: PutMirrorSourceInput): Promise<MirrorSourceView> {
|
|
644
|
+
return this.post("app.rocksky.mirror.putMirrorSource", { body: input });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Patch the viewer's Rockbox audio settings (`app.rocksky.rockbox.putAudioSettings`).
|
|
648
|
+
* Auth required. */
|
|
649
|
+
putAudioSettings(input: PutAudioSettingsInput): Promise<RockboxSettingsView> {
|
|
650
|
+
return this.post("app.rocksky.rockbox.putAudioSettings", { body: input });
|
|
651
|
+
}
|
|
545
652
|
}
|
package/src/errors.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
/** Error thrown when a Rocksky XRPC call returns a non-2xx `{ error, message }`. */
|
|
2
1
|
export class RockskyError extends Error {
|
|
2
|
+
/** The XRPC error code (e.g. "InvalidRequest", "AuthMissing"), when the
|
|
3
|
+
* server sent one. */
|
|
3
4
|
readonly kind?: string;
|
|
4
|
-
|
|
5
|
+
/** The HTTP status of the failed response, when known. */
|
|
6
|
+
readonly status?: number;
|
|
7
|
+
|
|
8
|
+
constructor(payload: unknown, status?: number) {
|
|
5
9
|
const p = payload as { error?: string; message?: string } | undefined;
|
|
6
10
|
super(p?.message || p?.error || "rocksky request failed");
|
|
7
11
|
this.name = "RockskyError";
|
|
8
12
|
this.kind = p?.error;
|
|
13
|
+
this.status = status;
|
|
9
14
|
}
|
|
10
15
|
}
|
package/src/generated/types.ts
CHANGED
|
@@ -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;
|
|
@@ -755,10 +767,12 @@ export interface GetActorPlaylistsOutput {
|
|
|
755
767
|
export interface GetActorPlaylistsParams {
|
|
756
768
|
/** The DID or handle of the actor */
|
|
757
769
|
did: AtIdentifier;
|
|
758
|
-
/** The maximum number of
|
|
770
|
+
/** The maximum number of playlists to return */
|
|
759
771
|
limit?: number;
|
|
760
772
|
/** The offset for pagination */
|
|
761
773
|
offset?: number;
|
|
774
|
+
/** 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. */
|
|
775
|
+
filter?: string;
|
|
762
776
|
}
|
|
763
777
|
|
|
764
778
|
export interface GetActorScrobblesOutput {
|
|
@@ -1565,6 +1579,23 @@ export interface LibrarySearchParams {
|
|
|
1565
1579
|
songOffset?: number;
|
|
1566
1580
|
}
|
|
1567
1581
|
|
|
1582
|
+
export interface LibraryUpdatePlaylistInput {
|
|
1583
|
+
/** The playlist id to update. */
|
|
1584
|
+
playlistId: string;
|
|
1585
|
+
/** New playlist name. */
|
|
1586
|
+
name?: string;
|
|
1587
|
+
/** New playlist comment. */
|
|
1588
|
+
comment?: string;
|
|
1589
|
+
/** A song id to add to the playlist. */
|
|
1590
|
+
songIdToAdd?: string;
|
|
1591
|
+
/** A track index to remove from the playlist. */
|
|
1592
|
+
songIndexToRemove?: number;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
export interface LibraryUpdatePlaylistOutput {
|
|
1596
|
+
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1568
1599
|
export interface LikeRecord {
|
|
1569
1600
|
/** The date when the like was created. */
|
|
1570
1601
|
createdAt: DateTime;
|
|
@@ -1723,16 +1754,27 @@ export interface PlayFileParams {
|
|
|
1723
1754
|
fileId: string;
|
|
1724
1755
|
}
|
|
1725
1756
|
|
|
1757
|
+
export interface PlaylistCreatePlaylistOutput {
|
|
1758
|
+
/** The AT-URI of the created app.rocksky.playlist record. */
|
|
1759
|
+
uri: AtUri;
|
|
1760
|
+
/** The CID of the created app.rocksky.playlist record. */
|
|
1761
|
+
cid: string;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1726
1764
|
export interface PlaylistCreatePlaylistParams {
|
|
1727
1765
|
/** The name of the playlist */
|
|
1728
1766
|
name: string;
|
|
1729
1767
|
/** A brief description of the playlist */
|
|
1730
1768
|
description?: string;
|
|
1769
|
+
/** The URL of the cover image for the playlist */
|
|
1770
|
+
pictureUrl?: Uri;
|
|
1731
1771
|
}
|
|
1732
1772
|
|
|
1733
1773
|
export interface PlaylistGetPlaylistParams {
|
|
1734
1774
|
/** The URI of the playlist to retrieve. */
|
|
1735
1775
|
uri: AtUri;
|
|
1776
|
+
/** 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 */
|
|
1777
|
+
filter?: string;
|
|
1736
1778
|
}
|
|
1737
1779
|
|
|
1738
1780
|
export interface PlaylistGetPlaylistsOutput {
|
|
@@ -1744,15 +1786,8 @@ export interface PlaylistGetPlaylistsParams {
|
|
|
1744
1786
|
limit?: number;
|
|
1745
1787
|
/** The offset for pagination, used to skip a number of playlists. */
|
|
1746
1788
|
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;
|
|
1789
|
+
/** 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. */
|
|
1790
|
+
filter?: string;
|
|
1756
1791
|
}
|
|
1757
1792
|
|
|
1758
1793
|
export interface PlaylistRecord {
|
|
@@ -1776,6 +1811,45 @@ export interface PlaylistRecord {
|
|
|
1776
1811
|
appleMusicLink?: string;
|
|
1777
1812
|
}
|
|
1778
1813
|
|
|
1814
|
+
export interface PlaylistSongRecord {
|
|
1815
|
+
/** Strong reference (AT-URI + CID) to the parent app.rocksky.playlist record. */
|
|
1816
|
+
playlist: StrongRef;
|
|
1817
|
+
/** Strong reference (AT-URI + CID) to the app.rocksky.song record this entry points at. */
|
|
1818
|
+
song: StrongRef;
|
|
1819
|
+
/** The title of the song. */
|
|
1820
|
+
title: string;
|
|
1821
|
+
/** The artist of the song. */
|
|
1822
|
+
artist: string;
|
|
1823
|
+
/** The album the song belongs to. */
|
|
1824
|
+
album: string;
|
|
1825
|
+
/** The album artist of the song. */
|
|
1826
|
+
albumArtist: string;
|
|
1827
|
+
/** The duration of the song in milliseconds. */
|
|
1828
|
+
duration: number;
|
|
1829
|
+
/** The URL of the album art of the song. */
|
|
1830
|
+
albumArtUrl?: Uri;
|
|
1831
|
+
/** The date and time the song was added to the playlist. */
|
|
1832
|
+
addedAt: DateTime;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
export interface PlaylistUpdatePlaylistOutput {
|
|
1836
|
+
/** The AT-URI of the updated app.rocksky.playlist record. */
|
|
1837
|
+
uri: AtUri;
|
|
1838
|
+
/** The CID of the updated app.rocksky.playlist record. */
|
|
1839
|
+
cid: string;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
export interface PlaylistUpdatePlaylistParams {
|
|
1843
|
+
/** The URI of the playlist to update */
|
|
1844
|
+
uri: AtUri;
|
|
1845
|
+
/** The new name of the playlist */
|
|
1846
|
+
name?: string;
|
|
1847
|
+
/** The new description of the playlist */
|
|
1848
|
+
description?: string;
|
|
1849
|
+
/** The new cover image URL for the playlist */
|
|
1850
|
+
pictureUrl?: Uri;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1779
1853
|
/** Basic view of a playlist, including its metadata */
|
|
1780
1854
|
export interface PlaylistViewBasic {
|
|
1781
1855
|
/** The unique identifier of the playlist. */
|
|
@@ -1800,6 +1874,8 @@ export interface PlaylistViewBasic {
|
|
|
1800
1874
|
createdAt?: DateTime;
|
|
1801
1875
|
/** The number of tracks in the playlist. */
|
|
1802
1876
|
trackCount?: number;
|
|
1877
|
+
/** 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. */
|
|
1878
|
+
trackArts?: Uri[];
|
|
1803
1879
|
}
|
|
1804
1880
|
|
|
1805
1881
|
/** Detailed view of a playlist, including its tracks and metadata */
|
|
@@ -1929,8 +2005,8 @@ export interface RemoveShoutParams {
|
|
|
1929
2005
|
export interface RemoveTrackParams {
|
|
1930
2006
|
/** The URI of the playlist to remove the track from */
|
|
1931
2007
|
uri: AtUri;
|
|
1932
|
-
/** The
|
|
1933
|
-
|
|
2008
|
+
/** The URI of the app.rocksky.song record to remove from the playlist */
|
|
2009
|
+
songUri: AtUri;
|
|
1934
2010
|
}
|
|
1935
2011
|
|
|
1936
2012
|
export interface ReplyShoutInput {
|
|
@@ -2714,23 +2790,6 @@ export interface UpdateNowPlayingOutput {
|
|
|
2714
2790
|
|
|
2715
2791
|
}
|
|
2716
2792
|
|
|
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
2793
|
export interface UpdateSeenInput {
|
|
2735
2794
|
/** The ids of the notifications to mark as viewed. Omit to mark all. */
|
|
2736
2795
|
ids?: string[];
|
|
@@ -2835,7 +2894,7 @@ export interface Endpoints {
|
|
|
2835
2894
|
"app.rocksky.library.startScan": StartScanOutput;
|
|
2836
2895
|
"app.rocksky.library.unstar": UnstarOutput;
|
|
2837
2896
|
"app.rocksky.library.updateNowPlaying": UpdateNowPlayingOutput;
|
|
2838
|
-
"app.rocksky.library.updatePlaylist":
|
|
2897
|
+
"app.rocksky.library.updatePlaylist": LibraryUpdatePlaylistOutput;
|
|
2839
2898
|
"app.rocksky.like.dislikeShout": ShoutView;
|
|
2840
2899
|
"app.rocksky.like.dislikeSong": SongViewDetailed;
|
|
2841
2900
|
"app.rocksky.like.likeShout": ShoutView;
|
|
@@ -2856,7 +2915,8 @@ export interface Endpoints {
|
|
|
2856
2915
|
"app.rocksky.player.playFile": void;
|
|
2857
2916
|
"app.rocksky.player.previous": void;
|
|
2858
2917
|
"app.rocksky.player.seek": void;
|
|
2859
|
-
"app.rocksky.playlist.
|
|
2918
|
+
"app.rocksky.playlist.addSongs": AddSongsOutput;
|
|
2919
|
+
"app.rocksky.playlist.createPlaylist": PlaylistCreatePlaylistOutput;
|
|
2860
2920
|
"app.rocksky.playlist.getPlaylist": PlaylistViewDetailed;
|
|
2861
2921
|
"app.rocksky.playlist.getPlaylists": PlaylistGetPlaylistsOutput;
|
|
2862
2922
|
"app.rocksky.playlist.insertDirectory": void;
|
|
@@ -2864,6 +2924,7 @@ export interface Endpoints {
|
|
|
2864
2924
|
"app.rocksky.playlist.removePlaylist": void;
|
|
2865
2925
|
"app.rocksky.playlist.removeTrack": void;
|
|
2866
2926
|
"app.rocksky.playlist.startPlaylist": void;
|
|
2927
|
+
"app.rocksky.playlist.updatePlaylist": PlaylistUpdatePlaylistOutput;
|
|
2867
2928
|
"app.rocksky.rockbox.getAudioSettings": RockboxSettingsView;
|
|
2868
2929
|
"app.rocksky.rockbox.putAudioSettings": RockboxSettingsView;
|
|
2869
2930
|
"app.rocksky.scrobble.createScrobble": ScrobbleViewBasic;
|
package/src/hash.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
2
3
|
|
|
3
4
|
// Lowercase-hex SHA-256 of s.toLowerCase() — the lowercasing is applied to the
|
|
4
5
|
// whole string (not per field), matching Rocksky's server and every other SDK.
|
|
6
|
+
// Pure-JS digest (@noble/hashes) so this module stays browser-safe.
|
|
5
7
|
function sha256Lower(s: string): string {
|
|
6
|
-
return
|
|
8
|
+
return bytesToHex(sha256(new TextEncoder().encode(s.toLowerCase())));
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/** Identity hash of a song: sha256(lower("{title} - {artist} - {album}")). */
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { RockskyClient, DEFAULT_APPVIEW, Interval } from "./client.js";
|
|
12
12
|
export type { DateInterval } from "./client.js";
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
RockskyLibrary,
|
|
15
|
+
type LibrarySong,
|
|
16
|
+
type LibraryPlaylist,
|
|
17
|
+
type LibraryPlaylistMutation,
|
|
18
|
+
type LibraryPlaylistResponse,
|
|
19
|
+
type LibraryPlaylistsResponse,
|
|
20
|
+
} from "./library.js";
|
|
14
21
|
export {
|
|
15
22
|
Agent,
|
|
16
23
|
MAX_SAFE_WRITES_PER_HOUR,
|
|
@@ -23,7 +30,9 @@ export {
|
|
|
23
30
|
type RateLimitOptions,
|
|
24
31
|
type RateLimitState,
|
|
25
32
|
} from "./agent.js";
|
|
26
|
-
|
|
33
|
+
// The dedup index (classic-level, Node-only) lives on the `@rocksky/sdk/dedup`
|
|
34
|
+
// subpath so this root entry stays browser-safe.
|
|
35
|
+
export type { IndexStats, RockskyIndex } from "./dedup.js";
|
|
27
36
|
export { runJetstream, DEFAULT_JETSTREAM_SERVERS, type JetstreamOptions } from "./jetstream.js";
|
|
28
37
|
export {
|
|
29
38
|
RemotePlayer,
|