@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.
Files changed (51) hide show
  1. package/README.md +44 -254
  2. package/dist/agent.d.ts +77 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/client.d.ts +26 -113
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/dedup.d.ts +38 -0
  7. package/dist/dedup.d.ts.map +1 -0
  8. package/dist/errors.d.ts +3 -23
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/generated/types.d.ts +113 -11
  11. package/dist/generated/types.d.ts.map +1 -1
  12. package/dist/hash.d.ts +7 -0
  13. package/dist/hash.d.ts.map +1 -0
  14. package/dist/index.d.ts +16 -17
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6648 -1288
  17. package/dist/jetstream.d.ts +17 -0
  18. package/dist/jetstream.d.ts.map +1 -0
  19. package/package.json +15 -11
  20. package/src/agent.ts +232 -0
  21. package/src/client.ts +82 -226
  22. package/src/dedup.ts +207 -0
  23. package/src/errors.ts +6 -43
  24. package/src/generated/types.ts +122 -11
  25. package/src/hash.ts +22 -0
  26. package/src/index.ts +16 -86
  27. package/src/jetstream.ts +122 -0
  28. package/src/http.ts +0 -195
  29. package/src/namespaces/_helpers.ts +0 -27
  30. package/src/namespaces/actor.ts +0 -93
  31. package/src/namespaces/album.ts +0 -34
  32. package/src/namespaces/apikey.ts +0 -50
  33. package/src/namespaces/artist.ts +0 -68
  34. package/src/namespaces/charts.ts +0 -38
  35. package/src/namespaces/dropbox.ts +0 -53
  36. package/src/namespaces/feed.ts +0 -112
  37. package/src/namespaces/googledrive.ts +0 -41
  38. package/src/namespaces/graph.ts +0 -62
  39. package/src/namespaces/like.ts +0 -46
  40. package/src/namespaces/mirror.ts +0 -27
  41. package/src/namespaces/player.ts +0 -125
  42. package/src/namespaces/playlist.ts +0 -95
  43. package/src/namespaces/scrobble.ts +0 -41
  44. package/src/namespaces/shout.ts +0 -99
  45. package/src/namespaces/song.ts +0 -60
  46. package/src/namespaces/spotify.ts +0 -56
  47. package/src/namespaces/stats.ts +0 -27
  48. package/src/paginate.ts +0 -90
  49. package/src/pipe.ts +0 -146
  50. package/src/realtime.ts +0 -408
  51. 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 cause?: unknown;
3
- constructor(message: string, options?: { cause?: unknown }) {
4
- super(message);
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.cause = options?.cause;
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
  }
@@ -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 seconds. */
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 seconds */
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 handle of the user who created the scrobble. */
1573
- user?: string;
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
- cover?: Uri;
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
- date?: DateTime;
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
- export {
2
- RockskyClient,
3
- RockskyClientBuilder,
4
- createClient,
5
- } from "./client.js";
6
- export {
7
- RockskyError,
8
- RockskyHttpError,
9
- RockskyTimeoutError,
10
- RockskyAuthError,
11
- } from "./errors.js";
12
- export {
13
- pipe,
14
- map,
15
- tap,
16
- withRetry,
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";