375 lines
8.4 KiB
TypeScript
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),
|
|
});
|
|
}
|
|
}
|