navidrome/server/subsonic/helpers.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

252 lines
6.5 KiB
Go
Raw Normal View History

package subsonic
import (
"context"
"fmt"
"mime"
"net/http"
2023-01-12 00:14:34 +01:00
"net/url"
"path/filepath"
2023-01-12 00:14:34 +01:00
"strconv"
"strings"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/artwork"
2020-01-24 01:44:08 +01:00
"github.com/navidrome/navidrome/model"
2020-05-13 22:49:55 +02:00
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server"
2020-01-24 01:44:08 +01:00
"github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils"
)
func newResponse() *responses.Subsonic {
2022-09-15 03:09:39 +02:00
return &responses.Subsonic{Status: "ok", Version: Version, Type: consts.AppName, ServerVersion: consts.Version}
}
2020-10-27 20:23:29 +01:00
func requiredParamString(r *http.Request, param string) (string, error) {
p := utils.ParamString(r, param)
if p == "" {
2020-10-27 20:23:29 +01:00
return "", newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
}
return p, nil
}
2020-10-27 20:23:29 +01:00
func requiredParamStrings(r *http.Request, param string) ([]string, error) {
ps := utils.ParamStrings(r, param)
if len(ps) == 0 {
2020-10-27 20:23:29 +01:00
return nil, newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
}
return ps, nil
}
2020-10-27 20:23:29 +01:00
func requiredParamInt(r *http.Request, param string) (int, error) {
p := utils.ParamString(r, param)
if p == "" {
2020-10-27 20:23:29 +01:00
return 0, newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
}
return utils.ParamInt(r, param, 0), nil
}
2020-10-27 20:23:29 +01:00
type subError struct {
code int
messages []interface{}
}
func newError(code int, message ...interface{}) error {
2020-10-27 20:23:29 +01:00
return subError{
code: code,
messages: message,
}
}
2020-10-27 20:23:29 +01:00
func (e subError) Error() string {
var msg string
if len(e.messages) == 0 {
msg = responses.ErrorMsg(e.code)
} else {
msg = fmt.Sprintf(e.messages[0].(string), e.messages[1:]...)
}
return msg
}
2021-10-30 04:55:28 +02:00
func getUser(ctx context.Context) model.User {
user, ok := request.UserFrom(ctx)
if ok {
2021-10-30 04:55:28 +02:00
return user
}
2021-10-30 04:55:28 +02:00
return model.User{}
}
func toArtists(r *http.Request, artists model.Artists) []responses.Artist {
as := make([]responses.Artist, len(artists))
for i, artist := range artists {
as[i] = toArtist(r, artist)
}
return as
}
func toArtist(r *http.Request, a model.Artist) responses.Artist {
artist := responses.Artist{
Id: a.ID,
Name: a.Name,
AlbumCount: a.AlbumCount,
UserRating: a.Rating,
2022-12-31 22:58:07 +01:00
CoverArt: a.CoverArtID().String(),
2023-01-12 00:14:34 +01:00
ArtistImageUrl: publicImageURL(r, a.CoverArtID(), 0),
}
if a.Starred {
artist.Starred = &a.StarredAt
}
return artist
}
func toArtistID3(r *http.Request, a model.Artist) responses.ArtistID3 {
artist := responses.ArtistID3{
Id: a.ID,
Name: a.Name,
AlbumCount: a.AlbumCount,
2022-12-31 22:58:07 +01:00
CoverArt: a.CoverArtID().String(),
2023-01-12 00:14:34 +01:00
ArtistImageUrl: publicImageURL(r, a.CoverArtID(), 0),
UserRating: a.Rating,
}
if a.Starred {
artist.Starred = &a.StarredAt
}
return artist
}
2023-01-12 00:14:34 +01:00
func publicImageURL(r *http.Request, artID model.ArtworkID, size int) string {
link := artwork.EncodeArtworkID(artID)
path := filepath.Join(consts.URLPathPublicImages, link)
params := url.Values{}
if size > 0 {
params.Add("size", strconv.Itoa(size))
}
return server.AbsoluteURL(r, path, params)
}
func toGenres(genres model.Genres) *responses.Genres {
2020-01-15 23:49:09 +01:00
response := make([]responses.Genre, len(genres))
for i, g := range genres {
response[i] = responses.Genre{
Name: g.Name,
SongCount: g.SongCount,
AlbumCount: g.AlbumCount,
}
2020-01-15 23:49:09 +01:00
}
return &responses.Genres{Genre: response}
}
func getTranscoding(ctx context.Context) (format string, bitRate int) {
2020-05-13 22:49:55 +02:00
if trc, ok := request.TranscodingFrom(ctx); ok {
format = trc.TargetFormat
}
2020-05-13 22:49:55 +02:00
if plr, ok := request.PlayerFrom(ctx); ok {
bitRate = plr.MaxBitRate
}
return
}
// This seems to be duplicated, but it is an initial step into merging `engine` and the `subsonic` packages,
// In the future there won't be any conversion to/from `engine. Entry` anymore
func childFromMediaFile(ctx context.Context, mf model.MediaFile) responses.Child {
child := responses.Child{}
child.Id = mf.ID
child.Title = mf.Title
child.IsDir = false
child.Parent = mf.AlbumID
child.Album = mf.Album
child.Year = mf.Year
child.Artist = mf.Artist
child.Genre = mf.Genre
child.Track = mf.TrackNumber
child.Duration = int(mf.Duration)
child.Size = mf.Size
child.Suffix = mf.Suffix
child.BitRate = mf.BitRate
2022-12-19 19:59:24 +01:00
child.CoverArt = mf.CoverArtID().String()
child.ContentType = mf.ContentType()
player, ok := request.PlayerFrom(ctx)
if ok && player.ReportRealPath {
child.Path = mf.Path
} else {
child.Path = fakePath(mf)
}
child.DiscNumber = mf.DiscNumber
child.Created = &mf.CreatedAt
child.AlbumId = mf.AlbumID
child.ArtistId = mf.ArtistID
child.Type = "music"
child.PlayCount = mf.PlayCount
if mf.PlayCount > 0 {
child.Played = &mf.PlayDate
}
if mf.Starred {
child.Starred = &mf.StarredAt
}
child.UserRating = mf.Rating
format, _ := getTranscoding(ctx)
if mf.Suffix != "" && format != "" && mf.Suffix != format {
child.TranscodedSuffix = format
child.TranscodedContentType = mime.TypeByExtension("." + format)
}
child.BookmarkPosition = mf.BookmarkPosition
return child
}
func fakePath(mf model.MediaFile) string {
filename := mapSlashToDash(mf.Title)
if mf.TrackNumber != 0 {
filename = fmt.Sprintf("%02d - %s", mf.TrackNumber, filename)
}
return fmt.Sprintf("%s/%s/%s.%s", mapSlashToDash(mf.AlbumArtist), mapSlashToDash(mf.Album), filename, mf.Suffix)
}
func mapSlashToDash(target string) string {
return strings.ReplaceAll(target, "/", "_")
}
func childrenFromMediaFiles(ctx context.Context, mfs model.MediaFiles) []responses.Child {
children := make([]responses.Child, len(mfs))
for i, mf := range mfs {
children[i] = childFromMediaFile(ctx, mf)
}
return children
}
2020-08-14 00:37:54 +02:00
2022-12-19 19:59:24 +01:00
func childFromAlbum(_ context.Context, al model.Album) responses.Child {
2020-08-14 00:37:54 +02:00
child := responses.Child{}
child.Id = al.ID
child.IsDir = true
child.Title = al.Name
2020-08-14 00:37:54 +02:00
child.Name = al.Name
child.Album = al.Name
2020-08-14 00:37:54 +02:00
child.Artist = al.AlbumArtist
child.Year = al.MaxYear
child.Genre = al.Genre
2022-12-19 19:59:24 +01:00
child.CoverArt = al.CoverArtID().String()
2020-08-14 00:37:54 +02:00
child.Created = &al.CreatedAt
child.Parent = al.AlbumArtistID
2020-08-14 00:37:54 +02:00
child.ArtistId = al.AlbumArtistID
child.Duration = int(al.Duration)
child.SongCount = al.SongCount
if al.Starred {
child.Starred = &al.StarredAt
}
child.PlayCount = al.PlayCount
if al.PlayCount > 0 {
child.Played = &al.PlayDate
}
2020-08-14 00:37:54 +02:00
child.UserRating = al.Rating
return child
}
func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child {
2020-08-14 00:37:54 +02:00
children := make([]responses.Child, len(als))
for i, al := range als {
children[i] = childFromAlbum(ctx, al)
2020-08-14 00:37:54 +02:00
}
return children
}