mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-28 22:27:10 +01:00
feat(jellyfin): emit DateCreated (Date Added) on items
BaseItemDto had no DateCreated, so clients showed 'No Date Added' and had nothing to sort 'Recently Added' by. Emit it as ISO 8601 from each entity's CreatedAt (the same field the recently_added sort uses) for songs, albums and artists.
This commit is contained in:
parent
4341b025ff
commit
81e6dc88b0
4 changed files with 43 additions and 10 deletions
|
|
@ -43,16 +43,19 @@ type BaseItemDto struct {
|
|||
// PlaylistItemId identifies this entry within a playlist listing (GET /Playlists/{id}/Items).
|
||||
// It's only set there, and is distinct from Id so a song appearing more than once in the same
|
||||
// playlist can be removed by occurrence (DELETE .../Items?EntryIds=...) rather than by song id.
|
||||
PlaylistItemId string `json:"PlaylistItemId,omitempty"`
|
||||
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"`
|
||||
PlaylistItemId string `json:"PlaylistItemId,omitempty"`
|
||||
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"`
|
||||
// DateCreated is the ISO 8601 date the item was added to the library; clients show it as
|
||||
// "Date Added" and sort "Recently Added" by it.
|
||||
DateCreated string `json:"DateCreated,omitempty"`
|
||||
Album string `json:"Album,omitempty"`
|
||||
AlbumId string `json:"AlbumId,omitempty"`
|
||||
AlbumArtist string `json:"AlbumArtist,omitempty"`
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@ import (
|
|||
|
||||
func TicksFromSeconds(sec float32) int64 { return int64(float64(sec) * 1e7) }
|
||||
|
||||
// jellyfinDate formats a time as the ISO 8601 string Jellyfin clients expect for DateCreated, or
|
||||
// "" for the zero time so the field is omitted rather than sent as a meaningless epoch.
|
||||
func jellyfinDate(t *time.Time) string {
|
||||
if t == nil || t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// channelLayout maps a channel count to the label Jellyfin clients expect on a MediaStream.
|
||||
func channelLayout(n int) string {
|
||||
switch n {
|
||||
|
|
@ -92,6 +101,7 @@ func SongToBaseItem(mf model.MediaFile) BaseItemDto {
|
|||
AlbumArtist: mf.AlbumArtist,
|
||||
Artists: []string{mf.Artist},
|
||||
RunTimeTicks: TicksFromSeconds(mf.Duration),
|
||||
DateCreated: jellyfinDate(&mf.CreatedAt),
|
||||
Container: mf.Suffix,
|
||||
CanDownload: true,
|
||||
BackdropImageTags: []string{},
|
||||
|
|
@ -134,6 +144,7 @@ func AlbumToBaseItem(al model.Album) BaseItemDto {
|
|||
ChildCount: new(al.SongCount),
|
||||
SongCount: new(al.SongCount),
|
||||
RunTimeTicks: TicksFromSeconds(al.Duration),
|
||||
DateCreated: jellyfinDate(&al.CreatedAt),
|
||||
ImageTags: map[string]string{"Primary": al.ID},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {al.ID: blurHash(al.ID)}},
|
||||
BackdropImageTags: []string{},
|
||||
|
|
@ -162,6 +173,7 @@ func ArtistToBaseItem(ar model.Artist) BaseItemDto {
|
|||
IsFolder: true,
|
||||
AlbumCount: new(ar.AlbumCount),
|
||||
SongCount: new(ar.SongCount),
|
||||
DateCreated: jellyfinDate(ar.CreatedAt),
|
||||
ImageTags: map[string]string{"Primary": ar.ID},
|
||||
ImageBlurHashes: map[string]map[string]string{"Primary": {ar.ID: blurHash(ar.ID)}},
|
||||
BackdropImageTags: []string{},
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ var _ = Describe("mappers", func() {
|
|||
Expect(item.ImageBlurHashes).To(BeNil())
|
||||
})
|
||||
|
||||
It("sets DateCreated from the media file's CreatedAt", func() {
|
||||
mf := model.MediaFile{ID: "s1", Title: "Song", CreatedAt: time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)}
|
||||
Expect(SongToBaseItem(mf).DateCreated).To(Equal("2024-01-15T10:30:00Z"))
|
||||
})
|
||||
|
||||
It("omits DateCreated when CreatedAt is the zero time", func() {
|
||||
Expect(SongToBaseItem(model.MediaFile{ID: "s1", Title: "Song"}).DateCreated).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("builds a MediaSourceInfo from a media file", func() {
|
||||
mf := model.MediaFile{ID: "s1", Size: 5242880, Suffix: "mp3", BitRate: 320, Duration: 100}
|
||||
src := MediaSourceFromMediaFile(mf)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package e2e
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/server/jellyfin/dto"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
|
|
@ -165,6 +166,14 @@ var _ = Describe("Browsing", func() {
|
|||
Expect(item.Type).To(Equal("Audio"))
|
||||
})
|
||||
|
||||
It("includes a parseable DateCreated (Date Added) on a song", func() {
|
||||
var item dto.BaseItemDto
|
||||
parseInto(get("/Items/"+enc(songID("So What"))), &item)
|
||||
Expect(item.DateCreated).ToNot(BeEmpty())
|
||||
_, err := time.Parse(time.RFC3339, item.DateCreated)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("resolves an artist", func() {
|
||||
var item dto.BaseItemDto
|
||||
parseInto(get("/Items/"+enc(artistID("Miles Davis"))), &item)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue