package jellyfin import "database/sql" // This file defines a documented SUBSET of the Jellyfin DTOs. Jellyfin's // BaseItemDto has roughly a hundred fields; real clients tolerate the absence // of most of them, and the ones below are what browse and playback actually // consume. Every struct is PascalCase on the wire because that is the Jellyfin // convention (the web app's own API is snake_case and is untouched). // UserDto is the subset of the Jellyfin user shape clients read. type UserDto struct { Name string `json:"Name"` ServerId string `json:"ServerId,omitempty"` Id string `json:"Id"` HasPassword bool `json:"HasPassword"` HasConfiguredPassword bool `json:"HasConfiguredPassword"` Policy *UserPolicy `json:"Policy,omitempty"` Configuration *UserConfig `json:"Configuration,omitempty"` } // UserPolicy carries the two things a client needs from a user's policy: the // admin flag and which library folders the account can see. EnabledFolders is // the user's VisibleLibraryIDs rendered as Jellyfin ids, so a client can hide // views it is not allowed to open. type UserPolicy struct { IsAdministrator bool `json:"IsAdministrator"` IsDisabled bool `json:"IsDisabled"` EnableAllFolders bool `json:"EnableAllFolders"` EnabledFolders []string `json:"EnabledFolders"` EnableMediaPlayback bool `json:"EnableMediaPlayback"` EnableContentDownloading bool `json:"EnableContentDownloading"` } // UserConfig is a near-empty subset; clients only require the key to exist. type UserConfig struct { EnableNextEpisodeAutoPlay bool `json:"EnableNextEpisodeAutoPlay"` } // UserItemDataDto is per-user state for one item. // // Ticks are 100ns units. Vellum stores milliseconds, so the conversion is // x10000 (ms -> ticks) and /10000 (ticks -> ms). type UserItemDataDto struct { PlaybackPositionTicks int64 `json:"PlaybackPositionTicks"` PlayedPercentage float64 `json:"PlayedPercentage,omitempty"` PlayCount int `json:"PlayCount"` IsFavorite bool `json:"IsFavorite"` Played bool `json:"Played"` } // BaseItemDto is the browse shape. Type is the Jellyfin item type // ("Movie"/"Audio"/"Book"/"Photo"/"CollectionFolder"/"Folder"), mapped from // Vellum's media kind. type BaseItemDto struct { Id string `json:"Id"` Name string `json:"Name"` Type string `json:"Type"` MediaType string `json:"MediaType,omitempty"` CollectionType string `json:"CollectionType,omitempty"` Container string `json:"Container,omitempty"` RunTimeTicks int64 `json:"RunTimeTicks,omitempty"` ProductionYear int `json:"ProductionYear,omitempty"` ParentId string `json:"ParentId,omitempty"` IsFolder bool `json:"IsFolder"` SortName string `json:"SortName,omitempty"` Path string `json:"Path,omitempty"` ImageTags map[string]string `json:"ImageTags,omitempty"` UserData *UserItemDataDto `json:"UserData,omitempty"` MediaSources []MediaSourceInfo `json:"MediaSources,omitempty"` } // MediaSourceInfo is one playable source. Vellum never transcodes, so // SupportsTranscoding is always false and TranscodingUrl is never set. type MediaSourceInfo struct { Id string `json:"Id"` Name string `json:"Name"` Path string `json:"Path,omitempty"` Container string `json:"Container,omitempty"` RunTimeTicks int64 `json:"RunTimeTicks,omitempty"` Size int64 `json:"Size,omitempty"` SupportsDirectPlay bool `json:"SupportsDirectPlay"` SupportsDirectStream bool `json:"SupportsDirectStream"` SupportsTranscoding bool `json:"SupportsTranscoding"` DirectStreamUrl string `json:"DirectStreamUrl,omitempty"` Protocol string `json:"Protocol"` Type string `json:"Type"` IsRemote bool `json:"IsRemote"` MediaStreams []MediaStream `json:"MediaStreams,omitempty"` } // MediaStream describes one track inside a file, using the codec facts the // scanner wrote from ffprobe. type MediaStream struct { Type string `json:"Type"` // "Video" | "Audio" Codec string `json:"Codec,omitempty"` Index int `json:"Index"` IsDefault bool `json:"IsDefault"` Width int `json:"Width,omitempty"` Height int `json:"Height,omitempty"` } // PlaybackInfoResponse is the response to PlaybackInfo. ErrorCode is set only // when the client demanded a transcode Vellum will not perform. type PlaybackInfoResponse struct { MediaSources []MediaSourceInfo `json:"MediaSources"` PlaySessionId string `json:"PlaySessionId,omitempty"` ErrorCode string `json:"ErrorCode,omitempty"` } // itemQueryResult is the browse envelope. type itemQueryResult struct { Items []BaseItemDto `json:"Items"` TotalRecordCount int `json:"TotalRecordCount"` StartIndex int `json:"StartIndex"` } // dbItem is one media_items row plus the file facts and per-user state a DTO // needs. It is the single shape both the list and single-item paths build. type dbItem struct { ID int64 LibraryID int64 Kind string Title string SortTitle string Year int ParentID sql.NullInt64 Container string DurationMS int64 VCodec string ACodec string Width int Height int Size int64 Path string HasCover bool // Per-user state, zero when the user has never touched the item. PositionMS int64 Percent float64 Finished bool HasProgress bool PlayCount int Favorite bool } // itemType maps a Vellum media kind to the Jellyfin item type. The four types // the task requires are Movie, Audio, Book and Photo; anything Vellum adds // later (games) degrades to Folder so a client can still show it. func itemType(kind string) string { switch kind { case "video": return "Movie" case "audiobooks", "music": return "Audio" case "photos": return "Photo" case "ebooks", "comics": return "Book" default: return "Folder" } } // mediaType maps a Vellum media kind to the Jellyfin MediaType. It is the // coarser classification ("Video"/"Audio"/"Book"/"Photo"). func mediaType(kind string) string { switch kind { case "video": return "Video" case "audiobooks", "music": return "Audio" case "photos": return "Photo" case "ebooks", "comics": return "Book" default: return "" } } // kindsForTypes maps requested Jellyfin IncludeItemTypes back to Vellum kinds, // so a client asking for Movies does not receive ebooks. An unknown type maps // to nothing rather than to everything. func kindsForTypes(types []string) []string { var out []string for _, t := range types { switch t { case "Movie": out = append(out, "video") case "Audio": out = append(out, "audiobooks", "music") case "Book": out = append(out, "ebooks", "comics") case "Photo": out = append(out, "photos") } } return out } // toBaseItemDto renders one dbItem. includeMediaSources is set from the Fields // query parameter; lists normally omit sources to keep responses small. func toBaseItemDto(it dbItem, includeMediaSources bool, apiKey string) BaseItemDto { dto := BaseItemDto{ Id: itemID(it.ID), Name: it.Title, Type: itemType(it.Kind), MediaType: mediaType(it.Kind), Container: it.Container, RunTimeTicks: msToTicks(it.DurationMS), ProductionYear: it.Year, IsFolder: false, SortName: it.SortTitle, Path: it.Path, } if it.ParentID.Valid { dto.ParentId = itemID(it.ParentID.Int64) } if it.HasCover { // The tag is the item id: stable, and it changes when the item changes. dto.ImageTags = map[string]string{"Primary": itemID(it.ID)} } dto.UserData = &UserItemDataDto{ PlaybackPositionTicks: msToTicks(it.PositionMS), PlayedPercentage: it.Percent * 100, PlayCount: it.PlayCount, IsFavorite: it.Favorite, Played: it.Finished, } if includeMediaSources { dto.MediaSources = []MediaSourceInfo{buildMediaSource(it, apiKey)} } return dto } // buildMediaSource renders the single direct-play source for an item. It is // shared by PlaybackInfo and by list responses that requested the // MediaSources field. func buildMediaSource(it dbItem, apiKey string) MediaSourceInfo { ms := MediaSourceInfo{ Id: itemID(it.ID), Name: it.Title, Path: it.Path, Container: it.Container, RunTimeTicks: msToTicks(it.DurationMS), Size: it.Size, SupportsDirectPlay: true, SupportsDirectStream: true, // The hard constraint: no transcoding, ever. SupportsTranscoding: false, Protocol: "File", Type: "Default", IsRemote: false, } ms.DirectStreamUrl = directStreamURL(it, apiKey) if it.VCodec != "" { ms.MediaStreams = append(ms.MediaStreams, MediaStream{ Type: "Video", Codec: it.VCodec, Index: 0, IsDefault: true, Width: it.Width, Height: it.Height, }) } if it.ACodec != "" { ms.MediaStreams = append(ms.MediaStreams, MediaStream{ Type: "Audio", Codec: it.ACodec, Index: 1, IsDefault: true, }) } return ms } // directStreamURL builds the URL a client should play. Video items stream from // /Videos, everything else (audio, books, photos) from /Audio, which is the // same file-serving path either way. The api_key is embedded because media // elements cannot set headers. func directStreamURL(it dbItem, apiKey string) string { base := "/Audio/" + itemID(it.ID) + "/stream" if it.Kind == "video" { base = "/Videos/" + itemID(it.ID) + "/stream" } if apiKey == "" { return base } return base + "?api_key=" + apiKey } // msToTicks converts Vellum's milliseconds to Jellyfin's 100ns ticks. func msToTicks(ms int64) int64 { return ms * 10000 } // ticksToMS converts Jellyfin's 100ns ticks to Vellum's milliseconds. func ticksToMS(ticks int64) int64 { return ticks / 10000 }