Shows some quick stats about your teal.fm records. Kind of like Spotify Wrapped

typed everything

Changed files
+37 -16
src
components
+37 -16
src/components/LookUp.vue
··· 10 10 } from "@atcute/identity-resolver"; 11 11 import {AtpAgent} from "@atproto/api"; 12 12 13 + // Types for fm.teal.alpha.feed.play 14 + interface PlayArtist { 15 + artistMbId?: string; 16 + artistName: string; 17 + } 18 + interface PlayValue { 19 + $type?: "fm.teal.alpha.feed.play"; 20 + artists?: PlayArtist[]; 21 + // legacy support: earlier records may use artistNames 22 + artistNames?: string[]; 23 + trackName: string; 24 + playedTime?: string; // ISO string 25 + releaseMbId?: string; 26 + releaseName?: string; 27 + recordingMbId?: string; 28 + submissionClientAgent?: string; 29 + } 30 + interface ListRecord<T> { 31 + uri: string; 32 + cid: string; 33 + value: T; 34 + } 35 + 13 36 // handle resolution 14 37 const handleResolver = new CompositeHandleResolver({ 15 38 strategy: "race", ··· 64 87 const doc = await docResolver.resolve(did); 65 88 console.log(doc); 66 89 67 - // const handler = simpleFetchHandler({ service: }); 90 + const endpoint = doc.service?.[0]?.serviceEndpoint as string | undefined; 91 + if (!endpoint) { 92 + throw new Error("could not resolve service endpoint for DID"); 93 + } 68 94 const agent = new AtpAgent({ 69 - service: doc.service[0].serviceEndpoint as string, 95 + service: endpoint, 70 96 }); 71 - let cursor = ""; 97 + let cursor: string | undefined = undefined; 72 98 let totalCount = 0; 73 99 let inner_tracks: { name: string; artist: string; plays: number }[] = []; 74 100 let inner_artists: { name: string; plays: number }[] = []; ··· 78 104 repo: did, 79 105 collection: "fm.teal.alpha.feed.play", 80 106 limit: 100, 81 - cursor: cursor, 107 + ...(cursor ? { cursor } : {}), 82 108 }); 83 109 84 110 // Process pages incrementally while fetching them 85 111 while (true) { 86 - const records = response.data.records ?? []; 112 + const records = (response.data.records as unknown as ListRecord<PlayValue>[]) ?? []; 87 113 totalCount += records.length; 88 114 89 115 for (const play of records) { 90 116 // spot-check if play is valid 91 - if ( 92 - play.value == undefined || 93 - play.value.artistName || 94 - play.value.trackName == undefined 95 - ) { 117 + if (!play.value || !play.value.trackName) { 96 118 continue; 97 119 } 98 120 // Aggregate by artist(s) ··· 129 151 (a) => a.name === play.value.trackName, 130 152 ); 131 153 if (!alreadyPlayed && play?.value) { 154 + const primaryArtist = play.value.artists?.[0]?.artistName ?? play.value.artistNames?.[0] ?? "Unknown Artist"; 132 155 inner_tracks.push({ 133 156 name: play.value.trackName, 134 - artist: play.value?.artists 135 - ? play.value.artists[0].artistName 136 - : play.value.artistNames[0], 157 + artist: primaryArtist, 137 158 plays: 1, 138 159 }); 139 160 } else if (alreadyPlayed) { ··· 169 190 repo: did, 170 191 collection: "fm.teal.alpha.feed.play", 171 192 limit: 100, 172 - cursor: cursor, 193 + ...(cursor ? { cursor } : {}), 173 194 }); 174 195 } 175 - } catch (error) { 176 - errorMessage.value = error.message; 196 + } catch (error: unknown) { 197 + errorMessage.value = error instanceof Error ? error.message : String(error); 177 198 throw error; 178 199 } finally { 179 200 loading.value = false;