From 967411d1afe5266e563c4aa62428704e85c699d7 Mon Sep 17 00:00:00 2001 From: Pete Date: Sun, 1 Feb 2026 21:00:14 -0600 Subject: [PATCH] Delete client.js.js --- client.js.js | 934 --------------------------------------------------- 1 file changed, 934 deletions(-) delete mode 100644 client.js.js diff --git a/client.js.js b/client.js.js deleted file mode 100644 index 2091ee4..0000000 --- a/client.js.js +++ /dev/null @@ -1,934 +0,0 @@ -console.log("Jellyfin plugin loading - version 30"); -let config = {}; -const PLATFORM = "Jellyfin"; - -// Ensure all functions exist before assignment -try { - // Check if required objects exist - if (typeof source === 'undefined') { - console.error('Source object not available'); - // Can't return here, just log the error - } else { - source.enable = enable; - source.disable = disable; - source.searchSuggestions = searchSuggestions; - source.getHome = getHome; - source.isContentDetailsUrl = isContentDetailsUrl; - source.getContentDetails = getContentDetails; - source.isChannelUrl = isChannelUrl; - source.getChannel = getChannel; - source.getChannelContents = getChannelContents; - source.isPlaylistUrl = isPlaylistUrl; - source.getPlaylist = getPlaylist; - source.searchSuggestions = searchSuggestions; - source.getSearchCapabilities = getSearchCapabilities; - source.search = search; - source.searchChannels = searchChannels; - source.searchPlaylists = searchPlaylists; - source.getComments = getComments; - source.getSubComments = getSubComments; - - console.log("Jellyfin plugin functions assigned successfully"); - } -} catch (e) { - console.error("Error assigning plugin functions:", e); -} - -class JellyfinContentPager extends ContentPager { - constructor({ url, type, limit = 20, errorMessage = "Could not fetch items" }) { - let baseUrl; - - if (url instanceof URL) { - baseUrl = url; - } else { - baseUrl = new URL(url); - } - - baseUrl.searchParams.set('limit', limit); - - // Fix sorting for music albums - if (type == "MusicAlbum") { - baseUrl.searchParams.set('sortBy', 'IndexNumber'); - } - - let body = simpleJsonGet(baseUrl.toString(), errorMessage).body; - let items = body.Items; - let authorCache = {}; - let authors = extractAuthors(items, authorCache); - let entries = formatEntries({ items, authors }); - // let entries = body.Items.map(formatItem); - - const totalItemCount = body.TotalRecordCount; - super(entries, entries.length < totalItemCount); - - this.url = baseUrl; - this.limit = limit; - this.errorMessage = errorMessage; - this.totalItemCount = totalItemCount; - this.currentIndex = 0; - this.authorCache = authorCache; - } - - nextPage() { - this.currentIndex += this.limit; - this.url.searchParams.set('startIndex', this.currentIndex); - - let body = simpleJsonGet(this.url.toString(), this.errorMessage).body; - let items = body.Items; - let authors = extractAuthors(items, this.authorCache); - this.results = formatEntries({ items, authors }); - this.hasMore = this.currentIndex + this.results.length < this.totalItemCount; - - return this; - } -} - -class JellyfinSearchContentPager extends ContentPager { - // TODO: Do something with these filter options - constructor({ url = toUrl('/Search/Hints'), query, type, order, filters, channelId, errorMessage = "Search failed", limit = 20 }) { - let searchUrl = new URL(url); - searchUrl.searchParams.append("SearchTerm", query); - - const body = simpleJsonGet(searchUrl.toString(), errorMessage).body; - const items = body.SearchHints.map((hint) => hint.Id); - - super([], items.length > 0); - this.errorMessage = errorMessage; - this.items = items; - this.limit = limit; - this.authorCache = {}; - } - - nextPage() { - const requestedItems = this.items.slice(0, this.limit); - const url = toUrl(`/Items?ids=${requestedItems.join(",")}`); - let body = simpleJsonGet(url, this.errorMessage).body; - let items = body.Items; - let authors = extractAuthors(items, this.authorCache); - this.results = formatEntries({ items, authors }) - this.items = this.items.slice(this.limit); - this.hasMore = this.items.length > 0; - - return this; - - } -} - -function enable(conf) { - config = conf; - - // Initialize constants if not provided - if (!config.constants) { - config.constants = { - uri: config.uri || "", - token: config.token || "", - client: config.client || "Grayjay", - version: config.version || "1.0.0", - device_id: config.device_id || "grayjay-device", - device_name: config.device_name || "Grayjay Client" - }; - } - - return config; -}; - -function disable() { }; - -function getHome(continuationToken) { - return new JellyfinContentPager({ - url: toUrl("/Shows/NextUp?fields=DateCreated"), - errorMessage: "Could not fetch latest updates", - }); -}; - -function isContentDetailsUrl(url) { - // Basic URL pattern check that works before initialization - try { - const urlObj = new URL(url); - const pathname = urlObj.pathname; - const typeParam = urlObj.searchParams.get("type"); - - // Check if it's a Jellyfin URL with an item ID and type parameter - if (pathname.includes("/Items/") && typeParam) { - return ["Episode", "Video", "Audio", "Movie"].includes(typeParam); - } - - // Fallback for URLs without type parameter - if (pathname.includes("/Items/")) { - return true; // Assume it's content if it has an item ID - } - } catch (e) { - // Invalid URL - return false; - } - - return false; -}; - -function getContentDetails(url) { - const parsed = new URL(url); - const tokens = parsed.pathname.split("/"); - const itemId = tokens[tokens.length - 1]; - - const playbackDetails = { - DeviceProfile: { - DirectPlayProfiles: [ - { Container: "mkv", VideoCodec: "h264", Type: "Video" }, - { Container: "mp4", VideoCodec: "h264", Type: "Video" }, - { Container: "webm", Type: "Audio" }, - { Container: "mp3", Type: "Audio" }, - ], - TranscodingProfiles: [ - { - Container: "mp4", - Type: "Video", - VideoCodec: "h264", - AudioCodec: "aac", - Protocol: "hls", - }, - { Container: "mp3", Type: "Audio", AudioCodec: "aac", Protocol: "hls" }, - ], - }, - }; - - const [details, mediaSources] = batchedJSONRequests([ - { url: toUrl(`/Items/${itemId}?fields=DateCreated`) }, - { - url: toUrl(`/Items/${itemId}/PlaybackInfo`), - body: JSON.stringify(playbackDetails), - }, - ]); - - // Start playback session to get valid PlaySessionId - if (mediaSources.body.PlaySessionId) { - try { - http.POST( - toUrl(`/Sessions/Playing`), - JSON.stringify({ - ItemId: itemId, - MediaSourceId: mediaSources.body.MediaSources[0]?.Id, - PlaySessionId: mediaSources.body.PlaySessionId, - }), - authHeaders() - ); - } catch (e) { - console.log("Could not start playback session: " + e.message); - } - } - - switch (details.body.Type) { - case "Episode": - case "Movie": - return videoContent(details.body, mediaSources.body, itemId); - - case "Audio": - return audioContent(details.body, mediaSources.body, itemId); - } -}; - -function extractSources(details, mediaSource, itemId, playSessionId) { - let sources = []; - let subtitles = []; - - console.log("Extracting sources for itemId:", itemId, "playSessionId:", playSessionId); - - const videoStream = mediaSource.MediaStreams.find(s => s.Type === "Video"); - const audioStream = mediaSource.MediaStreams.find(s => s.Type === "Audio"); - - if (videoStream) { - // Calculate total bitrate (video + audio if available) - const videoBitrate = videoStream.BitRate || mediaSource.Bitrate || 0; - const audioBitrate = audioStream?.BitRate || 0; - const totalBitrate = videoBitrate + audioBitrate; - - // Use HLS streaming endpoint like the Jellyfin web client - // This is the format that works in browsers: /videos/{id}/master.m3u8 - // Note: We need to start playback session first for some Jellyfin servers - const hlsUrl = toUrl( - `/Videos/${itemId}/master.m3u8?MediaSourceId=${mediaSource.Id}&PlaySessionId=${playSessionId}&VideoCodec=h264,hevc,vp9&AudioCodec=aac,mp3,opus&VideoBitrate=${totalBitrate || 10000000}&AudioBitrate=384000&MaxFramerate=60&SegmentContainer=mp4&MinSegments=1&BreakOnNonKeyFrames=True&TranscodingMaxAudioChannels=6&api_key=${config.constants.token}` - ); - - console.log("Jellyfin HLS URL:", hlsUrl); - - sources.push( - new HLSSource({ - name: "HLS", - url: hlsUrl, - duration: toDuration(mediaSource.RunTimeTicks), - }) - ); - - // Also add direct stream as fallback - const directUrl = toUrl( - `/Videos/${itemId}/stream?MediaSourceId=${mediaSource.Id}&PlaySessionId=${playSessionId}&Static=true&api_key=${config.constants.token}` - ); - - if (directUrl) { - console.log("Jellyfin Direct URL:", directUrl); - sources.push( - new VideoUrlSource({ - url: directUrl, - width: videoStream?.Width || 1920, - height: videoStream?.Height || 1080, - container: "mp4", - codec: "h264", - name: "Direct Stream", - bitrate: totalBitrate || 0 - }) - ); - } - } - - for (const mediaStream of mediaSource.MediaStreams) { - if (mediaStream.Type == "Subtitle") { - const url = toUrl( - `/Videos/${details.Id}/${mediaSource.Id}/Subtitles/${mediaStream.Index}/0/Stream.vtt?api_key=${config.constants.token}`, - ); - - subtitles.push({ - name: mediaStream.DisplayTitle, - url: url, - format: "text/vtt", - - getSubtitles() { - const resp = http.GET(url, authHeaders(), false); - - if (!resp.isOk) { - throw new ScriptException("Could not fetch subtitles"); - } - - return resp.body; - }, - }); - } - } - - console.log("Returning", sources.length, "sources and", subtitles.length, "subtitles"); - return { sources, subtitles }; -} - -function audioContent(details, mediaSources, itemId) { - let { sources, _subtitles } = extractSources( - details, - mediaSources.MediaSources[0], - itemId, - mediaSources.PlaySessionId, - ); - - const [author] = extractAuthors([details], {}); - - return new PlatformVideoDetails({ - id: new PlatformID(PLATFORM, details.Id, config.id), - author: author, - name: details.Name, - thumbnails: itemThumbnails(details.AlbumId), - dateTime: - new Date(details.PremiereDate || details.DateCreated).getTime() / 1000, - duration: toDuration(details.RunTimeTicks), - viewCount: null, - isLive: false, - description: null, - video: new VideoSourceDescriptor(sources), - url: toUrl(`/Items/${details.Id}?type=Audio`), - }); -} - -function videoContent(details, mediaSources, itemId) { - let { sources, subtitles } = extractSources( - details, - mediaSources.MediaSources[0], - itemId, - mediaSources.PlaySessionId, - ); - - const [author] = extractAuthors([details], {}); - - return new PlatformVideoDetails({ - id: new PlatformID(PLATFORM, details.Id, config.id), - author: author, - name: details.Name, - thumbnails: itemThumbnails(details.Id), - dateTime: new Date(details.DateCreated).getTime() / 1000, - duration: toDuration(details.RunTimeTicks), - viewCount: null, - isLive: false, - description: null, - subtitles: subtitles, - video: new VideoSourceDescriptor(sources), - url: toUrl(`/Items/${details.Id}?type=Video`), - }); -} - -function isChannelUrl(url) { - // Basic URL pattern check that works before initialization - try { - const urlObj = new URL(url); - const pathname = urlObj.pathname; - const typeParam = urlObj.searchParams.get("type"); - - // Check if it's a Jellyfin URL with an item ID and type parameter - if (pathname.includes("/Items/") && typeParam) { - return ["Series", "MusicArtist", "Person", "Studio"].includes(typeParam); - } - } catch (e) { - // Invalid URL - return false; - } - - return false; -}; - -function getChannel(url) { - const req = simpleJsonGet(url); - const item = req.body; - let parsed = new URL(url); - parsed.searchParams.set("type", item.Type); - - return formatItem(item); -}; - -function getChannelContents(url) { - const itemId = urlId(url); - - return new JellyfinContentPager({ - url: toUrl(`/Items?ParentId=${itemId}`), - errorMessage: "Could not fetch Channel contents", - }); -}; - -function isPlaylistUrl(url) { - return isType(url, ["Playlist", "MusicAlbum", "Season", "Folder"]); -}; - -function getPlaylist(url) { - let externalUrls = new Map(); - let parsed = new URL(url); - - const item = simpleJsonGet(url).body; - const [author] = extractAuthors([item], {}); - parsed.searchParams.set("type", item.Type); - - const contents = new JellyfinContentPager({ - type: item.Type, - url: toUrl(`/Items?ParentId=${item.Id}`), - }); - - return new PlatformPlaylistDetails({ - id: new PlatformID(PLATFORM, item.Id, config.id), - name: item.Name, - thumbnail: thumbnail({ item, query: { fillWidth: 240 } }), - banner: banner({ item }), - subscribers: null, - description: item.Overview, - url: parsed.toString(), - links: externalUrls, - author: author, - contents: contents, - }); -}; - -function searchSuggestions(searchTerm) { - try { - const resp = simpleJsonGet(toUrl(`/Search/Hints?searchTerm=${searchTerm}`)); - - return resp.body.SearchHints.map((item) => item.Name).filter(onlyUnique); - } catch (e) { - console.error(e); - return []; - } -}; - -function getSearchCapabilities() { - return { - types: [Type.Feed.Mixed, Type.Feed.Streams, Type.Feed.Videos], - sorts: [], - }; -}; - -function search(query, type, order, filters, channelId) { - const url = toUrl('/Search/Hints?MediaTypes=Video,Audio') - - return new JellyfinSearchContentPager({ url, query, type, order, filters, channelId }); -}; - -function searchChannels(query) { - // TODO: Add back Person, Studio - const url = toUrl('/Search/Hints?includeItemTypes=Channel,Genre,MusicArtist,MusicGenre,Series') - - return new JellyfinSearchContentPager({ url, query }); -}; - - -// source.searchChannelContents = function ( -// channelUrl, -// query, -// type, -// order, -// filters, -// ) { -// return new ParentPaginator(channelUrl, query, type, order, filters); -// }; - -function searchPlaylists(query, type, order, filters, channelId) { - const url = toUrl('/Search/Hints?includeItemTypes=Folder,ManualPlaylistsFolder,MusicAlbum,Playlist,PlaylistsFolder,Season') - - return new JellyfinSearchContentPager({ url, query, type, order, filters, channelId }) -}; - -// Jellyfin does not have comments AFAIK -function getComments(url) { - return new CommentPager([], false, {}); -}; - -function getSubComments(comment) { - return new CommentPager([], false, {}); -}; - -// HELPERS -function authHeaders() { - // Return empty headers if config not initialized - if (!config.constants) { - return {}; - } - return { - authorization: `MediaBrowser Token="${config.constants.token || ""}", Client="${config.constants.client || "Grayjay"}", Version="${config.constants.version || "1.0.0"}", DeviceId="${config.constants.device_id || "grayjay-device"}", Device="${config.constants.device_name || "Grayjay Client"}"`, - }; -} - -function toUrl(path) { - // Return empty string if config not initialized to prevent errors - if (!config.constants || !config.constants.uri) { - return ""; - } - return `${config.constants.uri}${path}`; -} - -function simpleJsonGet(url, error) { - const resp = simpleGet(url, error); - return { ...resp, body: JSON.parse(resp.body) }; -} - -function simpleGet(url, error) { - const resp = http.GET(url, authHeaders(), false); - - if (!resp.isOk) { - throw new ScriptException(error || "Failed to request data from Jellyfin"); - } - - return resp; -} - -function batchedJSONRequests(requests, error) { - // Inject content-type into all headers - for (const request of requests) { - request.headers = Object.assign( - { "content-type": "application/json" }, - request.headers || {}, - ); - } - const responses = batchedRequests(requests, error); - - return responses.map((response) => ({ ...response, body: JSON.parse(response.body) })); -} - -function batchedRequests(requests, error) { - let client = http.batch(); - - for (const request of requests) { - const headers = Object.assign(authHeaders(), request.headers || {}); - - if (request.body != null) { - client.requestWithBody( - request.method || "POST", - request.url, - request.body, - headers, - false, - ); - } else { - client.request(request.method || "GET", request.url, headers, false); - } - } - - const responses = client.execute(); - - for (const response of responses) { - if (!response.isOk) { - throw new ScriptException( - error || "Failed to request data from Jellyfin", - ); - } - } - - return responses; -} - -function isType(url, types) { - try { - // Check if config is initialized before using toUrl - if (!config.constants || !config.constants.uri) { - // If not initialized, fall back to basic URL pattern matching - // Check for Jellyfin URL patterns with specific types - const urlObj = new URL(url); - const typeParam = urlObj.searchParams.get("type"); - if (typeParam) { - return types.includes(typeParam); - } - // For URLs without type param, we can't determine type without config - // Return false and let the initialized version handle it later - return false; - } - - if (url.startsWith(toUrl("/Items"))) { - let parsed = new URL(url); - let type = parsed.searchParams.get("type"); - - if (type == null) { - // Use pathname to extract itemId, avoiding query string issues - const itemId = urlId(url); - let resp = simpleJsonGet( - toUrl(`/Items/${itemId}`), - "Could not fetch details", - ); - - return types.includes(resp.body.Type); - } else { - return types.includes(type); - } - } else { - return false; - } - } catch (e) { - console.log("isType error: " + e.message + " for url: " + url); - return false; - } -} - -function onlyUnique(value, index, array) { - return array.indexOf(value) == index; -} - -function map_push_duplicate(map, key, value, index) { - let insertKey = key; - - if (index != null) { - insertKey = insertKey + ` ${index}`; - } else { - index = 1; - } - - if (map.has(insertKey)) { - map_push_duplicate(map, key, value, index + 1); - } else { - map.set(insertKey, value); - } -} - -function toDuration(runTimeTicks) { - return Math.round(runTimeTicks / 10_000_000); -} - -function author(item) { - return new PlatformAuthorLink( - new PlatformID(PLATFORM, item.Id, config.id), - item.Name, - itemUrl(item), - thumbnail({ item, query: { fillWidth: 256} }) - ); -} - -function itemThumbnails(itemId) { - let url = new URL(toUrl(`/Items/${itemId}/Images/Primary`)); - url.searchParams.set("quality", "50"); - - url.searchParams.set("fillWidth", "240"); - let url1 = url.toString(); - - url.searchParams.set("fillWidth", "480"); - let url2 = url.toString(); - - url.searchParams.set("quality", "50"); - url.searchParams.set("fillWidth", "720"); - let url3 = url.toString(); - - url.searchParams.set("fillWidth", "1080"); - let url4 = url.toString(); - - return new Thumbnails([ - new Thumbnail(url1, 240), - new Thumbnail(url2, 480), - new Thumbnail(url3, 720), - new Thumbnail(url4, 1080), - ]); -} - -function urlId(url) { - const segments = new URL(url).pathname.split("/") - return segments[segments.length - 1]; -} - -function parseItem(item) { - switch (item.Type) { - case "Episode": - case "Movie": - // case "MusicVideo": - // case "Video": - return new PlatformVideo({ - id: new PlatformID(PLATFORM, item.Id, config.id), - name: item.Name, - thumbnails: itemThumbnails(item.Id), - // uploadDate: new Date(item.DateCreated).getTime() / 1000, - url: toUrl(`/Items/${item.Id}?type=Video`), - duration: toDuration(item.RunTimeTicks), - isLive: false, - }); - - case "Audio": - return new PlatformVideo({ - id: new PlatformID(PLATFORM, item.Id, config.id), - name: item.Name, - thumbnails: item.AlbumId - ? itemThumbnails(item.AlbumId) - : itemThumbnails(item.Id), - // uploadDate: new Date(item.DateCreated).getTime() / 1000, - url: toUrl(`/Items/${item.Id}?type=Audio`), - duration: toDuration(item.RunTimeTicks), - isLive: false, - }); - case "AudioBook": - return new PlatformVideo({ - id: new PlatformID(PLATFORM, item.Id, config.id), - name: item.Name, - thumbnails: itemThumbnails(item.Id), - // uploadDate: new Date(item.DateCreated).getTime() / 1000, - url: toUrl(`/Items/${item.Id}?type=Audio`), - duration: toDuration(item.RunTimeTicks), - isLive: false, - }); - - - // case "Channel": - // case "LiveTvChannel": - case "MusicArtist": - // case "MusicGenre": - // case "Person": - case "Studio": - case "Series": - return new PlatformChannel({ - id: new PlatformID(PLATFORM, item.Id, config.id), - name: item.Name, - description: item.Overview, - thumbnail: thumbnail({ item }), - banner: banner({ item }), - url: toUrl(`/Items/${item.Id}?type=${item.Type}`), - links: item.ExternalUrls?.reduce((acc, item) => { - acc[item.Name] = item.Url; - return acc; - }, {}) - - }); - // return new - - case "Playlist": - case "Season": - case "MusicAlbum": - // case "Program": - return new PlatformPlaylist({ - id: new PlatformID(PLATFORM, item.Id, config.id), - name: item.Name, - url: toUrl(`/Items/${item.Id}?type=${item.Type}`), - thumbnail: banner({ item }), - }); - } -} - -function extractAuthors(items, authors) { - const authorIds = items.map(authorId); - const uniqueIds = authorIds - .filter(id => id != null) - .filter(authorId => !(authorId in authors)) - .filter(onlyUnique); - - if (uniqueIds.length == 0) return []; - - simpleJsonGet(toUrl(`/Items?Ids=${uniqueIds.join(",")}`)) - .body - .Items - .forEach((item) => authors[item.Id] = author(item)); - - return authorIds.map((id) => id && authors[id]); -} - -function authorId(item) { - switch (item.Type) { - case "Episode": - case "Season": - return item.SeriesId; - - case "Audio": - case "MusicAlbum": - if (item.AlbumArtists.length > 0) { - return item.AlbumArtists[0].Id; - } - - if (item.Artists.length > 0) { - return item.Artists[0].Id; - } - return null; - - case "Movie": - if (item.Studios.length > 0) { - return item.Studios[0].Id; - } - return null; - - default: - return null; - } -}; - -function formatEntries({ items, authors }) { - return zip(items, authors).map(([item, author]) => { - return formatItem(item, author) - }); -} - -function zip(...args) { - const [first, ...rest] = args; - - return first.map((item, i) => { - let acc = [item]; - rest.forEach((other) => acc.push(other[i])) - return acc; - }); -} - -function formatItem(item, author) { - const url = toUrl(`/Items/${item.Id}?type=${item.Type}`); - - // Create a default author if none provided - use series/album info or item itself - const defaultAuthor = author || createDefaultAuthor(item); - - switch (item.Type) { - case "Folder": - case "ManualPlaylistFolder": - case "Playlist": - case "PlaylistsFolder": - case "MusicAlbum": - case "Season": - return new PlatformPlaylist({ - id: itemId(item), - author: defaultAuthor, - name: item.Name, - url: url, - thumbnail: banner({ item }), - datetime: parseDate(item.PremiereDate || item.DateCreated), - videoCount: item.ChildCount - }); - - case "Channel": - case "Genre": - case "MusicArtist": - case "MusicGenre": - // case "Person": - case "Series": - case "Studio": - return new PlatformChannel({ - id: itemId(item), - name: item.Name, - description: item.Overview || item.Description, - url: url, - thumbnail: thumbnail({ item, order: ["Logo", "Thumb", "Primary"], query: { fillWidth: 128 } }), - banner: banner({ item }), - links: item.ExternalUrls?.reduce((acc, item) => { - acc[item.Name] = item.Url; - return acc; - }, {}) - }); - - default: - switch (item.MediaType) { - case "Video": - case "Audio": - return new PlatformVideo({ - id: itemId(item), - author: defaultAuthor, - name: item.Name, - thumbnails: itemThumbnails(item.Id), - url: url, - duration: toDuration(item.RunTimeTicks), - isLive: false, - viewCount: item.UserData?.PlaybackCount || 0, - datetime: parseDate(item.PremiereDate || item.DateCreated), - }); - - } - }; - throw new ScriptException("Unknown item type"); -} - -function createDefaultAuthor(item) { - // Try to get author info from item's series/album/studio info - let authorName = item.SeriesName || item.AlbumArtist || item.Album || item.Name; - let authorId = item.SeriesId || item.AlbumId || item.Id; - let authorUrl = item.SeriesId - ? toUrl(`/Items/${item.SeriesId}?type=Series`) - : toUrl(`/Items/${item.Id}?type=${item.Type}`); - - return new PlatformAuthorLink( - new PlatformID(PLATFORM, authorId, config.id), - authorName, - authorUrl, - null - ); -} - -function itemId(item) { - return new PlatformID(PLATFORM, item.Id, config.id); -} - -function itemUrl({ Id, Type }) { - return toUrl(`/Items/${Id}?type=${Type}`) -} - -function thumbnail({ item, order = ["Primary", "Logo", "Thumb"], query }) { - let type; - let tag; - - if (item.ImageTags != null) { - type = order.find((type) => type in item.ImageTags); - tag = item.ImageTags[type]; - } else { - type = order.find((type) => `${type}ImageTag` in item); - - if (type == null) return null; - - tag = item[`${type}ImageTag`]; - } - - let url = toUrl(`/Items/${item.Id}/Images/${type}?tag=${tag}`); - - return withQuery(url, query); -} - -function banner({ item, query }) { - if (item.BackdropImageTag != null) { - return withQuery(toUrl(`/Items/${item.Id}/Images/Backdrop?tag=${item.BackdropImageTag}]`), query); - } else if (item.BackgroundImageTags != null && item.BackdropImageTags.length > 0) { - return withQuery(toUrl(`/Items/${item.Id}/Images/Backdrop/0?tag=${item.BackdropImageTags[0]}]`), query); - } else { - return thumbnail({ item, query }); - } -} - -function withQuery(url, query) { - if (query == null) return url; - - let parsedUrl = new URL(url); - for (let key in query) parsedUrl.searchParams.append(key, query[key]); - return parsedUrl.toString(); -} - -function parseDate(value) { - return new Date(value).getTime() / 1000; -}