package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"net/url"
"os"
"sort"
"text/template"
)
type StreamingEvent struct {
TrackName string `json:"master_metadata_track_name"`
AlbumName string `json:"master_metadata_album_album_name"`
ArtistName string `json:"master_metadata_album_artist_name"`
MsPlayed int `json:"ms_played"`
}
type AlbumStats struct {
Name string
Artist string
Plays int
Songs map[string]int
MusicBrainzURL string
MedimopsArtistURL string
MedimopsAlbumURL string
}
type SongPlay struct {
Name string
Plays int
}
type TemplateData struct {
Albums []AlbumStats
Total int
}
func main() {
// Command line flags
inputFile := flag.String("input", "Streaming_History_Audio_2020-2025.json", "Input JSON file from Spotify export")
topLimit := flag.Int("limit", 50, "Limit output to top N albums (0 for all)")
jsonOutput := flag.Bool("json", false, "Output structured JSON instead of Markdown")
outputFile := flag.String("output", "", "Output file (if not specified, writes to stdout)")
flag.Parse()
// Open and read the JSON file
data, err := os.ReadFile(*inputFile)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", *inputFile, err)
os.Exit(1)
}
// Parse JSON into a slice of StreamingEvent
var events []StreamingEvent
if err := json.Unmarshal(data, &events); err != nil {
fmt.Printf("Error parsing JSON: %v\n", err)
os.Exit(1)
}
// Group by album
albumMap := make(map[string]map[string]int)
albumCount := make(map[string]int)
albumArtist := make(map[string]string)
for _, event := range events {
// Skip entries without album or track names
if event.AlbumName == "" || event.TrackName == "" {
continue
}
if _, ok := albumMap[event.AlbumName]; !ok {
albumMap[event.AlbumName] = make(map[string]int)
}
albumMap[event.AlbumName][event.TrackName]++
albumCount[event.AlbumName]++
// Store artist name for the album
if event.ArtistName != "" {
albumArtist[event.AlbumName] = event.ArtistName
}
}
// Create a slice of AlbumStats for sorting
var albums []AlbumStats
for albumName, songs := range albumMap {
// Create search URLs
query := url.QueryEscape(albumArtist[albumName] + " " + albumName)
musicBrainzURL := fmt.Sprintf("https://musicbrainz.org/search?query=%s&type=release&limit=25&method=indexed", query)
artistQuery := url.QueryEscape(albumArtist[albumName])
albumQuery := url.QueryEscape(albumName)
medimopsArtistURL := fmt.Sprintf("https://www.medimops.de/produkte-C0/?fcIsSearch=1&searchparam=%s", artistQuery)
medimopsAlbumURL := fmt.Sprintf("https://www.medimops.de/produkte-C0/?fcIsSearch=1&searchparam=%s", albumQuery)
albums = append(albums, AlbumStats{
Name: albumName,
Artist: albumArtist[albumName],
Plays: albumCount[albumName],
Songs: songs,
MusicBrainzURL: musicBrainzURL,
MedimopsArtistURL: medimopsArtistURL,
MedimopsAlbumURL: medimopsAlbumURL,
})
}
// Sort by number of plays (descending)
sort.Slice(albums, func(i, j int) bool {
return albums[i].Plays > albums[j].Plays
})
// Limit to top X albums
if *topLimit > 0 && len(albums) > *topLimit {
albums = albums[:*topLimit]
}
// Determine output writer
var writer io.Writer
if *outputFile != "" {
file, err := os.Create(*outputFile)
if err != nil {
fmt.Printf("Error creating output file %s: %v\n", *outputFile, err)
os.Exit(1)
}
defer file.Close()
writer = file
} else {
writer = os.Stdout
}
// Output based on format
if *jsonOutput {
generateJSONReport(albums, writer)
} else {
generateMarkdownReport(albums, writer)
}
}
func generateJSONReport(albums []AlbumStats, writer io.Writer) {
// Create JSON output structure
output := struct {
Summary struct {
TotalAlbums int `json:"total_albums"`
GeneratedAt string `json:"generated_at"`
} `json:"summary"`
Albums []AlbumStats `json:"albums"`
}{
Albums: albums,
}
output.Summary.TotalAlbums = len(albums)
output.Summary.GeneratedAt = "2025-08-28" // You could use time.Now() here
// Marshal to JSON with pretty printing
jsonData, err := json.MarshalIndent(output, "", " ")
if err != nil {
fmt.Printf("Error marshaling JSON: %v\n", err)
return
}
// Write to writer
_, err = writer.Write(jsonData)
if err != nil {
fmt.Printf("Error writing JSON: %v\n", err)
return
}
// If writing to stdout, don't show success message (it would interfere)
if writer != os.Stdout {
fmt.Printf("✅ JSON analysis complete!\n")
fmt.Printf("📊 Found %d albums in structured format\n", len(albums))
}
}
func generateMarkdownReport(albums []AlbumStats, writer io.Writer) {
const markdownTemplate = `# Spotify Export Analysis - Albums Worth Buying
## Summary
- **Total Albums Analyzed**: {{.Total}}
- **Albums ranked by play count**:
## Top Albums
| Rank | Album | Artist | Plays | Search Links |
|:------|:------|:--------|-------|--------------|
{{range $index, $album := .Albums}}| {{add $index 1}} | {{$album.Name}} | {{$album.Artist}} | {{$album.Plays}} | [MusicBrainz]({{$album.MusicBrainzURL}}) • [Medimops Artist]({{$album.MedimopsArtistURL}}) • [Medimops Album]({{$album.MedimopsAlbumURL}}) |
{{end}}
## Detailed Breakdown
{{range $index, $album := .Albums}}
### {{add $index 1}}. {{$album.Name}} by {{$album.Artist}}
**Total Plays: {{$album.Plays}}**
**Where to buy:**
- [Search MusicBrainz]({{$album.MusicBrainzURL}}) for official release info
- [Search Medimops for artist]({{$album.MedimopsArtistURL}})
- [Search Medimops for album]({{$album.MedimopsAlbumURL}})
| Song | Plays |
|:------|:------|
{{range $song := sortSongs $album.Songs}}| {{$song.Name}} | {{$song.Plays}} |
{{end}}
{{end}}
`
// Template functions
funcMap := template.FuncMap{
"add": func(a, b int) int {
return a + b
},
"sortSongs": func(songs map[string]int) []SongPlay {
var songList []SongPlay
for song, plays := range songs {
songList = append(songList, SongPlay{Name: song, Plays: plays})
}
sort.Slice(songList, func(i, j int) bool {
return songList[i].Plays > songList[j].Plays
})
return songList
},
}
// Parse and execute template
tmpl, err := template.New("report").Funcs(funcMap).Parse(markdownTemplate)
if err != nil {
fmt.Printf("Error parsing template: %v\n", err)
return
}
templateData := TemplateData{
Albums: albums,
Total: len(albums),
}
err = tmpl.Execute(writer, templateData)
if err != nil {
fmt.Printf("Error executing template: %v\n", err)
return
}
// If writing to stdout, don't show success message (it would interfere)
if writer != os.Stdout {
fmt.Printf("✅ Analysis complete!\n")
fmt.Printf("📊 Found %d albums worth considering\n", len(albums))
}
}
view raw analyze.go hosted with ❤ by GitHub