fix: improve subsonic client compatibility
This commit is contained in:
@@ -5,9 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -98,8 +100,10 @@ func NewRouter(cfg config.Config, database *sql.DB, scanService *scanner.Service
|
||||
restGet(authed, "getSong", application.subsonicSongByID)
|
||||
restGet(authed, "getRandomSongs", application.subsonicRandomSongs)
|
||||
restGet(authed, "getAlbumList2", application.subsonicAlbumList2)
|
||||
restGet(authed, "getSongsByGenre", application.subsonicSongsByGenre)
|
||||
restGet(authed, "getMusicFolders", application.subsonicMusicFolders)
|
||||
restGet(authed, "getGenres", application.subsonicGenres)
|
||||
restGet(authed, "getOpenSubsonicExtensions", application.subsonicOpenSubsonicExtensions)
|
||||
restGet(authed, "getPodcasts", application.subsonicPodcasts)
|
||||
restGet(authed, "getNewestPodcasts", application.subsonicNewestPodcasts)
|
||||
restGet(authed, "getInternetRadioStations", application.subsonicInternetRadioStations)
|
||||
@@ -469,14 +473,63 @@ func (a app) subsonicAlbumList2(w http.ResponseWriter, r *http.Request) {
|
||||
size = parsed
|
||||
}
|
||||
}
|
||||
albums, err := a.library.Albums(r.Context(), size)
|
||||
offset := parsePositiveInt(r.URL.Query().Get("offset"))
|
||||
albums, err := a.library.Albums(r.Context(), 5000)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, subsonic.ErrorResponse(0, "failed to load albums"))
|
||||
return
|
||||
}
|
||||
if genre := strings.TrimSpace(r.URL.Query().Get("genre")); genre != "" {
|
||||
filtered := make([]library.Album, 0, len(albums))
|
||||
for _, album := range albums {
|
||||
if strings.EqualFold(strings.TrimSpace(album.Genre), genre) {
|
||||
filtered = append(filtered, album)
|
||||
}
|
||||
}
|
||||
albums = filtered
|
||||
}
|
||||
typeName := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("type")))
|
||||
switch typeName {
|
||||
case "alphabeticalbyname":
|
||||
sort.SliceStable(albums, func(i, j int) bool {
|
||||
return strings.ToLower(albums[i].Title) < strings.ToLower(albums[j].Title)
|
||||
})
|
||||
case "random":
|
||||
rand.Shuffle(len(albums), func(i, j int) {
|
||||
albums[i], albums[j] = albums[j], albums[i]
|
||||
})
|
||||
default:
|
||||
sort.SliceStable(albums, func(i, j int) bool {
|
||||
return albums[i].Year > albums[j].Year
|
||||
})
|
||||
}
|
||||
if offset > len(albums) {
|
||||
albums = []library.Album{}
|
||||
} else if offset > 0 {
|
||||
albums = albums[offset:]
|
||||
}
|
||||
if size < len(albums) {
|
||||
albums = albums[:size]
|
||||
}
|
||||
writeJSON(w, http.StatusOK, subsonic.AlbumList2Response(albums))
|
||||
}
|
||||
|
||||
func (a app) subsonicSongsByGenre(w http.ResponseWriter, r *http.Request) {
|
||||
genre := strings.TrimSpace(r.URL.Query().Get("genre"))
|
||||
if genre == "" {
|
||||
writeJSON(w, http.StatusBadRequest, subsonic.ErrorResponse(10, "missing genre"))
|
||||
return
|
||||
}
|
||||
count := parsePositiveInt(r.URL.Query().Get("count"))
|
||||
offset := parsePositiveInt(r.URL.Query().Get("offset"))
|
||||
tracks, err := a.library.SongsByGenre(r.Context(), genre, count, offset)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, subsonic.ErrorResponse(0, "failed to load songs by genre"))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, subsonic.SongsByGenreResponse(tracks))
|
||||
}
|
||||
|
||||
func (a app) subsonicMusicFolders(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, subsonic.MusicFoldersResponse())
|
||||
}
|
||||
@@ -502,6 +555,10 @@ func (a app) subsonicInternetRadioStations(w http.ResponseWriter, r *http.Reques
|
||||
writeJSON(w, http.StatusOK, subsonic.InternetRadioStationsResponse())
|
||||
}
|
||||
|
||||
func (a app) subsonicOpenSubsonicExtensions(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, subsonic.OpenSubsonicExtensionsResponse())
|
||||
}
|
||||
|
||||
func (a app) subsonicSearch3(w http.ResponseWriter, r *http.Request) {
|
||||
query := strings.TrimSpace(r.URL.Query().Get("query"))
|
||||
if query == "" {
|
||||
|
||||
@@ -36,6 +36,7 @@ type Track struct {
|
||||
Title string `json:"title"`
|
||||
ArtistName string `json:"artistName"`
|
||||
AlbumTitle string `json:"albumTitle"`
|
||||
Genre string `json:"genre"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
DurationSecs int `json:"durationSeconds"`
|
||||
BitrateKbps int `json:"bitrateKbps"`
|
||||
@@ -269,7 +270,7 @@ func (s *Service) AlbumByID(ctx context.Context, id string) (AlbumDetail, error)
|
||||
func (s *Service) Tracks(ctx context.Context, limit int) ([]Track, error) {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
@@ -293,6 +294,7 @@ func (s *Service) Tracks(ctx context.Context, limit int) ([]Track, error) {
|
||||
&track.Title,
|
||||
&track.ArtistName,
|
||||
&track.AlbumTitle,
|
||||
&track.Genre,
|
||||
&track.TrackNumber,
|
||||
&track.DurationSecs,
|
||||
&track.BitrateKbps,
|
||||
@@ -313,7 +315,7 @@ func (s *Service) TrackByID(ctx context.Context, id string) (Track, error) {
|
||||
|
||||
err := s.db.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
@@ -327,6 +329,7 @@ func (s *Service) TrackByID(ctx context.Context, id string) (Track, error) {
|
||||
&track.Title,
|
||||
&track.ArtistName,
|
||||
&track.AlbumTitle,
|
||||
&track.Genre,
|
||||
&track.TrackNumber,
|
||||
&track.DurationSecs,
|
||||
&track.BitrateKbps,
|
||||
@@ -415,6 +418,36 @@ func (s *Service) Genres(ctx context.Context) ([]GenreSummary, error) {
|
||||
return genres, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) SongsByGenre(ctx context.Context, genre string, count, offset int) ([]Track, error) {
|
||||
if count <= 0 {
|
||||
count = 50
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
JOIN albums al ON al.id = t.album_id
|
||||
WHERE LOWER(TRIM(COALESCE(al.genre, ''))) = LOWER(TRIM(?))
|
||||
ORDER BY a.name ASC, al.year DESC, al.title ASC, t.disc_number ASC, t.track_number ASC
|
||||
LIMIT ? OFFSET ?`,
|
||||
genre,
|
||||
count,
|
||||
offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query songs by genre: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanTracks(rows)
|
||||
}
|
||||
|
||||
func (s *Service) Star(ctx context.Context, userID string, trackIDs, albumIDs, artistIDs []string) error {
|
||||
return s.updateFavorites(ctx, userID, trackIDs, albumIDs, artistIDs, true)
|
||||
}
|
||||
@@ -463,7 +496,7 @@ func (s *Service) RecentTracks(ctx context.Context, userID string, limit int) ([
|
||||
if userID != "" {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
@@ -488,7 +521,7 @@ func (s *Service) RecentTracks(ctx context.Context, userID string, limit int) ([
|
||||
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
@@ -617,7 +650,7 @@ func (s *Service) albumsByArtistID(ctx context.Context, artistID string) ([]Albu
|
||||
func (s *Service) tracksByAlbumID(ctx context.Context, albumID string) ([]Track, error) {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
@@ -697,8 +730,8 @@ func (s *Service) searchAlbums(ctx context.Context, pattern string, limit int) (
|
||||
func (s *Service) searchTracks(ctx context.Context, pattern string, limit int) ([]Track, error) {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
JOIN albums al ON al.id = t.album_id
|
||||
@@ -817,7 +850,7 @@ func (s *Service) starredAlbums(ctx context.Context, userID string) ([]Album, er
|
||||
func (s *Service) starredTracks(ctx context.Context, userID string) ([]Track, error) {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(al.genre, ''), COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), COALESCE(t.bitrate_kbps, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM favorites f
|
||||
JOIN tracks t ON t.id = f.entity_id
|
||||
@@ -846,6 +879,7 @@ func scanTracks(rows *sql.Rows) ([]Track, error) {
|
||||
&track.Title,
|
||||
&track.ArtistName,
|
||||
&track.AlbumTitle,
|
||||
&track.Genre,
|
||||
&track.TrackNumber,
|
||||
&track.DurationSecs,
|
||||
&track.BitrateKbps,
|
||||
|
||||
@@ -25,6 +25,7 @@ type Response struct {
|
||||
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"`
|
||||
@@ -36,6 +37,7 @@ type Response struct {
|
||||
Podcasts *Podcasts `json:"podcasts,omitempty"`
|
||||
NewestPods *NewestPods `json:"newestPodcasts,omitempty"`
|
||||
RadioStations *RadioStations `json:"internetRadioStations,omitempty"`
|
||||
Extensions *Extensions `json:"openSubsonicExtensions,omitempty"`
|
||||
ScanStatus *ScanStatus `json:"scanStatus,omitempty"`
|
||||
Error *ErrorRef `json:"error,omitempty"`
|
||||
}
|
||||
@@ -74,6 +76,7 @@ type ArtistFull struct {
|
||||
|
||||
type AlbumRef struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
ArtistID string `json:"artistId"`
|
||||
@@ -96,6 +99,10 @@ 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"`
|
||||
@@ -188,6 +195,15 @@ type RadioStations struct {
|
||||
InternetRadioStation []any `json:"internetRadioStation,omitempty"`
|
||||
}
|
||||
|
||||
type Extensions struct {
|
||||
Extension []Extension `json:"extension,omitempty"`
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Versions string `json:"versions"`
|
||||
}
|
||||
|
||||
type ErrorRef struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
@@ -248,7 +264,7 @@ func RandomSongsResponse(tracks []library.Track) Envelope {
|
||||
Artist: track.ArtistName,
|
||||
AlbumID: track.AlbumID,
|
||||
ArtistID: track.ArtistID,
|
||||
CoverArt: track.CoverArtID,
|
||||
CoverArt: track.AlbumID,
|
||||
})
|
||||
}
|
||||
return response
|
||||
@@ -265,12 +281,13 @@ func ArtistResponse(artist library.ArtistDetail) Envelope {
|
||||
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.CoverArtID,
|
||||
CoverArt: album.ID,
|
||||
})
|
||||
}
|
||||
response.SubsonicResponse.Artist = item
|
||||
@@ -289,12 +306,13 @@ func Search3Response(results library.SearchResults) Envelope {
|
||||
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.CoverArtID,
|
||||
CoverArt: album.ID,
|
||||
})
|
||||
}
|
||||
for _, track := range results.Tracks {
|
||||
@@ -305,7 +323,7 @@ func Search3Response(results library.SearchResults) Envelope {
|
||||
Artist: track.ArtistName,
|
||||
AlbumID: track.AlbumID,
|
||||
ArtistID: track.ArtistID,
|
||||
CoverArt: track.CoverArtID,
|
||||
CoverArt: track.AlbumID,
|
||||
})
|
||||
}
|
||||
response.SubsonicResponse.SearchResult3 = payload
|
||||
@@ -324,12 +342,13 @@ func Starred2Response(results library.StarredResults) Envelope {
|
||||
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.CoverArtID,
|
||||
CoverArt: album.ID,
|
||||
})
|
||||
}
|
||||
for _, track := range results.Tracks {
|
||||
@@ -340,7 +359,7 @@ func Starred2Response(results library.StarredResults) Envelope {
|
||||
Artist: track.ArtistName,
|
||||
AlbumID: track.AlbumID,
|
||||
ArtistID: track.ArtistID,
|
||||
CoverArt: track.CoverArtID,
|
||||
CoverArt: track.AlbumID,
|
||||
})
|
||||
}
|
||||
response.SubsonicResponse.Starred2 = payload
|
||||
@@ -387,7 +406,7 @@ func PlaylistResponse(owner string, detail playlist.Detail) Envelope {
|
||||
Artist: track.ArtistName,
|
||||
AlbumID: track.AlbumID,
|
||||
ArtistID: track.ArtistID,
|
||||
CoverArt: track.CoverArtID,
|
||||
CoverArt: track.AlbumID,
|
||||
})
|
||||
}
|
||||
response.SubsonicResponse.Playlist = item
|
||||
@@ -402,14 +421,17 @@ func AlbumResponse(album library.AlbumDetail) Envelope {
|
||||
Artist: album.ArtistName,
|
||||
ArtistID: album.ArtistID,
|
||||
Year: album.Year,
|
||||
CoverArt: album.CoverArtID,
|
||||
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,
|
||||
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
|
||||
@@ -422,18 +444,37 @@ func AlbumList2Response(albums []library.Album) Envelope {
|
||||
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.CoverArtID,
|
||||
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{
|
||||
@@ -445,7 +486,7 @@ func SongResponse(track library.Track) Envelope {
|
||||
ArtistID: track.ArtistID,
|
||||
Track: track.TrackNumber,
|
||||
Duration: track.DurationSecs,
|
||||
CoverArt: track.CoverArtID,
|
||||
CoverArt: track.AlbumID,
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -503,6 +544,17 @@ func InternetRadioStationsResponse() Envelope {
|
||||
return response
|
||||
}
|
||||
|
||||
func OpenSubsonicExtensionsResponse() Envelope {
|
||||
response := PingResponse()
|
||||
response.SubsonicResponse.Extensions = &Extensions{
|
||||
Extension: []Extension{
|
||||
{Name: "formPost", Versions: "1"},
|
||||
{Name: "apiKeyAuthentication", Versions: "1"},
|
||||
},
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func ErrorResponse(code int, message string) Envelope {
|
||||
response := PingResponse()
|
||||
response.SubsonicResponse.Status = "failed"
|
||||
|
||||
Reference in New Issue
Block a user