finish simplification of ids into single value
This commit is contained in:
parent
61795fdc5e
commit
7b363964f7
10 changed files with 86 additions and 132 deletions
|
@ -84,12 +84,12 @@ export const listItemIds = query(
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
data.Items?.map((item) => {
|
data.Items?.map((item) => {
|
||||||
const type = {
|
const type = {
|
||||||
Movie: 'movie',
|
Movie: 'm',
|
||||||
Series: 'tv',
|
Series: 's',
|
||||||
}[item.Type as string] ?? 'unknown';
|
}[item.Type as string] ?? '';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`${type}-${item.ProviderIds!["Tmdb"]!}`,
|
`${type}${item.ProviderIds!["Tmdb"]!}`,
|
||||||
{ jellyfin: item.Id! },
|
{ jellyfin: item.Id! },
|
||||||
];
|
];
|
||||||
}) ?? []
|
}) ?? []
|
||||||
|
@ -125,7 +125,7 @@ export const listItems = query(
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
data.Items?.map((item) => mapToEntry(item)) ?? []
|
data.Items?.map((item) => toEntry(item)) ?? []
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
"jellyfin.listItems",
|
"jellyfin.listItems",
|
||||||
|
@ -166,7 +166,7 @@ export const getRandomItems = query(
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
data?.Items?.map((item) => mapToEntry(item)) ?? []
|
data?.Items?.map((item) => toEntry(item)) ?? []
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
"jellyfin.listRandomItems",
|
"jellyfin.listRandomItems",
|
||||||
|
@ -201,7 +201,7 @@ export const getItem = query(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapToEntry(data);
|
return toEntry(data);
|
||||||
},
|
},
|
||||||
"jellyfin.getItem",
|
"jellyfin.getItem",
|
||||||
);
|
);
|
||||||
|
@ -357,21 +357,20 @@ function assertNoErrors<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapToEntry = (item: components['schemas']['BaseItemDto']): Entry => {
|
const toEntry = (item: components['schemas']['BaseItemDto']): Entry => {
|
||||||
const type = {
|
const type = {
|
||||||
Movie: 'movie',
|
Movie: 'm',
|
||||||
Series: 'tv',
|
Series: 's',
|
||||||
}[item.Type as string] as any;
|
}[item.Type as string] as any;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
id: `${type}${item.ProviderIds!["Tmdb"]!}`,
|
||||||
id: item.ProviderIds!["Tmdb"]!,
|
title: item.Name!,
|
||||||
title: item.Name!,
|
overview: item.Overview!,
|
||||||
overview: item.Overview!,
|
thumbnail: new URL(`/Items/${item.Id}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||||
thumbnail: new URL(`/Items/${item.Id}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
image: new URL(`/Items/${item.Id}/Images/Backdrop`, getBaseUrl()),
|
||||||
image: new URL(`/Items/${item.Id}/Images/Backdrop`, getBaseUrl()),
|
providers: {
|
||||||
providers: {
|
jellyfin: item.Id
|
||||||
jellyfin: item.Id
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
};
|
};
|
|
@ -1,9 +1,19 @@
|
||||||
import createClient from "openapi-fetch";
|
import createClient from "openapi-fetch";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { Entry, SearchResult } from "../types";
|
import { Entry, SearchResult } from "../types";
|
||||||
import { paths as pathsV3 } from "./tmdb.generated";
|
import { paths as pathsV3, operations } from "./tmdb.generated";
|
||||||
import { paths as pathsV4 } from "./tmdb.not.generated";
|
import { paths as pathsV4 } from "./tmdb.not.generated";
|
||||||
|
|
||||||
|
interface TMDBItem {
|
||||||
|
id: number;
|
||||||
|
media_type: string;
|
||||||
|
name?: string;
|
||||||
|
title?: string;
|
||||||
|
overview?: string;
|
||||||
|
backdrop_path?: string;
|
||||||
|
poster_path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const getClients = () => {
|
const getClients = () => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
@ -28,20 +38,27 @@ const getClients = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getEntry = query(
|
export const getEntry = query(
|
||||||
async (type: Entry['type'], id: string): Promise<Entry | undefined> => {
|
async (id: string): Promise<Entry | undefined> => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const [ clientV3 ] = getClients();
|
const [ clientV3 ] = getClients();
|
||||||
|
|
||||||
|
const mediaType = ({
|
||||||
|
m: 'movie',
|
||||||
|
s: 'tv',
|
||||||
|
} as const)[id[0]]!;
|
||||||
|
|
||||||
const endpoint = ({
|
const endpoint = ({
|
||||||
movie: "/movie/{movie_id}",
|
movie: "/movie/{movie_id}",
|
||||||
tv: '/tv/{series_id}',
|
tv: '/tv/{series_id}',
|
||||||
} as const)[type];
|
} as const)[mediaType];
|
||||||
|
|
||||||
const params = ({
|
const params = ({
|
||||||
movie: { movie_id: Number.parseInt(id) },
|
movie: { movie_id: Number.parseInt(id.slice(1)) },
|
||||||
tv: { series_id: Number.parseInt(id) },
|
tv: { series_id: Number.parseInt(id.slice(1)) },
|
||||||
} as const)[type];
|
} as const)[mediaType];
|
||||||
|
|
||||||
|
console.log(`going to fetch from '${endpoint}' with id '${id}'`)
|
||||||
|
|
||||||
const { data } = await clientV3.GET(endpoint, {
|
const { data } = await clientV3.GET(endpoint, {
|
||||||
params: {
|
params: {
|
||||||
|
@ -53,14 +70,7 @@ export const getEntry = query(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return toEntry(data as any, mediaType);
|
||||||
type,
|
|
||||||
id: String(data.id ?? -1),
|
|
||||||
title: data.title!,
|
|
||||||
overview: data.overview,
|
|
||||||
thumbnail: `http://image.tmdb.org/t/p/w342${data.poster_path}`,
|
|
||||||
image: `http://image.tmdb.org/t/p/original${data.backdrop_path}`,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
"tmdb.getEntry",
|
"tmdb.getEntry",
|
||||||
);
|
);
|
||||||
|
@ -85,22 +95,13 @@ export const getRecommendations = query(async (): Promise<Entry[]> => {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return data?.results.map(
|
return data?.results.map((item) => toEntry(item));
|
||||||
({ id, title, overview, poster_path, backdrop_path }) => ({
|
|
||||||
type: 'movie',
|
|
||||||
id: String(id ?? -1),
|
|
||||||
title: title!,
|
|
||||||
overview,
|
|
||||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
|
||||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}, "tmdb.getRecommendations");
|
}, "tmdb.getRecommendations");
|
||||||
|
|
||||||
export const getDiscovery = query(async (): Promise<Entry[]> => {
|
export const getDiscovery = query(async (): Promise<Entry[]> => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const [ clientV3 ] = getClients();
|
const [ clientV3 ] = getClients();
|
||||||
|
|
||||||
const [{ data: movies }, { data: series }] = await Promise.all([
|
const [{ data: movies }, { data: series }] = await Promise.all([
|
||||||
clientV3.GET("/discover/movie"),
|
clientV3.GET("/discover/movie"),
|
||||||
|
@ -111,25 +112,8 @@ export const getDiscovery = query(async (): Promise<Entry[]> => {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const movieEntries = movies?.results?.slice(0, 10)
|
const movieEntries = movies?.results?.slice(0, 10).map((item): Entry => toEntry(item)) ?? []
|
||||||
.map(({ id, title, overview, poster_path, backdrop_path }): Entry => ({
|
const seriesEntries = series?.results?.slice(0, 10).map((item): Entry => toEntry(item)) ?? []
|
||||||
type: 'movie',
|
|
||||||
id: String(id ?? -1),
|
|
||||||
title: title!,
|
|
||||||
overview,
|
|
||||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
|
||||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
|
||||||
})) ?? []
|
|
||||||
|
|
||||||
const seriesEntries = series?.results?.slice(0, 10)
|
|
||||||
.map(({ id, name, overview, poster_path, backdrop_path }): Entry => ({
|
|
||||||
type: 'tv',
|
|
||||||
id: String(id ?? -1),
|
|
||||||
title: name!,
|
|
||||||
overview,
|
|
||||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
|
||||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
|
||||||
})) ?? []
|
|
||||||
|
|
||||||
return movieEntries.concat(seriesEntries);
|
return movieEntries.concat(seriesEntries);
|
||||||
}, "tmdb.getDiscovery");
|
}, "tmdb.getDiscovery");
|
||||||
|
@ -158,18 +142,23 @@ export const searchMulti = query(async (query: string, page: number = 1): Promis
|
||||||
return { count: 0, pages: 0, results: [] };
|
return { count: 0, pages: 0, results: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`loaded page ${page}, found ${data.results?.length} results`);
|
return { count: data.total_results!, pages: data.total_pages!, results: data.results?.filter(({ media_type }) => media_type === 'movie' || media_type === 'tv').map((item): Entry => toEntry(item)) ?? [] };
|
||||||
console.log(data.results[0]);
|
}, "tmdb.search.multi");
|
||||||
|
|
||||||
return { count: data.total_results!, pages: data.total_pages!, results: data.results?.filter(({ media_type }) => media_type === 'movie' || media_type === 'tv').map(({ id, name, title, media_type, overview, backdrop_path, poster_path }): Entry => ({
|
function toEntry(item: TMDBItem): Entry;
|
||||||
type: ({
|
function toEntry(item: Omit<TMDBItem, 'media_type'>, mediaType: string): Entry;
|
||||||
movie: 'movie',
|
|
||||||
tv: 'tv',
|
function toEntry({ id, name, title, overview, media_type = '', backdrop_path, poster_path }: TMDBItem | Omit<TMDBItem, 'media_type'>, mediaType?: string): Entry {
|
||||||
}[media_type ?? '']) as any,
|
const type = ({
|
||||||
id: String(id),
|
movie: 'm',
|
||||||
title: `${name ?? title ?? ''} (${media_type})`,
|
tv: 's',
|
||||||
|
}[(mediaType ?? media_type) as string]);
|
||||||
|
|
||||||
|
return ({
|
||||||
|
id: `${type}${String(id ?? -1)}`,
|
||||||
|
title: `${name ?? title ?? ''}`,
|
||||||
overview,
|
overview,
|
||||||
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||||
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||||
})) ?? [] };
|
});
|
||||||
}, "tmdb.search.multi");
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { toSlug } from "~/utilities";
|
import { toSlug } from "~/utilities";
|
||||||
import type { Entry } from "./types";
|
import type { Entry } from "./types";
|
||||||
|
|
||||||
export const emptyEntry = Object.freeze<Entry>({ type: 'movie', id: '0', title: '' });
|
export const emptyEntry = Object.freeze<Entry>({ id: '0', title: '' });
|
||||||
|
|
||||||
export const createSlug = (entry: Entry) => toSlug(`${entry.title}-${entry.type}-${entry.id}`);
|
export const createSlug = (entry: Entry) => toSlug(`${entry.title}-${entry.id}`);
|
||||||
|
|
|
@ -15,10 +15,10 @@ const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||||
const lookupTable = query(async () => listItemIds(), 'content.lookupTable');
|
const lookupTable = query(async () => listItemIds(), 'content.lookupTable');
|
||||||
|
|
||||||
export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
||||||
export const getStream = query(async (type: Entry['type'], id: string, range: string) => {
|
export const getStream = query(async (id: string, range: string) => {
|
||||||
const table = await lookupTable();
|
const table = await lookupTable();
|
||||||
|
|
||||||
return getItemStream(table[`${type}-${id}`].jellyfin, range);
|
return getItemStream(table[id].jellyfin, range);
|
||||||
}, 'content.stream');
|
}, 'content.stream');
|
||||||
|
|
||||||
export const listCategories = query(async (): Promise<Category[]> => {
|
export const listCategories = query(async (): Promise<Category[]> => {
|
||||||
|
@ -35,16 +35,16 @@ export const listCategories = query(async (): Promise<Category[]> => {
|
||||||
|
|
||||||
export const getEntryFromSlug = query(
|
export const getEntryFromSlug = query(
|
||||||
async (slug: string): Promise<Entry | undefined> => {
|
async (slug: string): Promise<Entry | undefined> => {
|
||||||
const { type, id } = slug.match(/^.+-(?<type>\w+)-(?<id>\w+)$/)?.groups ?? {};
|
const { id } = slug.match(/^.+-(?<id>\w+)$/)?.groups ?? {};
|
||||||
|
|
||||||
return getTmdbEntry(type as any, id);
|
return getTmdbEntry(id);
|
||||||
},
|
},
|
||||||
"content.getFromSlug",
|
"content.getFromSlug",
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getEntry = query(
|
export const getEntry = query(
|
||||||
async (type: Entry['type'], id: Entry["id"]): Promise<Entry | undefined> => {
|
async (id: Entry["id"]): Promise<Entry | undefined> => {
|
||||||
return getTmdbEntry(type, id);
|
return getTmdbEntry(id);
|
||||||
},
|
},
|
||||||
"content.get",
|
"content.get",
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,6 @@ export interface Category {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
type: 'movie'|'tv';
|
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
overview?: string;
|
overview?: string;
|
||||||
|
|
|
@ -90,8 +90,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
|
|
||||||
<video
|
<video
|
||||||
ref={setVideo}
|
ref={setVideo}
|
||||||
src={`/api/content/${props.entry.type}/${props.entry.id}/stream`}
|
src={`/api/content/${props.entry.id}/stream`}
|
||||||
// src="https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4"
|
|
||||||
poster={props.entry.image}
|
poster={props.entry.image}
|
||||||
lang="en"
|
lang="en"
|
||||||
>
|
>
|
||||||
|
|
14
src/routes/api/content/[id]/stream.ts
Normal file
14
src/routes/api/content/[id]/stream.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { APIEvent } from "@solidjs/start/server";
|
||||||
|
import { getStream } from "~/features/content";
|
||||||
|
|
||||||
|
export const GET = async ({ request, params: { id } }: APIEvent) => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
const range = request.headers.get("range");
|
||||||
|
|
||||||
|
if (range === null) {
|
||||||
|
return new Response("Requires Range header", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return getStream(id, range);
|
||||||
|
};
|
|
@ -1,46 +0,0 @@
|
||||||
import { APIEvent } from "@solidjs/start/server";
|
|
||||||
import { getStream } from "~/features/content";
|
|
||||||
|
|
||||||
// const CHUNK_SIZE = 1 * 1e6; // 1MB
|
|
||||||
|
|
||||||
export const GET = async ({ request, params: { type, id } }: APIEvent) => {
|
|
||||||
"use server";
|
|
||||||
|
|
||||||
const range = request.headers.get("range");
|
|
||||||
|
|
||||||
if (range === null) {
|
|
||||||
return new Response("Requires Range header", { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return getStream(type, id, range);
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const file = Bun.file(
|
|
||||||
// import.meta.dirname + "/SampleVideo_1280x720_10mb.mp4",
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if ((await file.exists()) !== true) {
|
|
||||||
// return new Response("File not found", { status: 404 });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const videoSize = file.size;
|
|
||||||
// const start = Number.parseInt(range.replace(/\D/g, ""));
|
|
||||||
// const end = Math.min(start + CHUNK_SIZE - 1, videoSize - 1);
|
|
||||||
|
|
||||||
// console.log(`streaming slice(${start}, ${end})`);
|
|
||||||
|
|
||||||
// return new Response(file.slice(start, end), {
|
|
||||||
// status: 206,
|
|
||||||
// headers: {
|
|
||||||
// 'Accept-Ranges': 'bytes',
|
|
||||||
// 'Content-Range': `bytes ${start}-${end}/${videoSize}`,
|
|
||||||
// 'Content-Length': `${end - start + 1}`,
|
|
||||||
// 'Content-Type': 'video/mp4',
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// } catch (e) {
|
|
||||||
// console.error(e);
|
|
||||||
|
|
||||||
// throw e;
|
|
||||||
// }
|
|
||||||
};
|
|
Loading…
Add table
Add a link
Reference in a new issue