#!/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()