568 lines
15 KiB
Go
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)
|
|
}
|