streamarr/src/features/content/apis/jellyfin.ts

375 lines
8.4 KiB
TypeScript

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<paths>({
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<Record<string, { jellyfin: string }>> => {
"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<Entry[] | undefined> => {
"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<Entry | undefined> => {
"use server";
return getRandomItems(userId, 1).then((items) => items?.at(0));
},
"jellyfin.listRandomItem",
);
export const getRandomItems = query(
async (userId: string, limit: number = 20): Promise<Entry[]> => {
"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<Entry | undefined> => {
"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<Response> => {
"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<any | undefined> => {
"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<any | undefined> => {
"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<Entry[]> => {
"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<string>(
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<Entry> =>
result.value !== undefined,
)
.map(({ value }) => value);
},
"jellyfin.continueWatching",
);
function assertNoErrors<T>(
results: PromiseSettledResult<T>[],
): asserts results is PromiseFulfilledResult<T>[] {
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),
});
}
}