@rocksky/sdk 0.2.2 → 0.4.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 +44 -254
- package/dist/agent.d.ts +77 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/client.d.ts +26 -113
- package/dist/client.d.ts.map +1 -1
- package/dist/dedup.d.ts +38 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/errors.d.ts +3 -23
- package/dist/errors.d.ts.map +1 -1
- package/dist/generated/types.d.ts +113 -11
- package/dist/generated/types.d.ts.map +1 -1
- package/dist/hash.d.ts +7 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/index.d.ts +16 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6648 -1288
- package/dist/jetstream.d.ts +17 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/package.json +15 -11
- package/src/agent.ts +232 -0
- package/src/client.ts +82 -226
- package/src/dedup.ts +207 -0
- package/src/errors.ts +6 -43
- package/src/generated/types.ts +122 -11
- package/src/hash.ts +22 -0
- package/src/index.ts +16 -86
- package/src/jetstream.ts +122 -0
- package/src/http.ts +0 -195
- package/src/namespaces/_helpers.ts +0 -27
- package/src/namespaces/actor.ts +0 -93
- package/src/namespaces/album.ts +0 -34
- package/src/namespaces/apikey.ts +0 -50
- package/src/namespaces/artist.ts +0 -68
- package/src/namespaces/charts.ts +0 -38
- package/src/namespaces/dropbox.ts +0 -53
- package/src/namespaces/feed.ts +0 -112
- package/src/namespaces/googledrive.ts +0 -41
- package/src/namespaces/graph.ts +0 -62
- package/src/namespaces/like.ts +0 -46
- package/src/namespaces/mirror.ts +0 -27
- package/src/namespaces/player.ts +0 -125
- package/src/namespaces/playlist.ts +0 -95
- package/src/namespaces/scrobble.ts +0 -41
- package/src/namespaces/shout.ts +0 -99
- package/src/namespaces/song.ts +0 -60
- package/src/namespaces/spotify.ts +0 -56
- package/src/namespaces/stats.ts +0 -27
- package/src/paginate.ts +0 -90
- package/src/pipe.ts +0 -146
- package/src/realtime.ts +0 -408
- package/src/types.ts +0 -41
package/src/dedup.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { fromUint8Array } from "@atcute/car";
|
|
2
|
+
import { decode as cborDecode, fromBytes, isBytes, isCidLink } from "@atcute/cbor";
|
|
3
|
+
import { toString as cidToString } from "@atcute/cid";
|
|
4
|
+
import { ClassicLevel } from "classic-level";
|
|
5
|
+
|
|
6
|
+
import { albumHash, artistHash, songHash } from "./hash.js";
|
|
7
|
+
|
|
8
|
+
const C_ARTIST = "app.rocksky.artist";
|
|
9
|
+
const C_ALBUM = "app.rocksky.album";
|
|
10
|
+
const C_SONG = "app.rocksky.song";
|
|
11
|
+
const C_SCROBBLE = "app.rocksky.scrobble";
|
|
12
|
+
const SEP = "\x00";
|
|
13
|
+
|
|
14
|
+
/** What an {@link RockskyIndex.indexCar} pass added. */
|
|
15
|
+
export interface IndexStats {
|
|
16
|
+
artists: number;
|
|
17
|
+
albums: number;
|
|
18
|
+
songs: number;
|
|
19
|
+
scrobbles: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function totalIndexed(s: IndexStats): number {
|
|
23
|
+
return s.artists + s.albums + s.songs + s.scrobbles;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function identKey(did: string, col: string, hash: string): string {
|
|
27
|
+
return `${did}${SEP}${col}${SEP}${hash}`;
|
|
28
|
+
}
|
|
29
|
+
function scrobbleKey(did: string, songH: string, secs: number): string {
|
|
30
|
+
return `${did}${SEP}${C_SCROBBLE}${SEP}${songH}${SEP}${secs}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function primaryFor(did: string, col: string, rec: Record<string, unknown>): string | undefined {
|
|
34
|
+
const s = (k: string) => (typeof rec[k] === "string" ? (rec[k] as string) : "");
|
|
35
|
+
switch (col) {
|
|
36
|
+
case C_ARTIST:
|
|
37
|
+
return s("name") ? identKey(did, C_ARTIST, artistHash(s("name"))) : undefined;
|
|
38
|
+
case C_ALBUM:
|
|
39
|
+
return s("title") && s("artist") ? identKey(did, C_ALBUM, albumHash(s("title"), s("artist"))) : undefined;
|
|
40
|
+
case C_SONG:
|
|
41
|
+
return s("title") && s("artist") && s("album")
|
|
42
|
+
? identKey(did, C_SONG, songHash(s("title"), s("artist"), s("album")))
|
|
43
|
+
: undefined;
|
|
44
|
+
case C_SCROBBLE: {
|
|
45
|
+
if (s("title") && s("artist") && s("album") && s("createdAt")) {
|
|
46
|
+
const secs = Math.floor(Date.parse(s("createdAt")) / 1000);
|
|
47
|
+
if (!Number.isNaN(secs)) return scrobbleKey(did, songHash(s("title"), s("artist"), s("album")), secs);
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Local duplicate-prevention mirror of a user's repo, keyed by Rocksky's
|
|
57
|
+
* identity hashes, backed by an embedded LevelDB (classic-level). Built from the
|
|
58
|
+
* repo CAR by {@link Agent.syncRepo} and kept live by {@link Agent.hydrateFromJetstream}.
|
|
59
|
+
*/
|
|
60
|
+
export class RockskyIndex {
|
|
61
|
+
private db: ClassicLevel<string, string>;
|
|
62
|
+
|
|
63
|
+
constructor(path: string) {
|
|
64
|
+
this.db = new ClassicLevel<string, string>(path, { keyEncoding: "utf8", valueEncoding: "utf8" });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Open the database (call before use). */
|
|
68
|
+
open(): Promise<void> {
|
|
69
|
+
return this.db.open();
|
|
70
|
+
}
|
|
71
|
+
/** Close the database. */
|
|
72
|
+
close(): Promise<void> {
|
|
73
|
+
return this.db.close();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async get(key: string): Promise<string | undefined> {
|
|
77
|
+
try {
|
|
78
|
+
return await this.db.get(key);
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined; // NotFound
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
songUri(did: string, title: string, artist: string, album: string): Promise<string | undefined> {
|
|
85
|
+
return this.get(identKey(did, C_SONG, songHash(title, artist, album)));
|
|
86
|
+
}
|
|
87
|
+
albumUri(did: string, album: string, albumArtist: string): Promise<string | undefined> {
|
|
88
|
+
return this.get(identKey(did, C_ALBUM, albumHash(album, albumArtist)));
|
|
89
|
+
}
|
|
90
|
+
artistUri(did: string, albumArtist: string): Promise<string | undefined> {
|
|
91
|
+
return this.get(identKey(did, C_ARTIST, artistHash(albumArtist)));
|
|
92
|
+
}
|
|
93
|
+
scrobbleUri(did: string, title: string, artist: string, album: string, secs: number): Promise<string | undefined> {
|
|
94
|
+
return this.get(scrobbleKey(did, songHash(title, artist, album), secs));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async putPrimary(did: string, col: string, primary: string, uri: string): Promise<void> {
|
|
98
|
+
const rkey = uri.slice(uri.lastIndexOf("/") + 1);
|
|
99
|
+
await this.db.batch([
|
|
100
|
+
{ type: "put", key: primary, value: uri },
|
|
101
|
+
{ type: "put", key: `${SEP}rk${SEP}${did}${SEP}${col}${SEP}${rkey}`, value: primary },
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
recordSong(did: string, title: string, artist: string, album: string, uri: string): Promise<void> {
|
|
105
|
+
return this.putPrimary(did, C_SONG, identKey(did, C_SONG, songHash(title, artist, album)), uri);
|
|
106
|
+
}
|
|
107
|
+
recordAlbum(did: string, album: string, albumArtist: string, uri: string): Promise<void> {
|
|
108
|
+
return this.putPrimary(did, C_ALBUM, identKey(did, C_ALBUM, albumHash(album, albumArtist)), uri);
|
|
109
|
+
}
|
|
110
|
+
recordArtist(did: string, albumArtist: string, uri: string): Promise<void> {
|
|
111
|
+
return this.putPrimary(did, C_ARTIST, identKey(did, C_ARTIST, artistHash(albumArtist)), uri);
|
|
112
|
+
}
|
|
113
|
+
recordScrobble(did: string, title: string, artist: string, album: string, secs: number, uri: string): Promise<void> {
|
|
114
|
+
return this.putPrimary(did, C_SCROBBLE, scrobbleKey(did, songHash(title, artist, album), secs), uri);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
cursor(did: string): Promise<number> {
|
|
118
|
+
return this.get(`${SEP}meta${SEP}cursor${SEP}${did}`).then((v) => (v ? Number(v) : 0));
|
|
119
|
+
}
|
|
120
|
+
async setCursor(did: string, timeUS: number): Promise<void> {
|
|
121
|
+
await this.db.put(`${SEP}meta${SEP}cursor${SEP}${did}`, String(timeUS));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Ingest a full repo CAR for `did`, indexing song/album/artist/scrobble. */
|
|
125
|
+
async indexCar(did: string, car: Uint8Array): Promise<IndexStats> {
|
|
126
|
+
const reader = fromUint8Array(car);
|
|
127
|
+
const blocks = new Map<string, Uint8Array>();
|
|
128
|
+
for (const entry of reader) blocks.set(cidToString(entry.cid), entry.bytes);
|
|
129
|
+
|
|
130
|
+
const root = reader.roots[0];
|
|
131
|
+
if (!root) throw new Error("CAR has no root");
|
|
132
|
+
const commit = cborDecode(blocks.get(root.$link)!) as { data: { $link: string }; rev?: string };
|
|
133
|
+
|
|
134
|
+
const stats: IndexStats = { artists: 0, albums: 0, songs: 0, scrobbles: 0 };
|
|
135
|
+
const ops: { type: "put"; key: string; value: string }[] = [];
|
|
136
|
+
|
|
137
|
+
for (const [path, valueLink] of walkMst(blocks, commit.data.$link)) {
|
|
138
|
+
const slash = path.indexOf("/");
|
|
139
|
+
if (slash < 0) continue;
|
|
140
|
+
const col = path.slice(0, slash);
|
|
141
|
+
const rkey = path.slice(slash + 1);
|
|
142
|
+
if (col !== C_ARTIST && col !== C_ALBUM && col !== C_SONG && col !== C_SCROBBLE) continue;
|
|
143
|
+
const recBytes = blocks.get(valueLink);
|
|
144
|
+
if (!recBytes) continue;
|
|
145
|
+
const rec = cborDecode(recBytes) as Record<string, unknown>;
|
|
146
|
+
const primary = primaryFor(did, col, rec);
|
|
147
|
+
if (!primary) continue;
|
|
148
|
+
const uri = `at://${did}/${col}/${rkey}`;
|
|
149
|
+
ops.push({ type: "put", key: primary, value: uri });
|
|
150
|
+
ops.push({ type: "put", key: `${SEP}rk${SEP}${did}${SEP}${col}${SEP}${rkey}`, value: primary });
|
|
151
|
+
if (col === C_ARTIST) stats.artists++;
|
|
152
|
+
else if (col === C_ALBUM) stats.albums++;
|
|
153
|
+
else if (col === C_SONG) stats.songs++;
|
|
154
|
+
else stats.scrobbles++;
|
|
155
|
+
}
|
|
156
|
+
if (commit.rev) ops.push({ type: "put", key: `${SEP}meta${SEP}rev${SEP}${did}`, value: commit.rev });
|
|
157
|
+
await this.db.batch(ops);
|
|
158
|
+
return stats;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Apply a single Jetstream commit event to the index. */
|
|
162
|
+
async applyCommit(
|
|
163
|
+
did: string,
|
|
164
|
+
col: string,
|
|
165
|
+
operation: string,
|
|
166
|
+
rkey: string,
|
|
167
|
+
record: Record<string, unknown> | undefined,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
if (operation === "create" || operation === "update") {
|
|
170
|
+
if (!record) return;
|
|
171
|
+
const primary = primaryFor(did, col, record);
|
|
172
|
+
if (!primary) return;
|
|
173
|
+
await this.putPrimary(did, col, primary, `at://${did}/${col}/${rkey}`);
|
|
174
|
+
} else if (operation === "delete") {
|
|
175
|
+
const rk = `${SEP}rk${SEP}${did}${SEP}${col}${SEP}${rkey}`;
|
|
176
|
+
const primary = await this.get(rk);
|
|
177
|
+
if (primary) {
|
|
178
|
+
await this.db.batch([
|
|
179
|
+
{ type: "del", key: primary },
|
|
180
|
+
{ type: "del", key: rk },
|
|
181
|
+
]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// In-order MST traversal, reconstructing each leaf's full key. Yields
|
|
188
|
+
// [path, recordCidString]. Skips CIDs absent from `blocks`.
|
|
189
|
+
function* walkMst(blocks: Map<string, Uint8Array>, cid: string): Generator<[string, string]> {
|
|
190
|
+
const raw = blocks.get(cid);
|
|
191
|
+
if (!raw) return;
|
|
192
|
+
const node = cborDecode(raw) as {
|
|
193
|
+
l?: { $link: string } | null;
|
|
194
|
+
e: { p: number; k: unknown; v: { $link: string }; t?: { $link: string } | null }[];
|
|
195
|
+
};
|
|
196
|
+
if (node.l && isCidLink(node.l)) yield* walkMst(blocks, node.l.$link);
|
|
197
|
+
let last = new Uint8Array(0);
|
|
198
|
+
for (const e of node.e) {
|
|
199
|
+
const suffix = isBytes(e.k) ? fromBytes(e.k) : (e.k as Uint8Array);
|
|
200
|
+
const key = new Uint8Array(e.p + suffix.length);
|
|
201
|
+
key.set(last.subarray(0, e.p), 0);
|
|
202
|
+
key.set(suffix, e.p);
|
|
203
|
+
yield [new TextDecoder().decode(key), e.v.$link];
|
|
204
|
+
last = key;
|
|
205
|
+
if (e.t && isCidLink(e.t)) yield* walkMst(blocks, e.t.$link);
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,47 +1,10 @@
|
|
|
1
|
+
/** Error thrown when a Rocksky XRPC call returns a non-2xx `{ error, message }`. */
|
|
1
2
|
export class RockskyError extends Error {
|
|
2
|
-
readonly
|
|
3
|
-
constructor(
|
|
4
|
-
|
|
3
|
+
readonly kind?: string;
|
|
4
|
+
constructor(payload: unknown) {
|
|
5
|
+
const p = payload as { error?: string; message?: string } | undefined;
|
|
6
|
+
super(p?.message || p?.error || "rocksky request failed");
|
|
5
7
|
this.name = "RockskyError";
|
|
6
|
-
this.
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class RockskyHttpError extends RockskyError {
|
|
11
|
-
readonly status: number;
|
|
12
|
-
readonly statusText: string;
|
|
13
|
-
readonly url: string;
|
|
14
|
-
readonly body: unknown;
|
|
15
|
-
|
|
16
|
-
constructor(args: {
|
|
17
|
-
status: number;
|
|
18
|
-
statusText: string;
|
|
19
|
-
url: string;
|
|
20
|
-
body: unknown;
|
|
21
|
-
message?: string;
|
|
22
|
-
}) {
|
|
23
|
-
const message =
|
|
24
|
-
args.message ??
|
|
25
|
-
`Rocksky API ${args.status} ${args.statusText} at ${args.url}`;
|
|
26
|
-
super(message);
|
|
27
|
-
this.name = "RockskyHttpError";
|
|
28
|
-
this.status = args.status;
|
|
29
|
-
this.statusText = args.statusText;
|
|
30
|
-
this.url = args.url;
|
|
31
|
-
this.body = args.body;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class RockskyTimeoutError extends RockskyError {
|
|
36
|
-
constructor(ms: number) {
|
|
37
|
-
super(`Rocksky request timed out after ${ms}ms`);
|
|
38
|
-
this.name = "RockskyTimeoutError";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class RockskyAuthError extends RockskyError {
|
|
43
|
-
constructor(message = "Authentication required") {
|
|
44
|
-
super(message);
|
|
45
|
-
this.name = "RockskyAuthError";
|
|
8
|
+
this.kind = p?.error;
|
|
46
9
|
}
|
|
47
10
|
}
|
package/src/generated/types.ts
CHANGED
|
@@ -129,7 +129,7 @@ export interface AlbumRecord {
|
|
|
129
129
|
title: string;
|
|
130
130
|
/** The artist of the album. */
|
|
131
131
|
artist: string;
|
|
132
|
-
/** The duration of the album in
|
|
132
|
+
/** The duration of the album in milliseconds. */
|
|
133
133
|
duration?: number;
|
|
134
134
|
/** The release date of the album. */
|
|
135
135
|
releaseDate?: DateTime;
|
|
@@ -358,7 +358,7 @@ export interface CreateScrobbleInput {
|
|
|
358
358
|
artist: string;
|
|
359
359
|
/** The album of the track being scrobbled */
|
|
360
360
|
album?: string;
|
|
361
|
-
/** The duration of the track in milliseconds */
|
|
361
|
+
/** The duration of the track in milliseconds (e.g., 240000 for 4 minutes) */
|
|
362
362
|
duration?: number;
|
|
363
363
|
/** The MusicBrainz ID of the track, if available */
|
|
364
364
|
mbId?: string;
|
|
@@ -414,7 +414,7 @@ export interface CreateSongInput {
|
|
|
414
414
|
albumArtist: string;
|
|
415
415
|
/** The album of the song, if applicable */
|
|
416
416
|
album: string;
|
|
417
|
-
/** The duration of the song in
|
|
417
|
+
/** The duration of the song in milliseconds */
|
|
418
418
|
duration?: number;
|
|
419
419
|
/** The MusicBrainz ID of the song, if available */
|
|
420
420
|
mbId?: string;
|
|
@@ -874,6 +874,11 @@ export interface GetArtistTracksParams {
|
|
|
874
874
|
offset?: number;
|
|
875
875
|
}
|
|
876
876
|
|
|
877
|
+
export interface GetAudioSettingsParams {
|
|
878
|
+
/** DID or handle of the user whose settings to fetch. Required for unauthenticated requests. */
|
|
879
|
+
did?: AtIdentifier;
|
|
880
|
+
}
|
|
881
|
+
|
|
877
882
|
export interface GetCurrentlyPlayingParams {
|
|
878
883
|
playerId?: string;
|
|
879
884
|
/** Handle or DID of the actor to retrieve the currently playing track for. If not provided, defaults to the current user. */
|
|
@@ -1139,6 +1144,10 @@ export interface GetStatsParams {
|
|
|
1139
1144
|
export interface GetStoriesParams {
|
|
1140
1145
|
/** The maximum number of stories to return. */
|
|
1141
1146
|
size?: number;
|
|
1147
|
+
/** The feed URI to filter stories by. */
|
|
1148
|
+
feed?: AtUri;
|
|
1149
|
+
/** If true, only return stories from users the viewer follows. Requires authentication. */
|
|
1150
|
+
following?: boolean;
|
|
1142
1151
|
}
|
|
1143
1152
|
|
|
1144
1153
|
export interface GetTemporaryLinkParams {
|
|
@@ -1408,6 +1417,17 @@ export interface ProfileRecord {
|
|
|
1408
1417
|
createdAt?: DateTime;
|
|
1409
1418
|
}
|
|
1410
1419
|
|
|
1420
|
+
export interface PutAudioSettingsInput {
|
|
1421
|
+
/** Crossfade settings to apply. */
|
|
1422
|
+
crossfade?: RockboxCrossfadeSettings;
|
|
1423
|
+
/** Equalizer settings to apply. */
|
|
1424
|
+
equalizer?: RockboxEqualizerSettings;
|
|
1425
|
+
/** Replay gain settings to apply. */
|
|
1426
|
+
replayGain?: RockboxReplayGainSettings;
|
|
1427
|
+
/** Tone control settings to apply. */
|
|
1428
|
+
tone?: RockboxToneSettings;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1411
1431
|
export interface PutMirrorSourceInput {
|
|
1412
1432
|
/** One of: lastfm, listenbrainz, tealfm */
|
|
1413
1433
|
provider: string;
|
|
@@ -1502,6 +1522,74 @@ export interface ReportShoutInput {
|
|
|
1502
1522
|
reason?: string;
|
|
1503
1523
|
}
|
|
1504
1524
|
|
|
1525
|
+
export interface RockboxCrossfadeSettings {
|
|
1526
|
+
/** Crossfade mode: disabled | enabled | shuffle | albumChange | trackChange */
|
|
1527
|
+
mode?: string;
|
|
1528
|
+
/** Fade-in delay in ms */
|
|
1529
|
+
fadeInDelay?: number;
|
|
1530
|
+
/** Fade-in duration in ms */
|
|
1531
|
+
fadeInDuration?: number;
|
|
1532
|
+
/** Fade-out delay in ms */
|
|
1533
|
+
fadeOutDelay?: number;
|
|
1534
|
+
/** Fade-out duration in ms */
|
|
1535
|
+
fadeOutDuration?: number;
|
|
1536
|
+
/** Fade-out mix mode: crossfade | mix */
|
|
1537
|
+
fadeOutMixMode?: string;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
export interface RockboxEqualizerBand {
|
|
1541
|
+
/** Center frequency in Hz */
|
|
1542
|
+
frequency: number;
|
|
1543
|
+
/** Band gain in tenths of dB (e.g. 30 = +3.0 dB) */
|
|
1544
|
+
gain: number;
|
|
1545
|
+
/** Q factor × 10 (e.g. 7 = Q 0.7) */
|
|
1546
|
+
q: number;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
export interface RockboxEqualizerSettings {
|
|
1550
|
+
/** Whether the equalizer is enabled */
|
|
1551
|
+
enabled?: boolean;
|
|
1552
|
+
/** Pre-amplification cut in tenths of dB applied before EQ bands (e.g. -60 = -6.0 dB) */
|
|
1553
|
+
precut?: number;
|
|
1554
|
+
/** Up to 10 EQ bands */
|
|
1555
|
+
bands?: RockboxEqualizerBand[];
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
export interface RockboxReplayGainSettings {
|
|
1559
|
+
/** Replay gain mode: disabled | track | album | trackIfShuffling */
|
|
1560
|
+
mode?: string;
|
|
1561
|
+
/** Pre-amplification in tenths of dB (e.g. 15 = +1.5 dB) */
|
|
1562
|
+
preamp?: number;
|
|
1563
|
+
/** Whether to prevent clipping by reducing volume */
|
|
1564
|
+
preventClipping?: boolean;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
export interface RockboxSettingsView {
|
|
1568
|
+
/** Crossfade settings */
|
|
1569
|
+
crossfade?: RockboxCrossfadeSettings;
|
|
1570
|
+
/** Equalizer settings */
|
|
1571
|
+
equalizer?: RockboxEqualizerSettings;
|
|
1572
|
+
/** Replay gain settings */
|
|
1573
|
+
replayGain?: RockboxReplayGainSettings;
|
|
1574
|
+
/** Tone control settings (bass, treble, balance, channels) */
|
|
1575
|
+
tone?: RockboxToneSettings;
|
|
1576
|
+
/** When this settings record was first created. */
|
|
1577
|
+
createdAt: DateTime;
|
|
1578
|
+
/** When this settings record was last updated. */
|
|
1579
|
+
updatedAt?: DateTime;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
export interface RockboxToneSettings {
|
|
1583
|
+
/** Bass level in dB */
|
|
1584
|
+
bass?: number;
|
|
1585
|
+
/** Treble level in dB */
|
|
1586
|
+
treble?: number;
|
|
1587
|
+
/** Left/right balance. Negative = left, positive = right */
|
|
1588
|
+
balance?: number;
|
|
1589
|
+
/** Channel configuration: stereo | mono | monoLeft | monoRight | karaoke | wide */
|
|
1590
|
+
channels?: string;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1505
1593
|
export interface ScrobbleFirstScrobbleView {
|
|
1506
1594
|
/** The handle of the user who first scrobbled this song. */
|
|
1507
1595
|
handle?: string;
|
|
@@ -1569,26 +1657,32 @@ export interface ScrobbleRecord {
|
|
|
1569
1657
|
export interface ScrobbleViewBasic {
|
|
1570
1658
|
/** The unique identifier of the scrobble. */
|
|
1571
1659
|
id?: string;
|
|
1572
|
-
/** The
|
|
1573
|
-
|
|
1574
|
-
/** The display name of the user who created the scrobble. */
|
|
1575
|
-
userDisplayName?: string;
|
|
1576
|
-
/** The avatar URL of the user who created the scrobble. */
|
|
1577
|
-
userAvatar?: Uri;
|
|
1660
|
+
/** The unique identifier of the track this scrobble is of. */
|
|
1661
|
+
trackId?: string;
|
|
1578
1662
|
/** The title of the scrobble. */
|
|
1579
1663
|
title?: string;
|
|
1580
1664
|
/** The artist of the song. */
|
|
1581
1665
|
artist?: string;
|
|
1582
1666
|
/** The URI of the artist. */
|
|
1583
1667
|
artistUri?: AtUri;
|
|
1668
|
+
/** The album artist of the song. */
|
|
1669
|
+
albumArtist?: string;
|
|
1584
1670
|
/** The album of the song. */
|
|
1585
1671
|
album?: string;
|
|
1586
1672
|
/** The URI of the album. */
|
|
1587
1673
|
albumUri?: AtUri;
|
|
1588
1674
|
/** The album art URL of the song. */
|
|
1589
|
-
|
|
1675
|
+
albumArt?: Uri;
|
|
1676
|
+
/** The URI of the track (song) this scrobble is of. */
|
|
1677
|
+
trackUri?: AtUri;
|
|
1678
|
+
/** The handle of the user who created the scrobble. */
|
|
1679
|
+
handle?: string;
|
|
1680
|
+
/** The DID of the user who created the scrobble. */
|
|
1681
|
+
did?: AtIdentifier;
|
|
1682
|
+
/** The avatar URL of the user who created the scrobble. */
|
|
1683
|
+
avatar?: Uri;
|
|
1590
1684
|
/** The timestamp when the scrobble was created. */
|
|
1591
|
-
|
|
1685
|
+
createdAt?: DateTime;
|
|
1592
1686
|
/** The URI of the scrobble. */
|
|
1593
1687
|
uri?: Uri;
|
|
1594
1688
|
/** The SHA256 hash of the scrobble data. */
|
|
@@ -1642,6 +1736,21 @@ export interface SeekParams {
|
|
|
1642
1736
|
position: number;
|
|
1643
1737
|
}
|
|
1644
1738
|
|
|
1739
|
+
export interface SettingsRecord {
|
|
1740
|
+
/** Crossfade settings */
|
|
1741
|
+
crossfade?: RockboxCrossfadeSettings;
|
|
1742
|
+
/** Equalizer settings */
|
|
1743
|
+
equalizer?: RockboxEqualizerSettings;
|
|
1744
|
+
/** Replay gain settings */
|
|
1745
|
+
replayGain?: RockboxReplayGainSettings;
|
|
1746
|
+
/** Tone control settings (bass, treble, balance, channels) */
|
|
1747
|
+
tone?: RockboxToneSettings;
|
|
1748
|
+
/** When this settings record was first created. */
|
|
1749
|
+
createdAt: DateTime;
|
|
1750
|
+
/** When this settings record was last updated. */
|
|
1751
|
+
updatedAt?: DateTime;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1645
1754
|
export interface ShoutAuthor {
|
|
1646
1755
|
/** The unique identifier of the author. */
|
|
1647
1756
|
id?: string;
|
|
@@ -2114,6 +2223,8 @@ export interface Endpoints {
|
|
|
2114
2223
|
"app.rocksky.playlist.removePlaylist": void;
|
|
2115
2224
|
"app.rocksky.playlist.removeTrack": void;
|
|
2116
2225
|
"app.rocksky.playlist.startPlaylist": void;
|
|
2226
|
+
"app.rocksky.rockbox.getAudioSettings": RockboxSettingsView;
|
|
2227
|
+
"app.rocksky.rockbox.putAudioSettings": RockboxSettingsView;
|
|
2117
2228
|
"app.rocksky.scrobble.createScrobble": ScrobbleViewBasic;
|
|
2118
2229
|
"app.rocksky.scrobble.getScrobble": ScrobbleViewDetailed;
|
|
2119
2230
|
"app.rocksky.scrobble.getScrobbles": GetScrobblesOutput;
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
// Lowercase-hex SHA-256 of s.toLowerCase() — the lowercasing is applied to the
|
|
4
|
+
// whole string (not per field), matching Rocksky's server and every other SDK.
|
|
5
|
+
function sha256Lower(s: string): string {
|
|
6
|
+
return createHash("sha256").update(s.toLowerCase()).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Identity hash of a song: sha256(lower("{title} - {artist} - {album}")). */
|
|
10
|
+
export function songHash(title: string, artist: string, album: string): string {
|
|
11
|
+
return sha256Lower(`${title} - ${artist} - ${album}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Identity hash of an album: sha256(lower("{album} - {albumArtist}")). */
|
|
15
|
+
export function albumHash(album: string, albumArtist: string): string {
|
|
16
|
+
return sha256Lower(`${album} - ${albumArtist}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Identity hash of an artist: sha256(lower(albumArtist)) — a single field. */
|
|
20
|
+
export function artistHash(albumArtist: string): string {
|
|
21
|
+
return sha256Lower(albumArtist);
|
|
22
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,87 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./
|
|
12
|
-
export {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
withTimeout,
|
|
18
|
-
withFallback,
|
|
19
|
-
catchError,
|
|
20
|
-
type Op,
|
|
21
|
-
} from "./pipe.js";
|
|
22
|
-
export {
|
|
23
|
-
DEFAULT_BASE_URL,
|
|
24
|
-
type AuthProvider,
|
|
25
|
-
type ClientOptions,
|
|
26
|
-
type FetchLike,
|
|
27
|
-
type Json,
|
|
28
|
-
type Pagination,
|
|
29
|
-
type RequestOptions,
|
|
30
|
-
} from "./types.js";
|
|
31
|
-
export {
|
|
32
|
-
paginate,
|
|
33
|
-
type PageOpts,
|
|
34
|
-
type PageResult,
|
|
35
|
-
type PaginateArgs,
|
|
36
|
-
} from "./paginate.js";
|
|
37
|
-
export {
|
|
38
|
-
RealtimeClient,
|
|
39
|
-
RealtimeClientBuilder,
|
|
40
|
-
createRealtimeClient,
|
|
41
|
-
type RealtimeOptions,
|
|
42
|
-
type RealtimeEvent,
|
|
43
|
-
type RealtimeEventMap,
|
|
44
|
-
type ReconnectOptions,
|
|
45
|
-
type WebSocketCtor,
|
|
46
|
-
type WebSocketLike,
|
|
47
|
-
} from "./realtime.js";
|
|
48
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @rocksky/sdk — the official TypeScript SDK for Rocksky, built on atcute.
|
|
3
|
+
*
|
|
4
|
+
* {@link RockskyClient} does unauthenticated AppView reads; {@link Agent} logs
|
|
5
|
+
* in with an app password and writes app.rocksky.* records to the user's PDS
|
|
6
|
+
* (scrobble, like, follow, shout, now-playing). With a {@link RockskyIndex}
|
|
7
|
+
* attached it prevents duplicates, backfilled from the repo CAR
|
|
8
|
+
* ({@link Agent.syncRepo}) and kept live off the Jetstream firehose
|
|
9
|
+
* ({@link Agent.hydrateFromJetstream}).
|
|
10
|
+
*/
|
|
11
|
+
export { RockskyClient, DEFAULT_APPVIEW } from "./client.js";
|
|
12
|
+
export { Agent, type ScrobbleInput, type SongInput, type AlbumInput, type ArtistInput } from "./agent.js";
|
|
13
|
+
export { RockskyIndex, totalIndexed, type IndexStats } from "./dedup.js";
|
|
14
|
+
export { runJetstream, DEFAULT_JETSTREAM_SERVERS, type JetstreamOptions } from "./jetstream.js";
|
|
15
|
+
export { songHash, albumHash, artistHash } from "./hash.js";
|
|
16
|
+
export { RockskyError } from "./errors.js";
|
|
49
17
|
export type * from "./generated/types.js";
|
|
50
|
-
|
|
51
|
-
export type {
|
|
52
|
-
GetProfileParams,
|
|
53
|
-
ActorPagedParams,
|
|
54
|
-
ActorRangeParams,
|
|
55
|
-
} from "./namespaces/actor.js";
|
|
56
|
-
export type {
|
|
57
|
-
CreateScrobbleInput,
|
|
58
|
-
GetScrobblesParams,
|
|
59
|
-
} from "./namespaces/scrobble.js";
|
|
60
|
-
export type {
|
|
61
|
-
GetSongParams,
|
|
62
|
-
GetSongsParams,
|
|
63
|
-
MatchSongParams,
|
|
64
|
-
CreateSongInput,
|
|
65
|
-
} from "./namespaces/song.js";
|
|
66
|
-
export type { GetAlbumsParams } from "./namespaces/album.js";
|
|
67
|
-
export type {
|
|
68
|
-
GetArtistsParams,
|
|
69
|
-
ArtistListenersParams,
|
|
70
|
-
GetArtistTracksParams,
|
|
71
|
-
} from "./namespaces/artist.js";
|
|
72
|
-
export type {
|
|
73
|
-
ListApikeysParams,
|
|
74
|
-
CreateApikeyInput,
|
|
75
|
-
UpdateApikeyInput,
|
|
76
|
-
} from "./namespaces/apikey.js";
|
|
77
|
-
export type {
|
|
78
|
-
ScrobblesChartParams,
|
|
79
|
-
TopChartParams,
|
|
80
|
-
} from "./namespaces/charts.js";
|
|
81
|
-
export type {
|
|
82
|
-
FollowListParams,
|
|
83
|
-
KnownFollowersParams,
|
|
84
|
-
} from "./namespaces/graph.js";
|
|
85
|
-
export type { RecommendParams } from "./namespaces/feed.js";
|
|
86
|
-
export type { LikeInput } from "./namespaces/like.js";
|
|
87
|
-
export type { PutMirrorSourceInput } from "./namespaces/mirror.js";
|