mirror of
https://git.stupid.fish/teidesu/scripts.git
synced 2025-10-12 15:51:23 +11:00
223 lines
5.9 KiB
TypeScript
223 lines
5.9 KiB
TypeScript
#!/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<T> {
|
|
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<T>(endpoint: string): Promise<T> {
|
|
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<SpotifyTrack[]> {
|
|
const allTracks: SpotifyTrack[] = []
|
|
let url = '/me/tracks?limit=50'
|
|
|
|
while (url) {
|
|
const response = await this.makeRequest<SpotifyResponse<SpotifyTrack>>(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<SpotifyAlbum> {
|
|
return this.makeRequest<SpotifyAlbum>(`/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<string>()
|
|
const dayStats: DayStats = {}
|
|
|
|
// First, count unique albums from tracks
|
|
const uniqueAlbumIds = new Set<string>()
|
|
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()
|