package subsonic import ( "sort" "strings" "time" "unicode" "github.com/benya/temporserv/internal/library" "github.com/benya/temporserv/internal/playlist" "github.com/benya/temporserv/internal/scanner" ) type Envelope struct { SubsonicResponse Response `json:"subsonic-response"` } type Response struct { Status string `json:"status"` Version string `json:"version"` Type string `json:"type"` Server string `json:"serverVersion"` OpenAPI bool `json:"openSubsonic"` Artists *Artists `json:"artists,omitempty"` Artist *ArtistFull `json:"artist,omitempty"` Album *AlbumFull `json:"album,omitempty"` AlbumList2 *AlbumList2 `json:"albumList2,omitempty"` SongsByGenre *SongsByGenre `json:"songsByGenre,omitempty"` Song *SongFull `json:"song,omitempty"` RandomSong []SongRef `json:"randomSongs,omitempty"` SearchResult3 *SearchResult3 `json:"searchResult3,omitempty"` Starred2 *Starred2 `json:"starred2,omitempty"` Playlists *Playlists `json:"playlists,omitempty"` Playlist *Playlist `json:"playlist,omitempty"` MusicFolders *MusicFolders `json:"musicFolders,omitempty"` Genres *Genres `json:"genres,omitempty"` Podcasts *Podcasts `json:"podcasts,omitempty"` NewestPods *NewestPods `json:"newestPodcasts,omitempty"` RadioStations *RadioStations `json:"internetRadioStations,omitempty"` Extensions []Extension `json:"openSubsonicExtensions,omitempty"` ScanStatus *ScanStatus `json:"scanStatus,omitempty"` Error *ErrorRef `json:"error,omitempty"` } type ArtistRef struct { ID string `json:"id"` Name string `json:"name"` } type Artists struct { Index []ArtistIndex `json:"index,omitempty"` } type ArtistIndex struct { Name string `json:"name"` Artist []ArtistRef `json:"artist,omitempty"` } type SongRef struct { ID string `json:"id"` Title string `json:"title"` Album string `json:"album"` Artist string `json:"artist"` AlbumID string `json:"albumId,omitempty"` ArtistID string `json:"artistId,omitempty"` CoverArt string `json:"coverArt,omitempty"` } type ArtistFull struct { ID string `json:"id"` Name string `json:"name"` CoverArt string `json:"coverArt,omitempty"` AlbumCount int `json:"albumCount,omitempty"` Albums []AlbumRef `json:"album,omitempty"` } type AlbumRef struct { ID string `json:"id"` Name string `json:"name,omitempty"` Title string `json:"title"` Artist string `json:"artist"` ArtistID string `json:"artistId"` Year int `json:"year,omitempty"` Genre string `json:"genre,omitempty"` CoverArt string `json:"coverArt,omitempty"` } type AlbumFull struct { ID string `json:"id"` Name string `json:"name"` Artist string `json:"artist"` ArtistID string `json:"artistId"` Year int `json:"year,omitempty"` CoverArt string `json:"coverArt,omitempty"` Song []SongRef `json:"song,omitempty"` } type AlbumList2 struct { Album []AlbumRef `json:"album,omitempty"` } type SongsByGenre struct { Song []SongRef `json:"song,omitempty"` } type SongFull struct { ID string `json:"id"` Title string `json:"title"` Album string `json:"album"` Artist string `json:"artist"` AlbumID string `json:"albumId"` ArtistID string `json:"artistId"` Track int `json:"track,omitempty"` Duration int `json:"duration,omitempty"` CoverArt string `json:"coverArt,omitempty"` } type ScanStatus struct { Scanning bool `json:"scanning"` Count int `json:"count"` FolderCount int `json:"folderCount"` LastError string `json:"lastError,omitempty"` StartedAt string `json:"startedAt,omitempty"` FinishedAt string `json:"finishedAt,omitempty"` } type SearchResult3 struct { Artist []ArtistRef `json:"artist,omitempty"` Album []AlbumRef `json:"album,omitempty"` Song []SongRef `json:"song,omitempty"` } type Starred2 struct { Artist []ArtistRef `json:"artist,omitempty"` Album []AlbumRef `json:"album,omitempty"` Song []SongRef `json:"song,omitempty"` } type Playlists struct { Playlist []PlaylistSummary `json:"playlist,omitempty"` } type Playlist struct { ID string `json:"id"` Name string `json:"name"` Owner string `json:"owner"` Public bool `json:"public"` Comment string `json:"comment,omitempty"` SongCount int `json:"songCount"` Duration int `json:"duration,omitempty"` Created string `json:"created,omitempty"` Changed string `json:"changed,omitempty"` Entry []SongRef `json:"entry,omitempty"` } type PlaylistSummary struct { ID string `json:"id"` Name string `json:"name"` Owner string `json:"owner"` Public bool `json:"public"` SongCount int `json:"songCount"` Duration int `json:"duration,omitempty"` Created string `json:"created,omitempty"` Changed string `json:"changed,omitempty"` } type MusicFolders struct { MusicFolder []MusicFolder `json:"musicFolder,omitempty"` } type MusicFolder struct { ID string `json:"id"` Name string `json:"name"` } type Genres struct { Genre []Genre `json:"genre,omitempty"` } type Genre struct { Value string `json:"value"` AlbumCount int `json:"albumCount,omitempty"` SongCount int `json:"songCount,omitempty"` } type Podcasts struct { Channel []any `json:"channel,omitempty"` } type NewestPods struct { Episode []any `json:"episode,omitempty"` } type RadioStations struct { InternetRadioStation []any `json:"internetRadioStation,omitempty"` } type Extension struct { Name string `json:"name"` Versions []int `json:"versions"` } type ErrorRef struct { Code int `json:"code"` Message string `json:"message"` } func PingResponse() Envelope { return Envelope{ SubsonicResponse: Response{ Status: "ok", Version: "1.16.1", Type: "temporserv", Server: "0.1.0", OpenAPI: true, }, } } func ArtistsResponse(artists []library.Artist) Envelope { response := PingResponse() groups := map[string][]ArtistRef{} for _, artist := range artists { initial := "#" name := []rune(strings.TrimSpace(artist.Name)) if len(name) > 0 { first := unicode.ToUpper(name[0]) if unicode.IsLetter(first) { initial = string(first) } } groups[initial] = append(groups[initial], ArtistRef{ ID: artist.ID, Name: artist.Name, }) } keys := make([]string, 0, len(groups)) for key := range groups { keys = append(keys, key) } sort.Strings(keys) payload := &Artists{} for _, key := range keys { payload.Index = append(payload.Index, ArtistIndex{ Name: key, Artist: groups[key], }) } response.SubsonicResponse.Artists = payload return response } func RandomSongsResponse(tracks []library.Track) Envelope { response := PingResponse() for _, track := range tracks { response.SubsonicResponse.RandomSong = append(response.SubsonicResponse.RandomSong, SongRef{ ID: track.ID, Title: track.Title, Album: track.AlbumTitle, Artist: track.ArtistName, AlbumID: track.AlbumID, ArtistID: track.ArtistID, CoverArt: track.AlbumID, }) } return response } func ArtistResponse(artist library.ArtistDetail) Envelope { response := PingResponse() item := &ArtistFull{ ID: artist.ID, Name: artist.Name, CoverArt: artist.CoverArtID, AlbumCount: artist.AlbumCount, } for _, album := range artist.Albums { item.Albums = append(item.Albums, AlbumRef{ ID: album.ID, Name: album.Title, Title: album.Title, Artist: album.ArtistName, ArtistID: album.ArtistID, Year: album.Year, Genre: album.Genre, CoverArt: album.ID, }) } response.SubsonicResponse.Artist = item return response } func Search3Response(results library.SearchResults) Envelope { response := PingResponse() payload := &SearchResult3{} for _, artist := range results.Artists { payload.Artist = append(payload.Artist, ArtistRef{ ID: artist.ID, Name: artist.Name, }) } for _, album := range results.Albums { payload.Album = append(payload.Album, AlbumRef{ ID: album.ID, Name: album.Title, Title: album.Title, Artist: album.ArtistName, ArtistID: album.ArtistID, Year: album.Year, Genre: album.Genre, CoverArt: album.ID, }) } for _, track := range results.Tracks { payload.Song = append(payload.Song, SongRef{ ID: track.ID, Title: track.Title, Album: track.AlbumTitle, Artist: track.ArtistName, AlbumID: track.AlbumID, ArtistID: track.ArtistID, CoverArt: track.AlbumID, }) } response.SubsonicResponse.SearchResult3 = payload return response } func Starred2Response(results library.StarredResults) Envelope { response := PingResponse() payload := &Starred2{} for _, artist := range results.Artists { payload.Artist = append(payload.Artist, ArtistRef{ ID: artist.ID, Name: artist.Name, }) } for _, album := range results.Albums { payload.Album = append(payload.Album, AlbumRef{ ID: album.ID, Name: album.Title, Title: album.Title, Artist: album.ArtistName, ArtistID: album.ArtistID, Year: album.Year, Genre: album.Genre, CoverArt: album.ID, }) } for _, track := range results.Tracks { payload.Song = append(payload.Song, SongRef{ ID: track.ID, Title: track.Title, Album: track.AlbumTitle, Artist: track.ArtistName, AlbumID: track.AlbumID, ArtistID: track.ArtistID, CoverArt: track.AlbumID, }) } response.SubsonicResponse.Starred2 = payload return response } func PlaylistsResponse(owner string, playlists []playlist.Summary) Envelope { response := PingResponse() payload := &Playlists{} for _, item := range playlists { payload.Playlist = append(payload.Playlist, PlaylistSummary{ ID: item.ID, Name: item.Name, Owner: owner, Public: item.Public, SongCount: item.SongCount, Duration: item.Duration, Created: formatTime(item.CreatedAt), Changed: formatTime(item.UpdatedAt), }) } response.SubsonicResponse.Playlists = payload return response } func PlaylistResponse(owner string, detail playlist.Detail) Envelope { response := PingResponse() item := &Playlist{ ID: detail.ID, Name: detail.Name, Owner: owner, Public: detail.Public, Comment: detail.Comment, SongCount: detail.SongCount, Duration: detail.Duration, Created: formatTime(detail.CreatedAt), Changed: formatTime(detail.UpdatedAt), } for _, track := range detail.Tracks { item.Entry = append(item.Entry, SongRef{ ID: track.ID, Title: track.Title, Album: track.AlbumTitle, Artist: track.ArtistName, AlbumID: track.AlbumID, ArtistID: track.ArtistID, CoverArt: track.AlbumID, }) } response.SubsonicResponse.Playlist = item return response } func AlbumResponse(album library.AlbumDetail) Envelope { response := PingResponse() item := &AlbumFull{ ID: album.ID, Name: album.Title, Artist: album.ArtistName, ArtistID: album.ArtistID, Year: album.Year, CoverArt: album.ID, } for _, track := range album.Tracks { item.Song = append(item.Song, SongRef{ ID: track.ID, Title: track.Title, Album: track.AlbumTitle, Artist: track.ArtistName, AlbumID: track.AlbumID, ArtistID: track.ArtistID, CoverArt: track.AlbumID, }) } response.SubsonicResponse.Album = item return response } func AlbumList2Response(albums []library.Album) Envelope { response := PingResponse() payload := &AlbumList2{} for _, album := range albums { payload.Album = append(payload.Album, AlbumRef{ ID: album.ID, Name: album.Title, Title: album.Title, Artist: album.ArtistName, ArtistID: album.ArtistID, Year: album.Year, Genre: album.Genre, CoverArt: album.ID, }) } response.SubsonicResponse.AlbumList2 = payload return response } func SongsByGenreResponse(tracks []library.Track) Envelope { response := PingResponse() payload := &SongsByGenre{} for _, track := range tracks { payload.Song = append(payload.Song, SongRef{ ID: track.ID, Title: track.Title, Album: track.AlbumTitle, Artist: track.ArtistName, AlbumID: track.AlbumID, ArtistID: track.ArtistID, CoverArt: track.AlbumID, }) } response.SubsonicResponse.SongsByGenre = payload return response } func SongResponse(track library.Track) Envelope { response := PingResponse() response.SubsonicResponse.Song = &SongFull{ ID: track.ID, Title: track.Title, Album: track.AlbumTitle, Artist: track.ArtistName, AlbumID: track.AlbumID, ArtistID: track.ArtistID, Track: track.TrackNumber, Duration: track.DurationSecs, CoverArt: track.AlbumID, } return response } func ScanStatusResponse(status scanner.Status) Envelope { response := PingResponse() response.SubsonicResponse.ScanStatus = &ScanStatus{ Scanning: status.Scanning, Count: status.Tracks, FolderCount: status.Albums, LastError: status.LastError, StartedAt: formatTime(status.StartedAt), FinishedAt: formatTime(status.FinishedAt), } return response } func MusicFoldersResponse() Envelope { response := PingResponse() response.SubsonicResponse.MusicFolders = &MusicFolders{ MusicFolder: []MusicFolder{{ID: "1", Name: "Music"}}, } return response } func GenresResponse(genres []library.GenreSummary) Envelope { response := PingResponse() payload := &Genres{} for _, genre := range genres { payload.Genre = append(payload.Genre, Genre{ Value: genre.Value, AlbumCount: genre.AlbumCount, SongCount: genre.SongCount, }) } response.SubsonicResponse.Genres = payload return response } func PodcastsResponse() Envelope { response := PingResponse() response.SubsonicResponse.Podcasts = &Podcasts{} return response } func NewestPodcastsResponse() Envelope { response := PingResponse() response.SubsonicResponse.NewestPods = &NewestPods{} return response } func InternetRadioStationsResponse() Envelope { response := PingResponse() response.SubsonicResponse.RadioStations = &RadioStations{} return response } func OpenSubsonicExtensionsResponse() Envelope { response := PingResponse() response.SubsonicResponse.Extensions = []Extension{ {Name: "formPost", Versions: []int{1}}, {Name: "apiKeyAuthentication", Versions: []int{1}}, } return response } func ErrorResponse(code int, message string) Envelope { response := PingResponse() response.SubsonicResponse.Status = "failed" response.SubsonicResponse.Error = &ErrorRef{ Code: code, Message: message, } return response } func formatTime(value time.Time) string { if value.IsZero() { return "" } return value.Format(time.RFC3339) }