mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-29 06:37:17 +01:00
feat(jellyfin): add Jellyfin DTOs and model mappers
Adds BaseItemDto, QueryResult, UserItemDataDto, UserDto, AuthenticationResult, MediaSourceInfo, PlaybackInfoResponse, NameGuidPair and SessionInfo DTOs to server/jellyfin/dto, plus model-to-DTO mappers for songs, albums, artists and genres that later browse/stream/write endpoints will consume.
This commit is contained in:
parent
8a4b7d1221
commit
2e0e3264c7
4 changed files with 275 additions and 0 deletions
|
|
@ -19,3 +19,99 @@ type SystemInfo struct {
|
|||
SupportsLibraryMonitor bool `json:"SupportsLibraryMonitor"`
|
||||
CachePath string `json:"CachePath,omitempty"`
|
||||
}
|
||||
|
||||
type NameGuidPair struct {
|
||||
Name string `json:"Name"`
|
||||
Id string `json:"Id"`
|
||||
}
|
||||
|
||||
type UserItemDataDto struct {
|
||||
Rating *float64 `json:"Rating,omitempty"`
|
||||
PlaybackPositionTicks int64 `json:"PlaybackPositionTicks"`
|
||||
PlayCount int `json:"PlayCount"`
|
||||
IsFavorite bool `json:"IsFavorite"`
|
||||
Played bool `json:"Played"`
|
||||
Key string `json:"Key"`
|
||||
ItemId string `json:"ItemId,omitempty"`
|
||||
LastPlayedDate *string `json:"LastPlayedDate,omitempty"`
|
||||
}
|
||||
|
||||
type BaseItemDto struct {
|
||||
Name string `json:"Name"`
|
||||
ServerId string `json:"ServerId,omitempty"`
|
||||
Id string `json:"Id"`
|
||||
Type string `json:"Type"`
|
||||
IsFolder bool `json:"IsFolder"`
|
||||
MediaType string `json:"MediaType,omitempty"`
|
||||
CollectionType string `json:"CollectionType,omitempty"`
|
||||
ParentId string `json:"ParentId,omitempty"`
|
||||
RunTimeTicks int64 `json:"RunTimeTicks,omitempty"`
|
||||
IndexNumber *int `json:"IndexNumber,omitempty"`
|
||||
ParentIndexNumber *int `json:"ParentIndexNumber,omitempty"`
|
||||
ProductionYear *int `json:"ProductionYear,omitempty"`
|
||||
Album string `json:"Album,omitempty"`
|
||||
AlbumId string `json:"AlbumId,omitempty"`
|
||||
AlbumArtist string `json:"AlbumArtist,omitempty"`
|
||||
AlbumArtists []NameGuidPair `json:"AlbumArtists,omitempty"`
|
||||
AlbumPrimaryImageTag string `json:"AlbumPrimaryImageTag,omitempty"`
|
||||
Artists []string `json:"Artists,omitempty"`
|
||||
ArtistItems []NameGuidPair `json:"ArtistItems,omitempty"`
|
||||
Genres []string `json:"Genres,omitempty"`
|
||||
ChildCount *int `json:"ChildCount,omitempty"`
|
||||
SongCount *int `json:"SongCount,omitempty"`
|
||||
AlbumCount *int `json:"AlbumCount,omitempty"`
|
||||
ImageTags map[string]string `json:"ImageTags,omitempty"`
|
||||
BackdropImageTags []string `json:"BackdropImageTags"`
|
||||
UserData *UserItemDataDto `json:"UserData,omitempty"`
|
||||
MediaSources []MediaSourceInfo `json:"MediaSources,omitempty"`
|
||||
Container string `json:"Container,omitempty"`
|
||||
CanDownload bool `json:"CanDownload"`
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
Items []BaseItemDto `json:"Items"`
|
||||
TotalRecordCount int `json:"TotalRecordCount"`
|
||||
StartIndex int `json:"StartIndex"`
|
||||
}
|
||||
|
||||
type UserDto struct {
|
||||
Name string `json:"Name"`
|
||||
ServerId string `json:"ServerId,omitempty"`
|
||||
Id string `json:"Id"`
|
||||
HasPassword bool `json:"HasPassword"`
|
||||
HasConfiguredPassword bool `json:"HasConfiguredPassword"`
|
||||
HasConfiguredEasyPassword bool `json:"HasConfiguredEasyPassword"`
|
||||
PrimaryImageTag string `json:"PrimaryImageTag,omitempty"`
|
||||
}
|
||||
|
||||
type SessionInfo struct {
|
||||
Id string `json:"Id"`
|
||||
UserId string `json:"UserId"`
|
||||
}
|
||||
|
||||
type AuthenticationResult struct {
|
||||
User *UserDto `json:"User"`
|
||||
SessionInfo *SessionInfo `json:"SessionInfo,omitempty"`
|
||||
AccessToken string `json:"AccessToken"`
|
||||
ServerId string `json:"ServerId"`
|
||||
}
|
||||
|
||||
type MediaSourceInfo struct {
|
||||
Id string `json:"Id"`
|
||||
Path string `json:"Path,omitempty"`
|
||||
Protocol string `json:"Protocol"`
|
||||
Container string `json:"Container,omitempty"`
|
||||
Size int64 `json:"Size,omitempty"`
|
||||
Name string `json:"Name,omitempty"`
|
||||
IsRemote bool `json:"IsRemote"`
|
||||
RunTimeTicks int64 `json:"RunTimeTicks,omitempty"`
|
||||
SupportsTranscoding bool `json:"SupportsTranscoding"`
|
||||
SupportsDirectStream bool `json:"SupportsDirectStream"`
|
||||
SupportsDirectPlay bool `json:"SupportsDirectPlay"`
|
||||
Type string `json:"Type"`
|
||||
}
|
||||
|
||||
type PlaybackInfoResponse struct {
|
||||
MediaSources []MediaSourceInfo `json:"MediaSources"`
|
||||
PlaySessionId string `json:"PlaySessionId"`
|
||||
}
|
||||
|
|
|
|||
17
server/jellyfin/dto/dto_suite_test.go
Normal file
17
server/jellyfin/dto/dto_suite_test.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestDto(t *testing.T) {
|
||||
tests.Init(t, false)
|
||||
log.SetLevel(log.LevelFatal)
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Jellyfin DTO Suite")
|
||||
}
|
||||
114
server/jellyfin/dto/mappers.go
Normal file
114
server/jellyfin/dto/mappers.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package dto
|
||||
|
||||
import "github.com/navidrome/navidrome/model"
|
||||
|
||||
func TicksFromSeconds(sec float32) int64 { return int64(float64(sec) * 1e7) }
|
||||
|
||||
func intPtr(v int) *int { return &v }
|
||||
|
||||
func UserData(a model.Annotations, itemID string) *UserItemDataDto {
|
||||
d := &UserItemDataDto{
|
||||
PlayCount: int(a.PlayCount),
|
||||
IsFavorite: a.Starred,
|
||||
Played: a.PlayCount > 0,
|
||||
Key: itemID,
|
||||
ItemId: itemID,
|
||||
}
|
||||
if a.Rating > 0 {
|
||||
r := float64(a.Rating) * 2 // Navidrome 0-5 -> Jellyfin 0-10
|
||||
d.Rating = &r
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func SongToBaseItem(mf model.MediaFile) BaseItemDto {
|
||||
item := BaseItemDto{
|
||||
Name: mf.Title,
|
||||
Id: mf.ID,
|
||||
Type: "Audio",
|
||||
MediaType: "Audio",
|
||||
IsFolder: false,
|
||||
ParentId: mf.AlbumID,
|
||||
Album: mf.Album,
|
||||
AlbumId: mf.AlbumID,
|
||||
AlbumArtist: mf.AlbumArtist,
|
||||
Artists: []string{mf.Artist},
|
||||
RunTimeTicks: TicksFromSeconds(mf.Duration),
|
||||
IndexNumber: intPtr(mf.TrackNumber),
|
||||
ParentIndexNumber: intPtr(mf.DiscNumber),
|
||||
Container: mf.Suffix,
|
||||
CanDownload: true,
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(mf.Annotations, mf.ID),
|
||||
}
|
||||
if mf.Year > 0 {
|
||||
item.ProductionYear = intPtr(mf.Year)
|
||||
}
|
||||
if len(mf.Genres) > 0 {
|
||||
for _, g := range mf.Genres {
|
||||
item.Genres = append(item.Genres, g.Name)
|
||||
}
|
||||
} else if mf.Genre != "" {
|
||||
item.Genres = []string{mf.Genre}
|
||||
}
|
||||
// Finamp resolves song art via AlbumId + a non-empty AlbumPrimaryImageTag.
|
||||
if mf.AlbumID != "" {
|
||||
item.AlbumPrimaryImageTag = mf.AlbumID
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func AlbumToBaseItem(al model.Album) BaseItemDto {
|
||||
item := BaseItemDto{
|
||||
Name: al.Name,
|
||||
Id: al.ID,
|
||||
Type: "MusicAlbum",
|
||||
IsFolder: true,
|
||||
ParentId: al.AlbumArtistID,
|
||||
AlbumArtist: al.AlbumArtist,
|
||||
Album: al.Name,
|
||||
ChildCount: intPtr(al.SongCount),
|
||||
SongCount: intPtr(al.SongCount),
|
||||
RunTimeTicks: TicksFromSeconds(al.Duration),
|
||||
ImageTags: map[string]string{"Primary": al.ID},
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(al.Annotations, al.ID),
|
||||
}
|
||||
if al.AlbumArtistID != "" {
|
||||
item.AlbumArtists = []NameGuidPair{{Name: al.AlbumArtist, Id: al.AlbumArtistID}}
|
||||
item.ArtistItems = item.AlbumArtists
|
||||
}
|
||||
if al.MaxYear > 0 {
|
||||
item.ProductionYear = intPtr(al.MaxYear)
|
||||
}
|
||||
if len(al.Genres) > 0 {
|
||||
for _, g := range al.Genres {
|
||||
item.Genres = append(item.Genres, g.Name)
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func ArtistToBaseItem(ar model.Artist) BaseItemDto {
|
||||
return BaseItemDto{
|
||||
Name: ar.Name,
|
||||
Id: ar.ID,
|
||||
Type: "MusicArtist",
|
||||
IsFolder: true,
|
||||
AlbumCount: intPtr(ar.AlbumCount),
|
||||
SongCount: intPtr(ar.SongCount),
|
||||
ImageTags: map[string]string{"Primary": ar.ID},
|
||||
BackdropImageTags: []string{},
|
||||
UserData: UserData(ar.Annotations, ar.ID),
|
||||
}
|
||||
}
|
||||
|
||||
func GenreToBaseItem(g model.Genre) BaseItemDto {
|
||||
return BaseItemDto{
|
||||
Name: g.Name,
|
||||
Id: g.ID,
|
||||
Type: "MusicGenre",
|
||||
IsFolder: true,
|
||||
BackdropImageTags: []string{},
|
||||
}
|
||||
}
|
||||
48
server/jellyfin/dto/mappers_test.go
Normal file
48
server/jellyfin/dto/mappers_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/model"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("mappers", func() {
|
||||
It("maps a song to an Audio BaseItemDto", func() {
|
||||
mf := model.MediaFile{
|
||||
ID: "song-1", Title: "Song", Album: "Alb", AlbumID: "alb-1",
|
||||
Artist: "Art", AlbumArtist: "AA", TrackNumber: 3, DiscNumber: 1,
|
||||
Year: 1999, Duration: 60,
|
||||
}
|
||||
mf.PlayCount = 2
|
||||
mf.Starred = true
|
||||
item := SongToBaseItem(mf)
|
||||
Expect(item.Type).To(Equal("Audio"))
|
||||
Expect(item.IsFolder).To(BeFalse())
|
||||
Expect(item.Id).To(Equal("song-1"))
|
||||
Expect(item.AlbumId).To(Equal("alb-1"))
|
||||
Expect(item.ParentId).To(Equal("alb-1"))
|
||||
Expect(item.RunTimeTicks).To(Equal(int64(600_000_000)))
|
||||
Expect(*item.IndexNumber).To(Equal(3))
|
||||
Expect(item.UserData.IsFavorite).To(BeTrue())
|
||||
Expect(item.UserData.PlayCount).To(Equal(2))
|
||||
Expect(item.UserData.Played).To(BeTrue())
|
||||
})
|
||||
|
||||
It("maps an album to a MusicAlbum folder item", func() {
|
||||
al := model.Album{ID: "alb-1", Name: "Alb", AlbumArtist: "AA", AlbumArtistID: "art-1", MaxYear: 1999, SongCount: 10}
|
||||
item := AlbumToBaseItem(al)
|
||||
Expect(item.Type).To(Equal("MusicAlbum"))
|
||||
Expect(item.IsFolder).To(BeTrue())
|
||||
Expect(item.ParentId).To(Equal("art-1"))
|
||||
Expect(*item.ProductionYear).To(Equal(1999))
|
||||
Expect(*item.ChildCount).To(Equal(10))
|
||||
})
|
||||
|
||||
It("maps an artist to a MusicArtist folder item", func() {
|
||||
ar := model.Artist{ID: "art-1", Name: "AA", AlbumCount: 2, SongCount: 20}
|
||||
item := ArtistToBaseItem(ar)
|
||||
Expect(item.Type).To(Equal("MusicArtist"))
|
||||
Expect(item.IsFolder).To(BeTrue())
|
||||
Expect(*item.AlbumCount).To(Equal(2))
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue