2020-01-20 00:21:44 +01:00
|
|
|
package subsonic
|
2016-03-03 02:00:55 +01:00
|
|
|
|
|
|
|
import (
|
2020-03-16 17:56:15 +01:00
|
|
|
"context"
|
2020-01-07 20:56:26 +01:00
|
|
|
"net/http"
|
2016-03-12 02:49:01 +01:00
|
|
|
"time"
|
|
|
|
|
2020-01-24 01:44:08 +01:00
|
|
|
"github.com/deluan/navidrome/conf"
|
|
|
|
"github.com/deluan/navidrome/engine"
|
|
|
|
"github.com/deluan/navidrome/log"
|
|
|
|
"github.com/deluan/navidrome/model"
|
|
|
|
"github.com/deluan/navidrome/server/subsonic/responses"
|
|
|
|
"github.com/deluan/navidrome/utils"
|
2016-03-03 02:00:55 +01:00
|
|
|
)
|
|
|
|
|
2016-03-09 16:22:10 +01:00
|
|
|
type BrowsingController struct {
|
2016-03-08 14:48:47 +01:00
|
|
|
browser engine.Browser
|
2016-03-03 02:00:55 +01:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func NewBrowsingController(browser engine.Browser) *BrowsingController {
|
|
|
|
return &BrowsingController{browser: browser}
|
2016-03-03 02:00:55 +01:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func (c *BrowsingController) GetMusicFolders(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-01-22 14:32:31 +01:00
|
|
|
mediaFolderList, _ := c.browser.MediaFolders(r.Context())
|
2016-03-20 18:14:04 +01:00
|
|
|
folders := make([]responses.MusicFolder, len(mediaFolderList))
|
|
|
|
for i, f := range mediaFolderList {
|
2020-05-10 04:29:02 +02:00
|
|
|
folders[i].Id = f.ID
|
2016-03-09 16:22:10 +01:00
|
|
|
folders[i].Name = f.Name
|
|
|
|
}
|
2020-01-11 23:18:20 +01:00
|
|
|
response := NewResponse()
|
2016-03-09 16:22:10 +01:00
|
|
|
response.MusicFolders = &responses.MusicFolders{Folders: folders}
|
2020-01-07 20:56:26 +01:00
|
|
|
return response, nil
|
2016-03-09 16:22:10 +01:00
|
|
|
}
|
|
|
|
|
2020-02-01 23:17:05 +01:00
|
|
|
func (c *BrowsingController) getArtistIndex(r *http.Request, musicFolderId string, ifModifiedSince time.Time) (*responses.Indexes, error) {
|
|
|
|
indexes, lastModified, err := c.browser.Indexes(r.Context(), musicFolderId, ifModifiedSince)
|
2016-03-09 16:22:10 +01:00
|
|
|
if err != nil {
|
2020-01-09 02:45:07 +01:00
|
|
|
log.Error(r, "Error retrieving Indexes", "error", err)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
2016-03-09 16:22:10 +01:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
res := &responses.Indexes{
|
2020-01-24 07:29:31 +01:00
|
|
|
IgnoredArticles: conf.Server.IgnoredArticles,
|
2020-05-10 03:00:38 +02:00
|
|
|
LastModified: utils.ToMillis(lastModified),
|
2016-03-09 16:22:10 +01:00
|
|
|
}
|
|
|
|
|
2016-03-20 18:08:24 +01:00
|
|
|
res.Index = make([]responses.Index, len(indexes))
|
|
|
|
for i, idx := range indexes {
|
2020-01-11 01:41:35 +01:00
|
|
|
res.Index[i].Name = idx.ID
|
2016-03-09 16:22:10 +01:00
|
|
|
res.Index[i].Artists = make([]responses.Artist, len(idx.Artists))
|
|
|
|
for j, a := range idx.Artists {
|
2020-01-18 02:46:19 +01:00
|
|
|
res.Index[i].Artists[j].Id = a.ID
|
|
|
|
res.Index[i].Artists[j].Name = a.Name
|
2016-03-28 02:13:00 +02:00
|
|
|
res.Index[i].Artists[j].AlbumCount = a.AlbumCount
|
2016-03-09 16:22:10 +01:00
|
|
|
}
|
|
|
|
}
|
2020-01-07 20:56:26 +01:00
|
|
|
return res, nil
|
2016-03-28 02:35:10 +02:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func (c *BrowsingController) GetIndexes(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-02-07 00:55:38 +01:00
|
|
|
musicFolderId := utils.ParamString(r, "musicFolderId")
|
|
|
|
ifModifiedSince := utils.ParamTime(r, "ifModifiedSince", time.Time{})
|
2016-03-28 02:35:10 +02:00
|
|
|
|
2020-02-01 23:17:05 +01:00
|
|
|
res, err := c.getArtistIndex(r, musicFolderId, ifModifiedSince)
|
2020-01-07 20:56:26 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-03-09 16:22:10 +01:00
|
|
|
|
2020-01-11 23:18:20 +01:00
|
|
|
response := NewResponse()
|
2020-01-07 20:56:26 +01:00
|
|
|
response.Indexes = res
|
|
|
|
return response, nil
|
2016-03-09 16:22:10 +01:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func (c *BrowsingController) GetArtists(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-02-07 00:55:38 +01:00
|
|
|
musicFolderId := utils.ParamString(r, "musicFolderId")
|
2020-02-01 23:17:05 +01:00
|
|
|
res, err := c.getArtistIndex(r, musicFolderId, time.Time{})
|
2020-01-07 20:56:26 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-03-28 02:35:10 +02:00
|
|
|
|
2020-01-11 23:18:20 +01:00
|
|
|
response := NewResponse()
|
2020-01-07 20:56:26 +01:00
|
|
|
response.Artist = res
|
|
|
|
return response, nil
|
2016-03-28 02:35:10 +02:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func (c *BrowsingController) GetMusicDirectory(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-02-07 00:55:38 +01:00
|
|
|
id := utils.ParamString(r, "id")
|
2020-01-09 02:45:07 +01:00
|
|
|
dir, err := c.browser.Directory(r.Context(), id)
|
2016-03-03 15:50:50 +01:00
|
|
|
switch {
|
2020-01-15 04:22:34 +01:00
|
|
|
case err == model.ErrNotFound:
|
2020-01-10 05:33:01 +01:00
|
|
|
log.Error(r, "Requested ID not found ", "id", id)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorDataNotFound, "Directory not found")
|
2016-03-08 14:48:47 +01:00
|
|
|
case err != nil:
|
2020-01-09 02:45:07 +01:00
|
|
|
log.Error(err)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
2016-03-03 02:00:55 +01:00
|
|
|
}
|
|
|
|
|
2020-01-11 23:18:20 +01:00
|
|
|
response := NewResponse()
|
2020-03-16 17:56:15 +01:00
|
|
|
response.Directory = c.buildDirectory(r.Context(), dir)
|
2020-01-07 20:56:26 +01:00
|
|
|
return response, nil
|
2016-03-25 05:04:22 +01:00
|
|
|
}
|
2016-03-08 14:48:47 +01:00
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func (c *BrowsingController) GetArtist(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-02-07 00:55:38 +01:00
|
|
|
id := utils.ParamString(r, "id")
|
2020-01-09 02:45:07 +01:00
|
|
|
dir, err := c.browser.Artist(r.Context(), id)
|
2016-03-28 03:27:45 +02:00
|
|
|
switch {
|
2020-01-15 04:22:34 +01:00
|
|
|
case err == model.ErrNotFound:
|
2020-01-10 05:33:01 +01:00
|
|
|
log.Error(r, "Requested ArtistID not found ", "id", id)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorDataNotFound, "Artist not found")
|
2016-03-28 03:27:45 +02:00
|
|
|
case err != nil:
|
2020-01-09 02:45:07 +01:00
|
|
|
log.Error(r, err)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
2016-03-28 03:27:45 +02:00
|
|
|
}
|
|
|
|
|
2020-01-11 23:18:20 +01:00
|
|
|
response := NewResponse()
|
2020-03-16 17:56:15 +01:00
|
|
|
response.ArtistWithAlbumsID3 = c.buildArtist(r.Context(), dir)
|
2020-01-07 20:56:26 +01:00
|
|
|
return response, nil
|
2016-03-28 03:27:45 +02:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func (c *BrowsingController) GetAlbum(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-02-07 00:55:38 +01:00
|
|
|
id := utils.ParamString(r, "id")
|
2020-01-09 02:45:07 +01:00
|
|
|
dir, err := c.browser.Album(r.Context(), id)
|
2016-03-28 15:16:03 +02:00
|
|
|
switch {
|
2020-01-15 04:22:34 +01:00
|
|
|
case err == model.ErrNotFound:
|
2020-01-10 05:33:01 +01:00
|
|
|
log.Error(r, "Requested ID not found ", "id", id)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorDataNotFound, "Album not found")
|
2016-03-28 15:16:03 +02:00
|
|
|
case err != nil:
|
2020-01-09 02:45:07 +01:00
|
|
|
log.Error(r, err)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
2016-03-28 15:16:03 +02:00
|
|
|
}
|
|
|
|
|
2020-01-11 23:18:20 +01:00
|
|
|
response := NewResponse()
|
2020-03-16 17:56:15 +01:00
|
|
|
response.AlbumWithSongsID3 = c.buildAlbum(r.Context(), dir)
|
2020-01-07 20:56:26 +01:00
|
|
|
return response, nil
|
2016-03-28 15:16:03 +02:00
|
|
|
}
|
|
|
|
|
2020-01-07 20:56:26 +01:00
|
|
|
func (c *BrowsingController) GetSong(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-02-07 00:55:38 +01:00
|
|
|
id := utils.ParamString(r, "id")
|
2020-01-22 05:01:43 +01:00
|
|
|
song, err := c.browser.GetSong(r.Context(), id)
|
2016-03-25 05:04:22 +01:00
|
|
|
switch {
|
2020-01-15 04:22:34 +01:00
|
|
|
case err == model.ErrNotFound:
|
2020-01-10 05:33:01 +01:00
|
|
|
log.Error(r, "Requested ID not found ", "id", id)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorDataNotFound, "Song not found")
|
2016-03-25 05:04:22 +01:00
|
|
|
case err != nil:
|
2020-01-09 02:45:07 +01:00
|
|
|
log.Error(r, err)
|
2020-01-07 20:56:26 +01:00
|
|
|
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
2016-03-25 05:04:22 +01:00
|
|
|
}
|
|
|
|
|
2020-01-11 23:18:20 +01:00
|
|
|
response := NewResponse()
|
2020-03-16 17:56:15 +01:00
|
|
|
child := ToChild(r.Context(), *song)
|
2016-03-25 05:04:22 +01:00
|
|
|
response.Song = &child
|
2020-01-07 20:56:26 +01:00
|
|
|
return response, nil
|
2016-03-03 02:00:55 +01:00
|
|
|
}
|
2016-03-03 02:50:16 +01:00
|
|
|
|
2020-01-15 23:49:09 +01:00
|
|
|
func (c *BrowsingController) GetGenres(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
2020-01-22 14:32:31 +01:00
|
|
|
genres, err := c.browser.GetGenres(r.Context())
|
2020-01-15 23:49:09 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Error(r, err)
|
|
|
|
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
|
|
|
}
|
|
|
|
|
|
|
|
response := NewResponse()
|
|
|
|
response.Genres = ToGenres(genres)
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
2020-07-31 20:00:26 +02:00
|
|
|
const placeholderArtistImageSmallUrl = "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png"
|
|
|
|
const placeholderArtistImageMediumUrl = "https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png"
|
|
|
|
const placeholderArtistImageLargeUrl = "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
|
2020-02-10 01:42:37 +01:00
|
|
|
|
|
|
|
// TODO Integrate with Last.FM
|
|
|
|
func (c *BrowsingController) GetArtistInfo(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
|
|
|
response := NewResponse()
|
|
|
|
response.ArtistInfo = &responses.ArtistInfo{}
|
|
|
|
response.ArtistInfo.Biography = "Biography not available"
|
2020-07-31 20:00:26 +02:00
|
|
|
response.ArtistInfo.SmallImageUrl = placeholderArtistImageSmallUrl
|
|
|
|
response.ArtistInfo.MediumImageUrl = placeholderArtistImageMediumUrl
|
|
|
|
response.ArtistInfo.LargeImageUrl = placeholderArtistImageLargeUrl
|
2020-02-10 01:42:37 +01:00
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO Integrate with Last.FM
|
|
|
|
func (c *BrowsingController) GetArtistInfo2(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
|
|
|
response := NewResponse()
|
|
|
|
response.ArtistInfo2 = &responses.ArtistInfo2{}
|
|
|
|
response.ArtistInfo2.Biography = "Biography not available"
|
2020-07-31 20:00:26 +02:00
|
|
|
response.ArtistInfo2.SmallImageUrl = placeholderArtistImageSmallUrl
|
|
|
|
response.ArtistInfo2.MediumImageUrl = placeholderArtistImageSmallUrl
|
|
|
|
response.ArtistInfo2.LargeImageUrl = placeholderArtistImageSmallUrl
|
2020-02-10 01:42:37 +01:00
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
2020-03-16 17:56:15 +01:00
|
|
|
func (c *BrowsingController) buildDirectory(ctx context.Context, d *engine.DirectoryInfo) *responses.Directory {
|
2016-03-21 14:35:18 +01:00
|
|
|
dir := &responses.Directory{
|
2016-03-21 15:24:40 +01:00
|
|
|
Id: d.Id,
|
|
|
|
Name: d.Name,
|
|
|
|
Parent: d.Parent,
|
|
|
|
PlayCount: d.PlayCount,
|
2016-03-28 03:27:45 +02:00
|
|
|
AlbumCount: d.AlbumCount,
|
2016-03-21 15:24:40 +01:00
|
|
|
UserRating: d.UserRating,
|
2016-03-21 14:35:18 +01:00
|
|
|
}
|
|
|
|
if !d.Starred.IsZero() {
|
|
|
|
dir.Starred = &d.Starred
|
|
|
|
}
|
2016-03-08 14:48:47 +01:00
|
|
|
|
2020-03-16 17:56:15 +01:00
|
|
|
dir.Child = ToChildren(ctx, d.Entries)
|
2016-03-03 15:50:50 +01:00
|
|
|
return dir
|
|
|
|
}
|
2016-03-28 03:27:45 +02:00
|
|
|
|
2020-03-16 17:56:15 +01:00
|
|
|
func (c *BrowsingController) buildArtist(ctx context.Context, d *engine.DirectoryInfo) *responses.ArtistWithAlbumsID3 {
|
2016-03-28 15:16:03 +02:00
|
|
|
dir := &responses.ArtistWithAlbumsID3{}
|
|
|
|
dir.Id = d.Id
|
|
|
|
dir.Name = d.Name
|
|
|
|
dir.AlbumCount = d.AlbumCount
|
|
|
|
dir.CoverArt = d.CoverArt
|
2016-03-28 03:27:45 +02:00
|
|
|
if !d.Starred.IsZero() {
|
|
|
|
dir.Starred = &d.Starred
|
|
|
|
}
|
|
|
|
|
2020-03-16 17:56:15 +01:00
|
|
|
dir.Album = ToAlbums(ctx, d.Entries)
|
2016-03-28 03:27:45 +02:00
|
|
|
return dir
|
|
|
|
}
|
2016-03-28 15:16:03 +02:00
|
|
|
|
2020-03-16 17:56:15 +01:00
|
|
|
func (c *BrowsingController) buildAlbum(ctx context.Context, d *engine.DirectoryInfo) *responses.AlbumWithSongsID3 {
|
2016-03-28 15:16:03 +02:00
|
|
|
dir := &responses.AlbumWithSongsID3{}
|
|
|
|
dir.Id = d.Id
|
|
|
|
dir.Name = d.Name
|
|
|
|
dir.Artist = d.Artist
|
|
|
|
dir.ArtistId = d.ArtistId
|
|
|
|
dir.CoverArt = d.CoverArt
|
|
|
|
dir.SongCount = d.SongCount
|
|
|
|
dir.Duration = d.Duration
|
2020-05-20 05:50:27 +02:00
|
|
|
dir.PlayCount = d.PlayCount
|
2016-03-28 15:16:03 +02:00
|
|
|
dir.Year = d.Year
|
|
|
|
dir.Genre = d.Genre
|
|
|
|
if !d.Created.IsZero() {
|
|
|
|
dir.Created = &d.Created
|
|
|
|
}
|
|
|
|
if !d.Starred.IsZero() {
|
|
|
|
dir.Starred = &d.Starred
|
|
|
|
}
|
|
|
|
|
2020-03-16 17:56:15 +01:00
|
|
|
dir.Song = ToChildren(ctx, d.Entries)
|
2016-03-28 15:16:03 +02:00
|
|
|
return dir
|
|
|
|
}
|