163 lines
5.7 KiB
Go
163 lines
5.7 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
|
)
|
|
|
|
type podcastChannelDTO struct {
|
|
ID string `xml:"id,attr" json:"id"`
|
|
URL string `xml:"url,attr" json:"url"`
|
|
Title string `xml:"title,attr" json:"title"`
|
|
Description string `xml:"description,attr" json:"description"`
|
|
CoverArt string `xml:"coverArt,attr" json:"coverArt"`
|
|
OriginalImageURL string `xml:"originalImageUrl,attr" json:"originalImageUrl"`
|
|
Status string `xml:"status,attr" json:"status"`
|
|
ErrorMessage string `xml:"errorMessage,attr" json:"errorMessage"`
|
|
Episodes []podcastEpisodeDTO `xml:"episode" json:"episode"`
|
|
}
|
|
|
|
type podcastEpisodeDTO struct {
|
|
ID string `xml:"id,attr" json:"id"`
|
|
StreamID string `xml:"streamId,attr" json:"streamId"`
|
|
ChannelID string `xml:"channelId,attr" json:"channelId"`
|
|
Title string `xml:"title,attr" json:"title"`
|
|
Description string `xml:"description,attr" json:"description"`
|
|
Status string `xml:"status,attr" json:"status"`
|
|
CoverArt string `xml:"coverArt,attr" json:"coverArt"`
|
|
PublishDate time.Time `xml:"publishDate,attr" json:"publishDate"`
|
|
Duration int `xml:"duration,attr" json:"duration"`
|
|
Size int64 `xml:"size,attr" json:"size"`
|
|
BitRate int `xml:"bitRate,attr" json:"bitRate"`
|
|
ContentType string `xml:"contentType,attr" json:"contentType"`
|
|
}
|
|
|
|
type podcastAPIResponse struct {
|
|
XMLName xml.Name `xml:"subsonic-response" json:"-"`
|
|
Status string `xml:"status,attr" json:"status"`
|
|
Error *struct {
|
|
Code int `xml:"code,attr" json:"code"`
|
|
Message string `xml:"message,attr" json:"message"`
|
|
} `xml:"error" json:"error"`
|
|
Podcasts struct {
|
|
Channels []podcastChannelDTO `xml:"channel" json:"channel"`
|
|
} `xml:"podcasts" json:"podcasts"`
|
|
Newest struct {
|
|
Episodes []podcastEpisodeDTO `xml:"episode" json:"episode"`
|
|
} `xml:"newestPodcasts" json:"newestPodcasts"`
|
|
Episode *podcastEpisodeDTO `xml:"podcastEpisode" json:"podcastEpisode"`
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) podcastRequest(endpoint string, params url.Values) (*podcastAPIResponse, error) {
|
|
endpoint += ".view"
|
|
resp, err := s.client.Request(http.MethodGet, endpoint, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("podcast API: HTTP %s", resp.Status)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out podcastAPIResponse
|
|
if s.client.UseJSON {
|
|
var wrapper struct {
|
|
Response podcastAPIResponse `json:"subsonic-response"`
|
|
}
|
|
if err = json.Unmarshal(body, &wrapper); err != nil {
|
|
return nil, err
|
|
}
|
|
out = wrapper.Response
|
|
} else if err = xml.Unmarshal(body, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
if out.Status != "ok" {
|
|
if out.Error != nil {
|
|
return nil, fmt.Errorf("podcast API error %d: %s", out.Error.Code, out.Error.Message)
|
|
}
|
|
return nil, fmt.Errorf("podcast API returned status %q", out.Status)
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
func episodeFromDTO(e podcastEpisodeDTO, ch *mediaprovider.PodcastChannel) *mediaprovider.PodcastEpisode {
|
|
p := &mediaprovider.PodcastEpisode{ID: e.ID, StreamID: e.StreamID, ChannelID: e.ChannelID, Title: e.Title,
|
|
Description: e.Description, Status: e.Status, CoverArtID: e.CoverArt, PublishDate: e.PublishDate,
|
|
Duration: time.Duration(e.Duration) * time.Second, Size: e.Size, BitRate: e.BitRate, ContentType: e.ContentType}
|
|
if ch != nil {
|
|
p.ChannelTitle = ch.Title
|
|
if p.ChannelID == "" {
|
|
p.ChannelID = ch.ID
|
|
}
|
|
if p.CoverArtID == "" {
|
|
p.CoverArtID = ch.CoverArtID
|
|
}
|
|
p.OriginalImageURL = ch.OriginalImageURL
|
|
}
|
|
return p
|
|
}
|
|
|
|
func channelFromDTO(c podcastChannelDTO) *mediaprovider.PodcastChannel {
|
|
ch := &mediaprovider.PodcastChannel{ID: c.ID, URL: c.URL, Title: c.Title, Description: c.Description,
|
|
CoverArtID: c.CoverArt, OriginalImageURL: c.OriginalImageURL, Status: c.Status, ErrorMessage: c.ErrorMessage}
|
|
for _, e := range c.Episodes {
|
|
ch.Episodes = append(ch.Episodes, episodeFromDTO(e, ch))
|
|
}
|
|
return ch
|
|
}
|
|
|
|
func (s *subsonicMediaProvider) GetPodcastChannels() ([]*mediaprovider.PodcastChannel, error) {
|
|
r, err := s.podcastRequest("getPodcasts", url.Values{"includeEpisodes": {"false"}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]*mediaprovider.PodcastChannel, 0, len(r.Podcasts.Channels))
|
|
for _, c := range r.Podcasts.Channels {
|
|
result = append(result, channelFromDTO(c))
|
|
}
|
|
return result, nil
|
|
}
|
|
func (s *subsonicMediaProvider) GetPodcastChannel(id string) (*mediaprovider.PodcastChannel, error) {
|
|
r, err := s.podcastRequest("getPodcasts", url.Values{"id": {id}, "includeEpisodes": {"true"}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(r.Podcasts.Channels) == 0 {
|
|
return nil, errors.New("podcast channel not found")
|
|
}
|
|
return channelFromDTO(r.Podcasts.Channels[0]), nil
|
|
}
|
|
func (s *subsonicMediaProvider) GetNewestPodcastEpisodes(count int) ([]*mediaprovider.PodcastEpisode, error) {
|
|
r, err := s.podcastRequest("getNewestPodcasts", url.Values{"count": {strconv.Itoa(count)}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]*mediaprovider.PodcastEpisode, 0, len(r.Newest.Episodes))
|
|
for _, e := range r.Newest.Episodes {
|
|
result = append(result, episodeFromDTO(e, nil))
|
|
}
|
|
return result, nil
|
|
}
|
|
func (s *subsonicMediaProvider) GetPodcastEpisode(id string) (*mediaprovider.PodcastEpisode, error) {
|
|
r, err := s.podcastRequest("getPodcastEpisode", url.Values{"id": {id}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.Episode == nil {
|
|
return nil, errors.New("podcast episode not found")
|
|
}
|
|
return episodeFromDTO(*r.Episode, nil), nil
|
|
}
|