@ewanc26/atproto 0.1.0 → 0.2.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/dist/fetch.js ADDED
@@ -0,0 +1,280 @@
1
+ import { cache } from './cache.js';
2
+ import { withFallback, resolveIdentity } from './agents.js';
3
+ import { buildPdsBlobUrl } from './media.js';
4
+ import { findArtwork } from './musicbrainz.js';
5
+ export async function fetchProfile(did, fetchFn) {
6
+ const cacheKey = `profile:${did}`;
7
+ const cached = cache.get(cacheKey);
8
+ if (cached)
9
+ return cached;
10
+ const profile = await withFallback(did, async (agent) => {
11
+ const response = await agent.getProfile({ actor: did });
12
+ return response.data;
13
+ }, false, fetchFn);
14
+ let pronouns;
15
+ try {
16
+ const recordResponse = await withFallback(did, async (agent) => {
17
+ const response = await agent.com.atproto.repo.getRecord({
18
+ repo: did,
19
+ collection: 'app.bsky.actor.profile',
20
+ rkey: 'self'
21
+ });
22
+ return response.data;
23
+ }, false, fetchFn);
24
+ pronouns = recordResponse.value.pronouns;
25
+ }
26
+ catch { /* pronouns optional */ }
27
+ const data = {
28
+ did: profile.did,
29
+ handle: profile.handle,
30
+ displayName: profile.displayName,
31
+ description: profile.description,
32
+ avatar: profile.avatar,
33
+ banner: profile.banner,
34
+ followersCount: profile.followersCount,
35
+ followsCount: profile.followsCount,
36
+ postsCount: profile.postsCount,
37
+ pronouns
38
+ };
39
+ cache.set(cacheKey, data);
40
+ return data;
41
+ }
42
+ export async function fetchSiteInfo(did, fetchFn) {
43
+ const cacheKey = `siteinfo:${did}`;
44
+ const cached = cache.get(cacheKey);
45
+ if (cached)
46
+ return cached;
47
+ try {
48
+ const result = await withFallback(did, async (agent) => {
49
+ try {
50
+ const response = await agent.com.atproto.repo.getRecord({
51
+ repo: did,
52
+ collection: 'uk.ewancroft.site.info',
53
+ rkey: 'self'
54
+ });
55
+ return response.data;
56
+ }
57
+ catch (err) {
58
+ if (err.error === 'RecordNotFound')
59
+ return null;
60
+ throw err;
61
+ }
62
+ }, true, fetchFn);
63
+ if (!result?.value)
64
+ return null;
65
+ const data = result.value;
66
+ cache.set(cacheKey, data);
67
+ return data;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ export async function fetchLinks(did, fetchFn) {
74
+ const cacheKey = `links:${did}`;
75
+ const cached = cache.get(cacheKey);
76
+ if (cached)
77
+ return cached;
78
+ try {
79
+ const value = await withFallback(did, async (agent) => {
80
+ const response = await agent.com.atproto.repo.getRecord({
81
+ repo: did,
82
+ collection: 'blue.linkat.board',
83
+ rkey: 'self'
84
+ });
85
+ return response.data.value;
86
+ }, true, fetchFn);
87
+ if (!value || !Array.isArray(value.cards))
88
+ return null;
89
+ const data = { cards: value.cards };
90
+ cache.set(cacheKey, data);
91
+ return data;
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ export async function fetchMusicStatus(did, fetchFn) {
98
+ const cacheKey = `music-status:${did}`;
99
+ const cached = cache.get(cacheKey);
100
+ if (cached)
101
+ return cached;
102
+ try {
103
+ // Try actor status first
104
+ try {
105
+ const statusRecords = await withFallback(did, async (agent) => {
106
+ const response = await agent.com.atproto.repo.listRecords({
107
+ repo: did,
108
+ collection: 'fm.teal.alpha.actor.status',
109
+ limit: 1
110
+ });
111
+ return response.data.records;
112
+ }, true, fetchFn);
113
+ if (statusRecords?.length) {
114
+ const record = statusRecords[0];
115
+ const value = record.value;
116
+ if (value.expiry) {
117
+ const expiryTime = parseInt(value.expiry) * 1000;
118
+ if (Date.now() <= expiryTime) {
119
+ const trackName = value.item?.trackName || value.trackName;
120
+ const artists = value.item?.artists || value.artists || [];
121
+ const releaseName = value.item?.releaseName || value.releaseName;
122
+ const artistName = artists[0]?.artistName;
123
+ const releaseMbId = value.item?.releaseMbId || value.releaseMbId;
124
+ let artworkUrl;
125
+ if (releaseName && artistName) {
126
+ artworkUrl = (await findArtwork(releaseName, artistName, releaseName, releaseMbId, fetchFn)) || undefined;
127
+ }
128
+ if (!artworkUrl && trackName && artistName) {
129
+ artworkUrl = (await findArtwork(trackName, artistName, releaseName, releaseMbId, fetchFn)) || undefined;
130
+ }
131
+ if (!artworkUrl) {
132
+ const artwork = value.item?.artwork || value.artwork;
133
+ if (artwork?.ref?.$link) {
134
+ const identity = await resolveIdentity(did, fetchFn);
135
+ artworkUrl = buildPdsBlobUrl(identity.pds, did, artwork.ref.$link);
136
+ }
137
+ }
138
+ const data = {
139
+ trackName,
140
+ artists,
141
+ releaseName,
142
+ playedTime: value.item?.playedTime || value.playedTime,
143
+ originUrl: value.item?.originUrl || value.originUrl,
144
+ recordingMbId: value.item?.recordingMbId || value.recordingMbId,
145
+ releaseMbId,
146
+ isrc: value.isrc,
147
+ duration: value.duration,
148
+ musicServiceBaseDomain: value.item?.musicServiceBaseDomain || value.musicServiceBaseDomain,
149
+ submissionClientAgent: value.item?.submissionClientAgent || value.submissionClientAgent,
150
+ $type: 'fm.teal.alpha.actor.status',
151
+ expiry: value.expiry,
152
+ artwork: value.item?.artwork || value.artwork,
153
+ artworkUrl
154
+ };
155
+ cache.set(cacheKey, data);
156
+ return data;
157
+ }
158
+ }
159
+ }
160
+ }
161
+ catch { /* fall through to feed play */ }
162
+ // Fall back to feed play
163
+ const playRecords = await withFallback(did, async (agent) => {
164
+ const response = await agent.com.atproto.repo.listRecords({
165
+ repo: did,
166
+ collection: 'fm.teal.alpha.feed.play',
167
+ limit: 1
168
+ });
169
+ return response.data.records;
170
+ }, true, fetchFn);
171
+ if (playRecords?.length) {
172
+ const record = playRecords[0];
173
+ const value = record.value;
174
+ const artists = value.artists || [];
175
+ const artistName = artists[0]?.artistName;
176
+ let artworkUrl;
177
+ if (value.releaseName && artistName) {
178
+ artworkUrl = (await findArtwork(value.releaseName, artistName, value.releaseName, value.releaseMbId, fetchFn)) || undefined;
179
+ }
180
+ if (!artworkUrl && value.trackName && artistName) {
181
+ artworkUrl = (await findArtwork(value.trackName, artistName, value.releaseName, value.releaseMbId, fetchFn)) || undefined;
182
+ }
183
+ if (!artworkUrl && value.artwork?.ref?.$link) {
184
+ const identity = await resolveIdentity(did, fetchFn);
185
+ artworkUrl = buildPdsBlobUrl(identity.pds, did, value.artwork.ref.$link);
186
+ }
187
+ const data = {
188
+ trackName: value.trackName,
189
+ artists,
190
+ releaseName: value.releaseName,
191
+ playedTime: value.playedTime,
192
+ originUrl: value.originUrl,
193
+ recordingMbId: value.recordingMbId,
194
+ releaseMbId: value.releaseMbId,
195
+ isrc: value.isrc,
196
+ duration: value.duration,
197
+ musicServiceBaseDomain: value.musicServiceBaseDomain,
198
+ submissionClientAgent: value.submissionClientAgent,
199
+ $type: 'fm.teal.alpha.feed.play',
200
+ artwork: value.artwork,
201
+ artworkUrl
202
+ };
203
+ cache.set(cacheKey, data);
204
+ return data;
205
+ }
206
+ return null;
207
+ }
208
+ catch {
209
+ return null;
210
+ }
211
+ }
212
+ export async function fetchKibunStatus(did, fetchFn) {
213
+ const cacheKey = `kibun-status:${did}`;
214
+ const cached = cache.get(cacheKey);
215
+ if (cached)
216
+ return cached;
217
+ try {
218
+ const statusRecords = await withFallback(did, async (agent) => {
219
+ const response = await agent.com.atproto.repo.listRecords({
220
+ repo: did,
221
+ collection: 'social.kibun.status',
222
+ limit: 1
223
+ });
224
+ return response.data.records;
225
+ }, true, fetchFn);
226
+ if (statusRecords?.length) {
227
+ const value = statusRecords[0].value;
228
+ const data = {
229
+ text: value.text,
230
+ emoji: value.emoji,
231
+ createdAt: value.createdAt,
232
+ $type: 'social.kibun.status'
233
+ };
234
+ cache.set(cacheKey, data);
235
+ return data;
236
+ }
237
+ return null;
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
243
+ export async function fetchTangledRepos(did, fetchFn) {
244
+ const cacheKey = `tangled:${did}`;
245
+ const cached = cache.get(cacheKey);
246
+ if (cached)
247
+ return cached;
248
+ try {
249
+ const records = await withFallback(did, async (agent) => {
250
+ const response = await agent.com.atproto.repo.listRecords({
251
+ repo: did,
252
+ collection: 'sh.tangled.repo',
253
+ limit: 100
254
+ });
255
+ return response.data.records;
256
+ }, true, fetchFn);
257
+ if (!records.length)
258
+ return null;
259
+ const repos = records.map((record) => {
260
+ const value = record.value;
261
+ return {
262
+ uri: record.uri,
263
+ name: value.name,
264
+ description: value.description,
265
+ knot: value.knot,
266
+ createdAt: value.createdAt,
267
+ labels: value.labels,
268
+ source: value.source,
269
+ spindle: value.spindle
270
+ };
271
+ });
272
+ repos.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
273
+ const data = { repos };
274
+ cache.set(cacheKey, data);
275
+ return data;
276
+ }
277
+ catch {
278
+ return null;
279
+ }
280
+ }
@@ -0,0 +1,12 @@
1
+ export type { ProfileData, SiteInfoData, LinkData, LinkCard, BlueskyPost, BlogPost, PostAuthor, ExternalLink, Facet, Technology, License, BasedOnItem, RelatedService, Repository, Credit, SectionLicense, ResolvedIdentity, CacheEntry, MusicStatusData, MusicArtist, KibunStatusData, TangledRepo, TangledReposData, StandardSitePublication, StandardSitePublicationsData, StandardSiteDocument, StandardSiteDocumentsData, StandardSiteBasicTheme, StandardSiteThemeColor } from './types';
2
+ export { fetchProfile, fetchSiteInfo, fetchLinks, fetchMusicStatus, fetchKibunStatus, fetchTangledRepos } from './fetch';
3
+ export { fetchPublications, fetchDocuments, fetchRecentDocuments, fetchBlogPosts } from './documents';
4
+ export { fetchLatestBlueskyPost, fetchPostFromUri } from './posts';
5
+ export { buildPdsBlobUrl, extractCidFromImageObject, extractImageUrlsFromValue } from './media';
6
+ export { createAgent, constellationAgent, defaultAgent, resolveIdentity, getPublicAgent, getPDSAgent, withFallback, resetAgents } from './agents';
7
+ export { searchMusicBrainzRelease, buildCoverArtUrl, searchiTunesArtwork, searchDeezerArtwork, searchLastFmArtwork, findArtwork } from './musicbrainz';
8
+ export { fetchEngagementFromConstellation, fetchAllEngagement } from './engagement';
9
+ export { cache, ATProtoCache, CACHE_TTL } from './cache';
10
+ export { fetchAllRecords, fetchAllUserRecords } from './pagination';
11
+ export type { FetchRecordsConfig, AtProtoRecord } from './pagination';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,YAAY,EACX,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,KAAK,EACL,UAAU,EACV,OAAO,EACP,WAAW,EACX,cAAc,EACd,UAAU,EACV,MAAM,EACN,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,WAAW,EACX,eAAe,EACf,WAAW,EACX,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAC5B,oBAAoB,EACpB,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,EACtB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACN,iBAAiB,EACjB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEnE,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAEhG,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAElJ,OAAO,EACN,wBAAwB,EACxB,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,WAAW,EACX,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,gCAAgC,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEpF,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACpE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // Re-export types and functions from the AT Protocol service layer.
2
+ // Key API difference from the app's src/lib/services/atproto:
3
+ // All functions that previously read PUBLIC_ATPROTO_DID from the environment
4
+ // now accept `did: string` as their first argument.
5
+ export { fetchProfile, fetchSiteInfo, fetchLinks, fetchMusicStatus, fetchKibunStatus, fetchTangledRepos } from './fetch';
6
+ export { fetchPublications, fetchDocuments, fetchRecentDocuments, fetchBlogPosts } from './documents';
7
+ export { fetchLatestBlueskyPost, fetchPostFromUri } from './posts';
8
+ export { buildPdsBlobUrl, extractCidFromImageObject, extractImageUrlsFromValue } from './media';
9
+ export { createAgent, constellationAgent, defaultAgent, resolveIdentity, getPublicAgent, getPDSAgent, withFallback, resetAgents } from './agents';
10
+ export { searchMusicBrainzRelease, buildCoverArtUrl, searchiTunesArtwork, searchDeezerArtwork, searchLastFmArtwork, findArtwork } from './musicbrainz';
11
+ export { fetchEngagementFromConstellation, fetchAllEngagement } from './engagement';
12
+ export { cache, ATProtoCache, CACHE_TTL } from './cache';
13
+ export { fetchAllRecords, fetchAllUserRecords } from './pagination';
@@ -0,0 +1,4 @@
1
+ export declare function buildPdsBlobUrl(pds: string, did: string, cid: string): string;
2
+ export declare function extractCidFromImageObject(img: any): string | null;
3
+ export declare function extractImageUrlsFromValue(value: any, did: string, limit?: number): string[];
4
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../src/media.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAOjE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,MAAM,EAAE,CA4FtF"}
package/dist/media.js ADDED
@@ -0,0 +1,109 @@
1
+ export function buildPdsBlobUrl(pds, did, cid) {
2
+ return `${pds.replace(/\/$/, '')}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`;
3
+ }
4
+ export function extractCidFromImageObject(img) {
5
+ if (!img)
6
+ return null;
7
+ if (img.image && img.image.ref && img.image.ref.$link)
8
+ return img.image.ref.$link;
9
+ if (img.ref && img.ref.$link)
10
+ return img.ref.$link;
11
+ if (img.cid)
12
+ return img.cid;
13
+ if (typeof img === 'string')
14
+ return img;
15
+ return null;
16
+ }
17
+ export function extractImageUrlsFromValue(value, did, limit = 4) {
18
+ const urls = [];
19
+ try {
20
+ const embed = value?.embed ?? null;
21
+ if (embed) {
22
+ if (embed.$type === 'app.bsky.embed.images#view' && Array.isArray(embed.images)) {
23
+ for (const img of embed.images) {
24
+ const imageUrl = img.fullsize || img.thumb;
25
+ if (imageUrl)
26
+ urls.push(imageUrl);
27
+ else {
28
+ const cid = extractCidFromImageObject(img);
29
+ if (cid)
30
+ urls.push(`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}@jpeg`);
31
+ }
32
+ if (urls.length >= limit)
33
+ return urls;
34
+ }
35
+ }
36
+ if (embed.$type === 'app.bsky.embed.video#view' || embed.$type === 'app.bsky.embed.video') {
37
+ const videoCid = embed?.jobStatus?.blob ??
38
+ embed?.video?.ref?.$link ??
39
+ embed?.video?.cid ??
40
+ null;
41
+ if (videoCid) {
42
+ urls.push(`https://video.bsky.app/watch/${did}/${videoCid}/playlist.m3u8`);
43
+ if (urls.length >= limit)
44
+ return urls;
45
+ }
46
+ }
47
+ if (embed.$type === 'app.bsky.embed.recordWithMedia#view') {
48
+ const media = embed.media;
49
+ if (media &&
50
+ media.$type === 'app.bsky.embed.images#view' &&
51
+ Array.isArray(media.images)) {
52
+ for (const img of media.images) {
53
+ const imageUrl = img.fullsize || img.thumb;
54
+ if (imageUrl)
55
+ urls.push(imageUrl);
56
+ else {
57
+ const cid = extractCidFromImageObject(img);
58
+ if (cid)
59
+ urls.push(`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}@jpeg`);
60
+ }
61
+ if (urls.length >= limit)
62
+ return urls;
63
+ }
64
+ }
65
+ const quotedRecord = embed.record;
66
+ if (quotedRecord) {
67
+ const quotedValue = quotedRecord.value ?? quotedRecord.record?.value ?? null;
68
+ if (quotedValue) {
69
+ const nested = extractImageUrlsFromValue(quotedValue, did, limit - urls.length);
70
+ urls.push(...nested);
71
+ if (urls.length >= limit)
72
+ return urls;
73
+ }
74
+ }
75
+ }
76
+ if (embed.$type === 'app.bsky.embed.record#view' && embed.record) {
77
+ const quotedValue = embed.record.value ?? embed.record.record?.value ?? null;
78
+ if (quotedValue) {
79
+ const nested = extractImageUrlsFromValue(quotedValue, did, limit - urls.length);
80
+ urls.push(...nested);
81
+ if (urls.length >= limit)
82
+ return urls;
83
+ }
84
+ }
85
+ }
86
+ if (Array.isArray(value.embeds)) {
87
+ for (const e of value.embeds) {
88
+ if (e.$type === 'app.bsky.embed.images#view' && Array.isArray(e.images)) {
89
+ for (const img of e.images) {
90
+ const imageUrl = img.fullsize || img.thumb;
91
+ if (imageUrl)
92
+ urls.push(imageUrl);
93
+ else {
94
+ const cid = extractCidFromImageObject(img);
95
+ if (cid)
96
+ urls.push(`https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${cid}@jpeg`);
97
+ }
98
+ if (urls.length >= limit)
99
+ return urls;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch {
106
+ // conservative: return what we have
107
+ }
108
+ return urls.slice(0, limit);
109
+ }
@@ -0,0 +1,10 @@
1
+ export declare function searchMusicBrainzRelease(trackName: string, artistName: string, releaseName?: string): Promise<string | null>;
2
+ export declare function buildCoverArtUrl(releaseMbId: string, size?: 250 | 500 | 1200): string;
3
+ export declare function searchiTunesArtwork(trackName: string, artistName: string, releaseName?: string): Promise<string | null>;
4
+ export declare function searchDeezerArtwork(trackName: string, artistName: string, releaseName?: string): Promise<string | null>;
5
+ export declare function searchLastFmArtwork(trackName: string, artistName: string, releaseName?: string): Promise<string | null>;
6
+ /**
7
+ * Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
8
+ */
9
+ export declare function findArtwork(trackName: string, artistName: string, releaseName?: string, releaseMbId?: string, fetchFn?: typeof fetch): Promise<string | null>;
10
+ //# sourceMappingURL=musicbrainz.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"musicbrainz.d.ts","sourceRoot":"","sources":["../src/musicbrainz.ts"],"names":[],"mappings":"AAiBA,wBAAsB,wBAAwB,CAC7C,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBxB;AAkCD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,GAAE,GAAG,GAAG,GAAG,GAAG,IAAU,GAAG,MAAM,CAE1F;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuBxB;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA2BxB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,OAAO,KAAK,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BxB"}
@@ -0,0 +1,176 @@
1
+ import { cache } from './cache.js';
2
+ export async function searchMusicBrainzRelease(trackName, artistName, releaseName) {
3
+ const cacheKey = `mb:release:${trackName}:${artistName}:${releaseName || 'none'}`;
4
+ const cached = cache.get(cacheKey);
5
+ if (cached !== null)
6
+ return cached;
7
+ try {
8
+ if (releaseName) {
9
+ const result = await searchByReleaseName(releaseName, artistName);
10
+ if (result) {
11
+ cache.set(cacheKey, result);
12
+ return result;
13
+ }
14
+ }
15
+ const result = await searchByTrackName(trackName, artistName);
16
+ if (result) {
17
+ cache.set(cacheKey, result);
18
+ return result;
19
+ }
20
+ cache.set(cacheKey, null);
21
+ return null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ async function searchByReleaseName(releaseName, artistName) {
28
+ try {
29
+ const query = `release:"${releaseName}" AND artist:"${artistName}"`;
30
+ const url = `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(query)}&fmt=json&limit=5`;
31
+ const response = await fetch(url, {
32
+ headers: { 'User-Agent': 'ewancroft.uk/1.0.0 (https://ewancroft.uk)', Accept: 'application/json' }
33
+ });
34
+ if (!response.ok)
35
+ return null;
36
+ const data = await response.json();
37
+ if (!data.releases?.length)
38
+ return null;
39
+ const best = data.releases[0];
40
+ if (best.score < 80)
41
+ return null;
42
+ return best.id;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function searchByTrackName(trackName, artistName) {
49
+ try {
50
+ const query = `recording:"${trackName}" AND artist:"${artistName}"`;
51
+ const url = `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(query)}&fmt=json&limit=5`;
52
+ const response = await fetch(url, {
53
+ headers: { 'User-Agent': 'ewancroft.uk/1.0.0 (https://ewancroft.uk)', Accept: 'application/json' }
54
+ });
55
+ if (!response.ok)
56
+ return null;
57
+ const data = await response.json();
58
+ if (!data.releases?.length)
59
+ return null;
60
+ const best = data.releases[0];
61
+ if (best.score < 75)
62
+ return null;
63
+ return best.id;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ export function buildCoverArtUrl(releaseMbId, size = 500) {
70
+ return `https://coverartarchive.org/release/${releaseMbId}/front-${size}`;
71
+ }
72
+ export async function searchiTunesArtwork(trackName, artistName, releaseName) {
73
+ const cacheKey = `itunes:artwork:${trackName}:${artistName}:${releaseName || 'none'}`;
74
+ const cached = cache.get(cacheKey);
75
+ if (cached !== null)
76
+ return cached;
77
+ try {
78
+ const searchTerm = releaseName ? `${releaseName} ${artistName}` : `${trackName} ${artistName}`;
79
+ const url = `https://itunes.apple.com/search?term=${encodeURIComponent(searchTerm)}&entity=album&limit=5`;
80
+ const response = await fetch(url);
81
+ if (!response.ok) {
82
+ cache.set(cacheKey, null);
83
+ return null;
84
+ }
85
+ const data = await response.json();
86
+ if (!data.results?.length) {
87
+ cache.set(cacheKey, null);
88
+ return null;
89
+ }
90
+ let artworkUrl = data.results[0].artworkUrl100;
91
+ if (artworkUrl) {
92
+ artworkUrl = artworkUrl.replace('100x100', '600x600');
93
+ cache.set(cacheKey, artworkUrl);
94
+ return artworkUrl;
95
+ }
96
+ cache.set(cacheKey, null);
97
+ return null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ export async function searchDeezerArtwork(trackName, artistName, releaseName) {
104
+ // Deezer has CORS restrictions in browsers — skip silently
105
+ return null;
106
+ }
107
+ export async function searchLastFmArtwork(trackName, artistName, releaseName) {
108
+ if (!releaseName)
109
+ return null;
110
+ const cacheKey = `lastfm:artwork:${trackName}:${artistName}:${releaseName}`;
111
+ const cached = cache.get(cacheKey);
112
+ if (cached !== null)
113
+ return cached;
114
+ try {
115
+ const url = `https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=8de8b91ab0c3f8d08a35c33bf0e0e803&artist=${encodeURIComponent(artistName)}&album=${encodeURIComponent(releaseName)}&format=json`;
116
+ const response = await fetch(url);
117
+ if (!response.ok) {
118
+ cache.set(cacheKey, null);
119
+ return null;
120
+ }
121
+ const data = await response.json();
122
+ if (!data.album?.image) {
123
+ cache.set(cacheKey, null);
124
+ return null;
125
+ }
126
+ const images = data.album.image;
127
+ const largeImage = images.find((img) => img.size === 'extralarge') ||
128
+ images.find((img) => img.size === 'large') ||
129
+ images.find((img) => img.size === 'medium');
130
+ if (largeImage?.['#text']) {
131
+ cache.set(cacheKey, largeImage['#text']);
132
+ return largeImage['#text'];
133
+ }
134
+ cache.set(cacheKey, null);
135
+ return null;
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ /**
142
+ * Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
143
+ */
144
+ export async function findArtwork(trackName, artistName, releaseName, releaseMbId, fetchFn) {
145
+ const _fetch = fetchFn || globalThis.fetch;
146
+ // 1. Try Cover Art Archive with known MBID
147
+ if (releaseMbId) {
148
+ const caaUrl = buildCoverArtUrl(releaseMbId, 500);
149
+ try {
150
+ const res = await _fetch(caaUrl, { method: 'HEAD' });
151
+ if (res.ok)
152
+ return caaUrl;
153
+ }
154
+ catch { /* continue */ }
155
+ }
156
+ // 2. Search MusicBrainz for MBID, then try CAA
157
+ const mbId = await searchMusicBrainzRelease(trackName, artistName, releaseName);
158
+ if (mbId) {
159
+ const caaUrl = buildCoverArtUrl(mbId, 500);
160
+ try {
161
+ const res = await _fetch(caaUrl, { method: 'HEAD' });
162
+ if (res.ok)
163
+ return caaUrl;
164
+ }
165
+ catch { /* continue */ }
166
+ }
167
+ // 3. Try iTunes
168
+ const iTunesUrl = await searchiTunesArtwork(trackName, artistName, releaseName);
169
+ if (iTunesUrl)
170
+ return iTunesUrl;
171
+ // 4. Try Last.fm
172
+ const lastFmUrl = await searchLastFmArtwork(trackName, artistName, releaseName);
173
+ if (lastFmUrl)
174
+ return lastFmUrl;
175
+ return null;
176
+ }
@@ -0,0 +1,14 @@
1
+ export interface FetchRecordsConfig {
2
+ repo: string;
3
+ collection: string;
4
+ limit?: number;
5
+ fetchFn?: typeof fetch;
6
+ }
7
+ export interface AtProtoRecord<T = any> {
8
+ uri: string;
9
+ value: T;
10
+ cid?: string;
11
+ }
12
+ export declare function fetchAllRecords<T = any>(config: FetchRecordsConfig): Promise<AtProtoRecord<T>[]>;
13
+ export declare function fetchAllUserRecords<T = any>(did: string, collection: string, fetchFn?: typeof fetch, limit?: number): Promise<AtProtoRecord<T>[]>;
14
+ //# sourceMappingURL=fetchAllRecords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchAllRecords.d.ts","sourceRoot":"","sources":["../../src/pagination/fetchAllRecords.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACvB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,GAAG;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,CAAC,CAAC;IACT,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,wBAAsB,eAAe,CAAC,CAAC,GAAG,GAAG,EAC5C,MAAM,EAAE,kBAAkB,GACxB,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CA8B7B;AAED,wBAAsB,mBAAmB,CAAC,CAAC,GAAG,GAAG,EAChD,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,OAAO,KAAK,EACtB,KAAK,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAE7B"}
@@ -0,0 +1,29 @@
1
+ import { withFallback } from '../agents.js';
2
+ export async function fetchAllRecords(config) {
3
+ const { repo, collection, limit = 100, fetchFn } = config;
4
+ const allRecords = [];
5
+ let cursor;
6
+ try {
7
+ do {
8
+ const records = await withFallback(repo, async (agent) => {
9
+ const response = await agent.com.atproto.repo.listRecords({
10
+ repo,
11
+ collection,
12
+ limit,
13
+ cursor
14
+ });
15
+ cursor = response.data.cursor;
16
+ return response.data.records;
17
+ }, true, fetchFn);
18
+ allRecords.push(...records);
19
+ } while (cursor);
20
+ }
21
+ catch (error) {
22
+ console.warn(`Failed to fetch records from ${collection}:`, error);
23
+ throw error;
24
+ }
25
+ return allRecords;
26
+ }
27
+ export async function fetchAllUserRecords(did, collection, fetchFn, limit) {
28
+ return fetchAllRecords({ repo: did, collection, limit, fetchFn });
29
+ }