Files

568 lines
15 KiB
Go

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)
}