diff --git a/scripts/misc/spotify-albums-weekday-stats.ts b/scripts/misc/spotify-albums-weekday-stats.ts new file mode 100644 index 0000000..6b537f5 --- /dev/null +++ b/scripts/misc/spotify-albums-weekday-stats.ts @@ -0,0 +1,223 @@ +#!/usr/bin/env tsx + +import { ffetch } from '../../utils/fetch.ts' +import { getEnv } from '../../utils/misc.ts' + +// context: had a discussion in a group chat about which day of the week albums are usually released on, needed a way to find out +// the script is mostly vibe-coded but i have no intentions to run it more than once so who cares + +interface SpotifyTrack { + track: { + id: string + name: string + album: { + id: string + name: string + release_date: string + release_date_precision: 'year' | 'month' | 'day' + } + artists: Array<{ + name: string + }> + } +} + +interface SpotifyAlbum { + id: string + name: string + release_date: string + release_date_precision: 'year' | 'month' | 'day' + artists: Array<{ + name: string + }> +} + +interface SpotifyResponse { + items: T[] + next: string | null + total: number +} + +class SpotifyClient { + private accessToken: string + private baseUrl = 'https://api.spotify.com/v1' + + constructor(accessToken: string) { + this.accessToken = accessToken + } + + private async makeRequest(endpoint: string): Promise { + const response = await ffetch(endpoint, { + baseUrl: this.baseUrl, + headers: { + 'Authorization': `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + throw new Error(`Spotify API error: ${response.status} ${response.statusText}: ${await response.text()}`) + } + + return response.json() + } + + async getLikedTracks(): Promise { + const allTracks: SpotifyTrack[] = [] + let url = '/me/tracks?limit=50' + + while (url) { + const response = await this.makeRequest>(url) + allTracks.push(...response.items) + console.log(`Fetched ${allTracks.length} out of ${response.total} tracks`) + url = response.next ? response.next.replace(this.baseUrl, '') : '' + } + + return allTracks + } + + async getAlbum(albumId: string): Promise { + return this.makeRequest(`/albums/${albumId}`) + } +} + +interface DayStats { + [key: string]: { + count: number + albums: Array<{ + name: string + artist: string + releaseDate: string + }> + } +} + +function getDayOfWeek(dateString: string, precision: string): string { + if (precision === 'year') { + return 'Unknown (Year only)' + } + + if (precision === 'month') { + return 'Unknown (Month only)' + } + + try { + const date = new Date(dateString) + if (Number.isNaN(date.getTime())) { + return 'Unknown (Invalid date)' + } + + const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + return days[date.getDay()] + } catch (error) { + return 'Unknown (Parse error)' + } +} + +async function main() { + const accessToken = getEnv('SPOTIFY_API_TOKEN') + + if (!accessToken) { + console.error('Error: SPOTIFY_API_TOKEN environment variable is required') + process.exit(1) + } + + console.log('šŸŽµ Fetching your liked tracks from Spotify...') + + const spotify = new SpotifyClient(accessToken) + + try { + const likedTracks = await spotify.getLikedTracks() + console.log(`Found ${likedTracks.length} liked tracks`) + + const processedAlbums = new Set() + const dayStats: DayStats = {} + + // First, count unique albums from tracks + const uniqueAlbumIds = new Set() + for (const track of likedTracks) { + uniqueAlbumIds.add(track.track.album.id) + } + + console.log(`šŸ“Š Analyzing ${uniqueAlbumIds.size} unique album release dates...`) + + let processedCount = 0 + let skippedCount = 0 + + for (const track of likedTracks) { + const albumId = track.track.album.id + + // Skip if we've already processed this album + if (processedAlbums.has(albumId)) { + skippedCount++ + continue + } + + processedAlbums.add(albumId) + processedCount++ + + try { + // Get detailed album info + const album = await spotify.getAlbum(albumId) + const dayOfWeek = getDayOfWeek(album.release_date, album.release_date_precision) + + if (!dayStats[dayOfWeek]) { + dayStats[dayOfWeek] = { + count: 0, + albums: [], + } + } + + dayStats[dayOfWeek].count++ + dayStats[dayOfWeek].albums.push({ + name: album.name, + artist: album.artists.map(a => a.name).join(', '), + releaseDate: album.release_date, + }) + + // Progress reporting + if (processedCount % 10 === 0 || processedCount === uniqueAlbumIds.size) { + console.log(`Progress: ${processedCount}/${uniqueAlbumIds.size} albums processed (${skippedCount} skipped)`) + } + + // Add a small delay to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)) + } catch (error) { + console.warn(`Failed to fetch album info for ${track.track.album.name}: ${error}`) + } + } + + console.log('\nšŸ“ˆ Album Release Day Statistics') + console.log('='.repeat(50)) + + // Sort by count (descending) + const sortedStats = Object.entries(dayStats) + .sort(([,a], [,b]) => b.count - a.count) + + for (const [day, stats] of sortedStats) { + console.log(`\n${day}: ${stats.count} albums`) + console.log('-'.repeat(30)) + + // Show top 5 albums for this day + const topAlbums = stats.albums.slice(0, 5) + for (const album of topAlbums) { + console.log(` • ${album.name} by ${album.artist} (${album.releaseDate})`) + } + + if (stats.albums.length > 5) { + console.log(` ... and ${stats.albums.length - 5} more`) + } + } + + console.log('\nšŸ“Š Summary:') + console.log(`Total unique albums found: ${uniqueAlbumIds.size}`) + console.log(`Total unique albums analyzed: ${processedAlbums.size}`) + console.log(`Albums skipped (duplicates): ${skippedCount}`) + console.log(`Total liked tracks: ${likedTracks.length}`) + } catch (error) { + console.error('Error:', error) + process.exit(1) + } +} + +await main()