package subsonic import ( "context" "errors" "fmt" "mime" "net/http" "sort" "strconv" "strings" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/server/public" "github.com/navidrome/navidrome/server/subsonic/responses" ) func newResponse() *responses.Subsonic { return &responses.Subsonic{ Status: "ok", Version: Version, Type: consts.AppName, ServerVersion: consts.Version, OpenSubsonic: true, } } type subError struct { code int messages []interface{} } func newError(code int, message ...interface{}) error { return subError{ code: code, messages: message, } } // errSubsonic and Unwrap are used to allow `errors.Is(err, errSubsonic)` to work var errSubsonic = errors.New("subsonic API error") func (e subError) Unwrap() error { return fmt.Errorf("%w: %d", errSubsonic, e.code) } 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 } func getUser(ctx context.Context) model.User { user, ok := request.UserFrom(ctx) if ok { return user } 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: int32(a.AlbumCount), UserRating: int32(a.Rating), CoverArt: a.CoverArtID().String(), ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600), } 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: int32(a.AlbumCount), CoverArt: a.CoverArtID().String(), ArtistImageUrl: public.ImageURL(r, a.CoverArtID(), 600), UserRating: int32(a.Rating), MusicBrainzId: a.MbzArtistID, SortName: a.SortArtistName, } if a.Starred { artist.Starred = a.StarredAt } return artist } func toGenres(genres model.Genres) *responses.Genres { response := make([]responses.Genre, len(genres)) for i, g := range genres { response[i] = responses.Genre{ Name: g.Name, SongCount: int32(g.SongCount), AlbumCount: int32(g.AlbumCount), } } return &responses.Genres{Genre: response} } func getTranscoding(ctx context.Context) (format string, bitRate int) { if trc, ok := request.TranscodingFrom(ctx); ok { format = trc.TargetFormat } 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 = int32(mf.Year) child.Artist = mf.Artist child.Genre = mf.Genre child.Genres = buildItemGenres(mf.Genres) child.Track = int32(mf.TrackNumber) child.Duration = int32(mf.Duration) child.Size = mf.Size child.Suffix = mf.Suffix child.BitRate = int32(mf.BitRate) 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 = int32(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 = int32(mf.Rating) format, _ := getTranscoding(ctx) if mf.Suffix != "" && format != "" && mf.Suffix != format { child.TranscodedSuffix = format child.TranscodedContentType = mime.TypeByExtension("." + format) } child.BookmarkPosition = mf.BookmarkPosition child.Comment = mf.Comment child.SortName = mf.SortTitle child.Bpm = int32(mf.Bpm) child.MediaType = responses.MediaTypeSong child.MusicBrainzId = mf.MbzRecordingID child.ReplayGain = responses.ReplayGain{ TrackGain: mf.RgTrackGain, AlbumGain: mf.RgAlbumGain, TrackPeak: mf.RgTrackPeak, AlbumPeak: mf.RgAlbumPeak, } 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 } func childFromAlbum(_ context.Context, al model.Album) responses.Child { child := responses.Child{} child.Id = al.ID child.IsDir = true child.Title = al.Name child.Name = al.Name child.Album = al.Name child.Artist = al.AlbumArtist child.Year = int32(al.MaxYear) child.Genre = al.Genre child.Genres = buildItemGenres(al.Genres) child.CoverArt = al.CoverArtID().String() child.Created = &al.CreatedAt child.Parent = al.AlbumArtistID child.ArtistId = al.AlbumArtistID child.Duration = int32(al.Duration) child.SongCount = int32(al.SongCount) if al.Starred { child.Starred = al.StarredAt } child.PlayCount = al.PlayCount if al.PlayCount > 0 { child.Played = al.PlayDate } child.UserRating = int32(al.Rating) child.SortName = al.SortAlbumName child.MediaType = responses.MediaTypeAlbum child.MusicBrainzId = al.MbzAlbumID return child } func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child { children := make([]responses.Child, len(als)) for i, al := range als { children[i] = childFromAlbum(ctx, al) } return children } // toItemDate converts a string date in the formats 'YYYY-MM-DD', 'YYYY-MM' or 'YYYY' to an OS ItemDate func toItemDate(date string) responses.ItemDate { itemDate := responses.ItemDate{} if date == "" { return itemDate } parts := strings.Split(date, "-") if len(parts) > 2 { itemDate.Day, _ = strconv.Atoi(parts[2]) } if len(parts) > 1 { itemDate.Month, _ = strconv.Atoi(parts[1]) } itemDate.Year, _ = strconv.Atoi(parts[0]) return itemDate } func buildItemGenres(genres model.Genres) []responses.ItemGenre { itemGenres := make([]responses.ItemGenre, len(genres)) for i, g := range genres { itemGenres[i] = responses.ItemGenre{Name: g.Name} } return itemGenres } func buildDiscSubtitles(_ context.Context, a model.Album) responses.DiscTitles { if len(a.Discs) == 0 { return nil } discTitles := responses.DiscTitles{} for num, title := range a.Discs { discTitles = append(discTitles, responses.DiscTitle{Disc: num, Title: title}) } sort.Slice(discTitles, func(i, j int) bool { return discTitles[i].Disc < discTitles[j].Disc }) return discTitles } func buildAlbumsID3(ctx context.Context, albums model.Albums) []responses.AlbumID3 { res := make([]responses.AlbumID3, len(albums)) for i, album := range albums { res[i] = buildAlbumID3(ctx, album) } return res } func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 { dir := responses.AlbumID3{} dir.Id = album.ID dir.Name = album.Name dir.Artist = album.AlbumArtist dir.ArtistId = album.AlbumArtistID dir.CoverArt = album.CoverArtID().String() dir.SongCount = int32(album.SongCount) dir.Duration = int32(album.Duration) dir.PlayCount = album.PlayCount if album.PlayCount > 0 { dir.Played = album.PlayDate } dir.Year = int32(album.MaxYear) dir.Genre = album.Genre dir.Genres = buildItemGenres(album.Genres) dir.DiscTitles = buildDiscSubtitles(ctx, album) dir.UserRating = int32(album.Rating) if !album.CreatedAt.IsZero() { dir.Created = &album.CreatedAt } if album.Starred { dir.Starred = album.StarredAt } dir.MusicBrainzId = album.MbzAlbumID dir.IsCompilation = album.Compilation dir.SortName = album.SortAlbumName dir.OriginalReleaseDate = toItemDate(album.OriginalDate) return dir } func buildStructuredLyric(mf *model.MediaFile, lyrics model.Lyrics) responses.StructuredLyric { lines := make([]responses.Line, len(lyrics.Line)) for i, line := range lyrics.Line { lines[i] = responses.Line{ Start: line.Start, Value: line.Value, } } structured := responses.StructuredLyric{ DisplayArtist: lyrics.DisplayArtist, DisplayTitle: lyrics.DisplayTitle, Lang: lyrics.Lang, Line: lines, Offset: lyrics.Offset, Synced: lyrics.Synced, } if structured.DisplayArtist == "" { structured.DisplayArtist = mf.Artist } if structured.DisplayTitle == "" { structured.DisplayTitle = mf.Title } return structured } func buildLyricsList(mf *model.MediaFile, lyricsList model.LyricList) *responses.LyricsList { lyricList := make(responses.StructuredLyrics, len(lyricsList)) for i, lyrics := range lyricsList { lyricList[i] = buildStructuredLyric(mf, lyrics) } res := &responses.LyricsList{ StructuredLyrics: lyricList, } return res }