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

Top days

Changed files
+49 -3
src
components
+49 -3
src/components/LookUp.vue
··· 32 32 const loading = ref(false); 33 33 const artists = ref<{ name: string; plays: number }[]>([]); 34 34 const tracks = ref<{ name: string; artist: string; plays: number }[]>([]); 35 + const topDays = ref<{ date: string; plays: number }[]>([]); 35 36 const totalSongs = ref(0); 36 37 37 38 const formatNumber = (n: number) => n.toLocaleString(); 39 + 40 + // Returns YYYY-MM-DD in the browser's local time zone 41 + const localDayKey = (iso?: string): string | null => { 42 + if (!iso) return null; 43 + const d = new Date(iso); 44 + if (isNaN(d.getTime())) return null; 45 + const y = d.getFullYear(); 46 + const m = (d.getMonth() + 1).toString().padStart(2, "0"); 47 + const day = d.getDate().toString().padStart(2, "0"); 48 + return `${y}-${m}-${day}`; 49 + }; 38 50 39 51 const lookup = async () => { 40 52 loading.value = true; ··· 59 71 let totalCount = 0; 60 72 let inner_tracks: { name: string; artist: string; plays: number }[] = []; 61 73 let inner_artists: { name: string; plays: number }[] = []; 74 + const dayCountMap = new Map<string, number>(); 62 75 63 76 let response = await agent.com.atproto.repo.listRecords({ 64 77 repo: did, ··· 82 95 ) { 83 96 continue; 84 97 } 85 - // new version of lexicon 98 + // Aggregate by artist(s) 86 99 if (play.value?.artists) { 87 100 for (const artist of play.value?.artists) { 88 101 let alreadyPlayed = inner_artists.find( ··· 94 107 plays: 1, 95 108 }); 96 109 } else { 97 - console.log(`Artist already played: ${alreadyPlayed}`); 98 110 alreadyPlayed.plays++; 99 111 } 100 112 } ··· 112 124 } 113 125 } 114 126 127 + // Aggregate by track 115 128 let alreadyPlayed = inner_tracks.find( 116 129 (a) => a.name === play.value.trackName, 117 130 ); ··· 126 139 } else if (alreadyPlayed) { 127 140 alreadyPlayed.plays++; 128 141 } 142 + 143 + // Aggregate by local day using playedTime 144 + const key = localDayKey(play.value?.playedTime); 145 + if (key) { 146 + dayCountMap.set(key, (dayCountMap.get(key) ?? 0) + 1); 147 + } 129 148 } 130 149 131 - // update reactive values incrementally (top 10) 150 + // update reactive values incrementally (top 25) 132 151 artists.value = inner_artists 133 152 .sort((a, b) => b.plays - a.plays) 134 153 .slice(0, 25); ··· 137 156 .slice(0, 25); 138 157 totalSongs.value = totalCount; 139 158 159 + // compute top 25 days 160 + topDays.value = Array.from(dayCountMap.entries()) 161 + .map(([date, plays]) => ({ date, plays })) 162 + .sort((a, b) => b.plays - a.plays) 163 + .slice(0, 25); 164 + 140 165 cursor = response.data.cursor; 141 166 if (!cursor) break; 142 167 ··· 221 246 <td>{{ idx + 1 }}.</td> 222 247 <td>{{ formatNumber(artist.plays) }}</td> 223 248 <td>{{ artist.name }}</td> 249 + </tr> 250 + </tbody> 251 + </table> 252 + </div> 253 + </div> 254 + <div v-if="topDays.length > 0" class="mt-8"> 255 + <h2 class="text-2xl font-bold mb-4">Top Days (Most Songs Played)</h2> 256 + <div class="overflow-x-auto"> 257 + <table class="table w-full"> 258 + <thead> 259 + <tr> 260 + <th>Rank</th> 261 + <th>Plays</th> 262 + <th>Date (yyyy-mm-dd)</th> 263 + </tr> 264 + </thead> 265 + <tbody> 266 + <tr v-for="(day, idx) in topDays" :key="day.date"> 267 + <td>{{ idx + 1 }}.</td> 268 + <td>{{ formatNumber(day.plays) }}</td> 269 + <td>{{ day.date }}</td> 224 270 </tr> 225 271 </tbody> 226 272 </table>