import type { paths } from "./jellyfin.generated"; // generated by openapi-typescript import createClient from "openapi-fetch"; import { query } from "@solidjs/router"; import { Entry } from "../types"; type ItemImageType = | "Primary" | "Art" | "Backdrop" | "Banner" | "Logo" | "Thumb" | "Disc" | "Box" | "Screenshot" | "Menu" | "Chapter" | "BoxRear" | "Profile"; const getBaseUrl = () => { "use server"; return process.env.JELLYFIN_BASE_URL; }; const getClient = () => { "use server"; return createClient({ baseUrl: getBaseUrl(), headers: { Authorization: `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`, "Content-Type": 'application/json; profile="CamelCase"', }, }); }; export const getCurrentUser = query(async () => { "use server"; const { data, error, response } = await getClient().GET("/Users/Public", { params: {}, }); return data; }, "jellyfin.getCurrentUser"); export const listUsers = query(async () => { "use server"; const { data, error } = await getClient().GET("/Users", { params: {}, }); return data ?? []; }, "jellyfin.listUsers"); export const listItemIds = query( async (): Promise> => { "use server"; const { data, error } = await getClient().GET("/Items", { params: { query: { hasImdbId: true, recursive: true, includeItemTypes: ["Movie", "Series"], fields: [ "ProviderIds", "Genres", "DateLastMediaAdded", "DateCreated", "MediaSources", ], }, }, }); if (data === undefined) { return {}; } return Object.fromEntries( data.Items?.map((item) => ([ item.ProviderIds!["Tmdb"]!, { jellyfin: item.Id! }, ])) ?? [] ); }, "jellyfin.listItemIds", ); export const listItems = query( async (userId: string): Promise => { "use server"; const { data, error } = await getClient().GET("/Items", { params: { query: { userId, hasTmdbInfo: true, recursive: true, includeItemTypes: ["Movie", "Series"], fields: [ "ProviderIds", "Genres", "DateLastMediaAdded", "DateCreated", "MediaSources", ], }, }, }); if (data === undefined) { return undefined; } return ( data.Items?.map((item) => ({ // id: item.Id!, id: item.ProviderIds!["Tmdb"]!, title: item.Name!, thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), })) ?? [] ); }, "jellyfin.listItems", ); export const getRandomItem = query( async (userId: string): Promise => { "use server"; return getRandomItems(userId, 1).then((items) => items?.at(0)); }, "jellyfin.listRandomItem", ); export const getRandomItems = query( async (userId: string, limit: number = 20): Promise => { "use server"; const { data, error } = await getClient().GET("/Items", { params: { query: { userId, hasTmdbInfo: true, recursive: true, limit, sortBy: ["Random"], includeItemTypes: ["Movie", "Series"], imageTypes: ["Primary", "Backdrop", "Thumb"], fields: [ "ProviderIds", "Genres", "DateLastMediaAdded", "DateCreated", "MediaSources", ], }, }, }); return ( data?.Items?.map((item) => ({ // id: item.Id!, id: item.ProviderIds!["Tmdb"]!, title: item.Name!, thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), image: new URL(`/Items/${item.Id!}/Images/Backdrop`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), })) ?? [] ); }, "jellyfin.listRandomItems", ); export const getItem = query( async (userId: string, itemId: string): Promise => { "use server"; const { data, error } = await getClient().GET("/Items/{itemId}", { params: { path: { itemId, }, query: { userId, hasTmdbInfo: true, recursive: true, includeItemTypes: ["Movie", "Series"], fields: [ "ProviderIds", "Genres", "DateLastMediaAdded", "DateCreated", "MediaSources", ], }, }, }); if (data === undefined) { return undefined; } return { // id: data.Id!, id: data.ProviderIds!["Tmdb"]!, title: data.Name!, overview: data.Overview!, thumbnail: new URL(`/Items/${itemId}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), image: new URL(`/Items/${itemId}/Images/Backdrop`, getBaseUrl()), providers: { jellyfin: data.Id } // ...data, }; }, "jellyfin.getItem", ); export const getItemStream = query( async (itemId: string, range: string): Promise => { "use server"; // I don't really know what is the big difference between mp4 and mkv. // But mkv is able to use ranges an can report the video's length, whereas mp4 doesn't const { response } = await getClient().GET("/Videos/{itemId}/stream", { params: { path: { itemId, }, query: { static: true, container: 'mkv', }, }, parseAs: 'stream', headers: { Range: range } }); return response; }, "jellyfin.getItemStream", ); export const getItemImage = query( async ( itemId: string, imageType: ItemImageType, ): Promise => { "use server"; const { data, error } = await getClient().GET( "/Items/{itemId}/Images/{imageType}", { parseAs: "blob", params: { path: { itemId, imageType, }, query: {}, }, }, ); return data; }, "jellyfin.getItemImage", ); export const getItemPlaybackInfo = query( async (userId: string, itemId: string): Promise => { "use server"; const { data, error, response } = await getClient().GET( "/Items/{itemId}/PlaybackInfo", { parseAs: "text", params: { path: { itemId, }, query: { userId, }, }, }, ); return undefined; }, "jellyfin.getItemPlaybackInfo", ); export const queryItems = query(async () => { "use server"; const { data, error } = await getClient().GET("/Items", { params: { query: { mediaTypes: ["Video"], isUnaired: true, limit: 10, // fields: ["ProviderIds", "Genres"], includeItemTypes: ["Series", "Movie"], recursive: true, }, }, }); console.log(data); }, "jellyfin.queryItems"); export const getContinueWatching = query( async (userId: string): Promise => { "use server"; const { data, error } = await getClient().GET("/UserItems/Resume", { params: { query: { userId, mediaTypes: ["Video"], // fields: ["ProviderIds", "Genres"], // includeItemTypes: ["Series", "Movie"] }, }, }); if (Array.isArray(data?.Items) !== true) { return []; } const uniqueIds = new Set( data.Items.map((item) => item.Type === "Episode" ? item.SeriesId! : item.Id!, ), ); const results = await Promise.allSettled( uniqueIds .values() .map((id) => getItem(userId, id)) .toArray(), ); assertNoErrors(results); return results .filter( (result): result is PromiseFulfilledResult => result.value !== undefined, ) .map(({ value }) => value); }, "jellyfin.continueWatching", ); function assertNoErrors( results: PromiseSettledResult[], ): asserts results is PromiseFulfilledResult[] { if (results.some(({ status }) => status !== "fulfilled")) { throw new Error("one or more promices failed", { cause: results .filter((r): r is PromiseRejectedResult => r.status === "rejected") .map((r) => r.reason), }); } }