package monstersirenfetch import ( "maps" "os" "slices" "strconv" "strings" ) // AlbumConflictError is returned by [Flatten] if two or more albums share the same CID. type AlbumConflictError struct { Index int Previous Album Current Album } func (e *AlbumConflictError) Error() string { return "album CID conflict on index " + strconv.Itoa(e.Index) + ": " + "previous album " + e.Previous.Name + ", " + "current album " + e.Current.Name } // SongConflictError is returned by [Flatten] if two or more songs belonging to the same [Album] share the same CID. type SongConflictError struct { Index int Previous *Song Current Song } func (e *SongConflictError) Error() string { return "song CID conflict on index " + strconv.Itoa(e.Index) + ": " + "previous song " + e.Previous.Name + ", " + "current song " + e.Current.Name } // CompositeAlbum represents an [Album] with a collection of its associated identifier-only [Song]. type CompositeAlbum struct { // Songs is a map of [Song.CID] to identifier-only [Song]. Songs SongsMap `json:"songs"` *Album } type ( // CompositeAlbumsMap is a map of [Album.CID] to [CompositeAlbum]. CompositeAlbumsMap map[StringInt]CompositeAlbum // SongsMap is a map of [Song.CID] to the address of [Song]. SongsMap map[StringInt]*Song ) func (m CompositeAlbumsMap) String() string { if len(m) == 0 { return "" } compAlbums := slices.Collect(maps.Values(m)) slices.SortFunc(compAlbums, func(a, b CompositeAlbum) int { return int(a.CID - b.CID) }) s := make([]string, len(compAlbums)) var buf strings.Builder for i, ca := range compAlbums { buf.WriteString( "Album: " + ca.Name + " (" + ca.CID.String() + ")\n" + "Cover: " + ca.CoverURL + "\n" + "Artist(s): " + strings.Join(ca.Artists, ", ") + "\n" + "Songs:\n") albumSongs := slices.Collect(maps.Values(ca.Songs)) slices.SortFunc(albumSongs, func(a, b *Song) int { return int(a.CID - b.CID) }) for _, cs := range albumSongs { buf.WriteString( " " + strings.Join(cs.Artists, ", ") + " ─ " + cs.Name + "\n") } s[i] = buf.String() buf.Reset() } return strings.Join(s, "\n") } // Flatten flattens [AlbumsData] and [SongsData] into a [CompositeAlbumsMap]. // All values in [CompositeAlbum.Songs] and the [CompositeAlbum.Album] field are guaranteed to be non-nil. func Flatten(albumData AlbumsData, songsData SongsData) (CompositeAlbumsMap, error) { m := make(CompositeAlbumsMap, len(albumData)) for i, a := range albumData { if c, ok := m[a.CID]; ok { return nil, &AlbumConflictError{Index: i, Previous: *c.Album, Current: a} } m[a.CID] = CompositeAlbum{Songs: make(map[StringInt]*Song), Album: &a} } for i, s := range songsData.List { var c *Song if a, ok := m[s.AlbumCID]; !ok { return nil, os.ErrNotExist } else if c, ok = a.Songs[s.CID]; ok { return nil, &SongConflictError{Index: i, Previous: c, Current: s} } else { a.Songs[s.CID] = &s } } return m, nil }