made some nice progress today!
This commit is contained in:
parent
78b9857f57
commit
7c5d2a25ff
15 changed files with 1065 additions and 330 deletions
|
@ -1,27 +1,9 @@
|
||||||
import { defineConfig } from '@solidjs/start/config';
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
import { browserslistToTargets, Features } from 'lightningcss';
|
import solidSvg from "vite-plugin-solid-svg";
|
||||||
import browserslist from 'browserslist';
|
import devtools from "solid-devtools/vite";
|
||||||
import solidSvg from 'vite-plugin-solid-svg';
|
|
||||||
import devtools from 'solid-devtools/vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
vite: {
|
vite: {
|
||||||
// css: {
|
|
||||||
// transformer: 'lightningcss',
|
|
||||||
// lightningcss: {
|
|
||||||
// targets: browserslistToTargets(browserslist('>= .25%')),
|
|
||||||
// include: Features.Nesting | Features.LightDark | Features.Colors,
|
|
||||||
// customAtRules: {
|
|
||||||
// property: {
|
|
||||||
// prelude: '<custom-ident>',
|
|
||||||
// body: 'style-block',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// build: {
|
|
||||||
// cssMinify: 'lightningcss',
|
|
||||||
// },
|
|
||||||
plugins: [
|
plugins: [
|
||||||
devtools({
|
devtools({
|
||||||
autoname: true,
|
autoname: true,
|
||||||
|
@ -35,12 +17,9 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
preset: 'bun',
|
preset: "bun",
|
||||||
prerender: {
|
prerender: {
|
||||||
routes: [
|
routes: ["/sitemaps.xml"],
|
||||||
'/sitemaps.xml',
|
|
||||||
'/watch/furiosa-a-mad-max-saga-1c829d55201c766641c4aec0346551c6'
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -1,16 +1,23 @@
|
||||||
@property --thumb-image {
|
@property --thumb-image {
|
||||||
syntax: '<image>';
|
syntax: "<image>";
|
||||||
inherits: true;
|
inherits: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
isolation: isolate;
|
||||||
display: block grid;
|
display: block grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: 100%;
|
grid-auto-columns: 100%;
|
||||||
|
|
||||||
|
container-type: inline-size;
|
||||||
|
|
||||||
overflow: hidden visible;
|
overflow: hidden visible;
|
||||||
scroll-snap-type: inline mandatory;
|
scroll-snap-type: inline mandatory;
|
||||||
scroll-behavior: smooth;
|
overscroll-behavior-inline: contain;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
scroll-marker-group: after;
|
scroll-marker-group: after;
|
||||||
|
|
||||||
|
@ -19,8 +26,10 @@
|
||||||
|
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-auto-columns: 5em;
|
grid-auto-columns: 5em;
|
||||||
gap: 1em;
|
gap: 1rem;
|
||||||
place-content: end center;
|
justify-content: start;
|
||||||
|
|
||||||
|
padding-inline: 2rem;
|
||||||
|
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: 8.333333em;
|
block-size: 8.333333em;
|
||||||
|
@ -30,6 +39,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
|
--__i: var(--sibling-index);
|
||||||
|
--__c: var(--sibling-count);
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -41,43 +52,49 @@
|
||||||
"thumbnail summary summary";
|
"thumbnail summary summary";
|
||||||
align-content: end;
|
align-content: end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1em;
|
gap: 1rem;
|
||||||
padding: 2em;
|
padding: 2rem;
|
||||||
block-size: 80vh;
|
block-size: 80vh;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
container-type: scroll-state;
|
||||||
|
|
||||||
|
animation:
|
||||||
|
animate-in linear forwards,
|
||||||
|
animate-out linear forwards;
|
||||||
|
animation-timeline: view(inline);
|
||||||
|
animation-range: entry, exit;
|
||||||
|
|
||||||
color: var(--gray-0);
|
color: var(--gray-0);
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: block;
|
display: block;
|
||||||
background: linear-gradient(185deg, transparent 20%, var(--surface-2) 90%), linear-gradient(transparent 50%, #0007 75%);
|
background: linear-gradient(182.5deg, transparent 20%, var(--surface-2) 90%),
|
||||||
|
linear-gradient(transparent 50%, #0007 75%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&::scroll-marker {
|
&::scroll-marker {
|
||||||
display: block;
|
display: block;
|
||||||
content: ' ';
|
content: " ";
|
||||||
|
|
||||||
inline-size: 15em;
|
inline-size: 5rem;
|
||||||
aspect-ratio: 3 / 5;
|
aspect-ratio: 3 / 5;
|
||||||
|
|
||||||
background: var(--thumb-image) center / cover no-repeat;
|
background: var(--thumb-image) center / cover no-repeat;
|
||||||
background-color: cornflowerblue;
|
background-color: cornflowerblue;
|
||||||
border-radius: var(--radius-3);
|
border-radius: var(--radius-2);
|
||||||
|
|
||||||
transform: scale(.333333);
|
transform: scale(1);
|
||||||
transition: .3s;
|
transform-origin: top left;
|
||||||
|
transition: 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::scroll-marker:target-current {
|
&::scroll-marker:target-current {
|
||||||
/* outline: 1px solid white; */
|
/* outline: 1px solid white; */
|
||||||
position: absolute;
|
transform: translate(calc(-0cqi - (6rem * (var(--__i) - 1))), -29rem)
|
||||||
top: -29em;
|
scale(3);
|
||||||
left: 2em;
|
|
||||||
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +124,7 @@
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
|
@ -129,3 +147,26 @@
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes animate-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
/* 80% {
|
||||||
|
opacity: 0;
|
||||||
|
} */
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes animate-out {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,10 +12,8 @@ export function Hero(props: HeroProps) {
|
||||||
const slug = createMemo(() => createSlug(entry()));
|
const slug = createMemo(() => createSlug(entry()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`${css.container} ${props.class ?? ''}`}>
|
<div class={`${css.container} ${props.class ?? ""}`}>
|
||||||
<For each={props.entries}>{
|
<For each={props.entries}>{(entry) => <Page entry={entry} />}</For>
|
||||||
entry => <Page entry={entry} />
|
|
||||||
}</For>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,15 +22,19 @@ const Page: Component<{ entry: Entry }> = (props) => {
|
||||||
const slug = createMemo(() => createSlug(props.entry));
|
const slug = createMemo(() => createSlug(props.entry));
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log(props.entry);
|
// console.log(props.entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`${css.page}`} style={{ '--thumb-image': `url(${props.entry.thumbnail})` }}>
|
<div
|
||||||
|
class={`${css.page}`}
|
||||||
|
style={{ "--thumb-image": `url(${props.entry.thumbnail})` }}
|
||||||
|
>
|
||||||
<h2 class={css.title}>{props.entry.title}</h2>
|
<h2 class={css.title}>{props.entry.title}</h2>
|
||||||
|
|
||||||
<a class={css.cta} href={`/watch/${slug()}`}>Continue</a>
|
<a class={css.cta} href={`/watch/${slug()}`}>
|
||||||
|
Continue
|
||||||
|
</a>
|
||||||
|
|
||||||
<img src={props.entry.thumbnail} class={css.thumbnail} />
|
<img src={props.entry.thumbnail} class={css.thumbnail} />
|
||||||
<img src={props.entry.image} class={css.background} />
|
<img src={props.entry.image} class={css.background} />
|
||||||
|
@ -52,7 +54,7 @@ const Page: Component<{ entry: Entry }> = (props) => {
|
||||||
</Index>
|
</Index>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<p class={css.summary}>{props.entry.synopsis}</p>
|
<p class={css.summary}>{props.entry.overview}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,25 @@
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid: auto auto / auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"heading metadata"
|
||||||
|
"list list";
|
||||||
|
justify-content: space-between;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
|
grid-area: heading;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
grid-area: metadata;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
|
grid-area: list;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
|
@ -36,12 +47,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
order: 0;
|
|
||||||
inline-size: 15cqi;
|
inline-size: 15cqi;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
order: 11;
|
|
||||||
inline-size: 100cqi;
|
inline-size: 100cqi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,8 +61,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
|
|
||||||
order: calc(var(--sibling-count) - var(--sibling-index));
|
z-index: calc(var(--sibling-count) - var(--sibling-index));
|
||||||
z-index: var(--sibling-index);
|
|
||||||
|
|
||||||
&:has(> :hover, > :focus-within) {
|
&:has(> :hover, > :focus-within) {
|
||||||
z-index: calc(var(--sibling-count) + 1);
|
z-index: calc(var(--sibling-count) + 1);
|
||||||
|
|
|
@ -15,6 +15,8 @@ export function List<T>(props: ListProps<T>) {
|
||||||
{props.label}
|
{props.label}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
|
<sub class={css.metadata}>{props.items.length} result(s)</sub>
|
||||||
|
|
||||||
<ul class={css.list}>
|
<ul class={css.list}>
|
||||||
<Index each={props.items}>
|
<Index each={props.items}>
|
||||||
{(item) => <li>{props.children(item)}</li>}
|
{(item) => <li>{props.children(item)}</li>}
|
||||||
|
|
|
@ -5,14 +5,27 @@ import createClient from "openapi-fetch";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { Entry } from "../types";
|
import { Entry } from "../types";
|
||||||
|
|
||||||
type ItemImageType = "Primary" | "Art" | "Backdrop" | "Banner" | "Logo" | "Thumb" | "Disc" | "Box" | "Screenshot" | "Menu" | "Chapter" | "BoxRear" | "Profile";
|
type ItemImageType =
|
||||||
|
| "Primary"
|
||||||
|
| "Art"
|
||||||
|
| "Backdrop"
|
||||||
|
| "Banner"
|
||||||
|
| "Logo"
|
||||||
|
| "Thumb"
|
||||||
|
| "Disc"
|
||||||
|
| "Box"
|
||||||
|
| "Screenshot"
|
||||||
|
| "Menu"
|
||||||
|
| "Chapter"
|
||||||
|
| "BoxRear"
|
||||||
|
| "Profile";
|
||||||
|
|
||||||
const baseUrl = process.env.JELLYFIN_BASE_URL;
|
const baseUrl = process.env.JELLYFIN_BASE_URL;
|
||||||
const client = createClient<paths>({
|
const client = createClient<paths>({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`,
|
Authorization: `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`,
|
||||||
'Content-Type': 'application/json; profile="CamelCase"',
|
"Content-Type": 'application/json; profile="CamelCase"',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,21 +34,24 @@ export const TEST = query(async () => {
|
||||||
const itemId = "919dfa97-e4da-d275-8a92-5d056e590a28";
|
const itemId = "919dfa97-e4da-d275-8a92-5d056e590a28";
|
||||||
const seriesId = "5230ddbcd-9400-733d-c07e-5b8cb7a4f49";
|
const seriesId = "5230ddbcd-9400-733d-c07e-5b8cb7a4f49";
|
||||||
|
|
||||||
const { data: seriesData } = await client.GET("/UserItems/{itemId}/UserData", {
|
const { data: seriesData } = await client.GET(
|
||||||
params: {
|
"/UserItems/{itemId}/UserData",
|
||||||
path: { itemId: seriesId },
|
{
|
||||||
query: { userId }
|
params: {
|
||||||
}
|
path: { itemId: seriesId },
|
||||||
});
|
query: { userId },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { data: epData } = await client.GET("/UserItems/{itemId}/UserData", {
|
const { data: epData } = await client.GET("/UserItems/{itemId}/UserData", {
|
||||||
params: {
|
params: {
|
||||||
path: { itemId },
|
path: { itemId },
|
||||||
query: { userId }
|
query: { userId },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(seriesData, epData)
|
console.log(seriesData, epData);
|
||||||
}, "jellyfin.TEST");
|
}, "jellyfin.TEST");
|
||||||
|
|
||||||
export const getCurrentUser = query(async () => {
|
export const getCurrentUser = query(async () => {
|
||||||
|
@ -43,8 +59,6 @@ export const getCurrentUser = query(async () => {
|
||||||
params: {},
|
params: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(data, error, response)
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}, "jellyfin.getCurrentUser");
|
}, "jellyfin.getCurrentUser");
|
||||||
|
|
||||||
|
@ -56,138 +70,170 @@ export const listUsers = query(async () => {
|
||||||
return data ?? [];
|
return data ?? [];
|
||||||
}, "jellyfin.listUsers");
|
}, "jellyfin.listUsers");
|
||||||
|
|
||||||
export const listItems = query(async (userId: string): Promise<Entry[] | undefined> => {
|
export const listItems = query(
|
||||||
const { data, error } = await client.GET("/Items", {
|
async (userId: string): Promise<Entry[] | undefined> => {
|
||||||
params: {
|
const { data, error } = await client.GET("/Items", {
|
||||||
query: {
|
params: {
|
||||||
userId,
|
query: {
|
||||||
hasTmdbInfo: true,
|
userId,
|
||||||
recursive: true,
|
hasTmdbInfo: true,
|
||||||
includeItemTypes: ["Movie", "Series"],
|
recursive: true,
|
||||||
fields: [
|
includeItemTypes: ["Movie", "Series"],
|
||||||
"ProviderIds",
|
fields: [
|
||||||
"Genres",
|
"ProviderIds",
|
||||||
"DateLastMediaAdded",
|
"Genres",
|
||||||
"DateCreated",
|
"DateLastMediaAdded",
|
||||||
"MediaSources",
|
"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`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"jellyfin.listItems",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getRandomItem = query(
|
||||||
|
async (userId: string): Promise<Entry | undefined> =>
|
||||||
|
getRandomItems(userId, 1).then((items) => items?.at(0)),
|
||||||
|
"jellyfin.listRandomItem",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getRandomItems = query(
|
||||||
|
async (userId: string, limit: number = 10): Promise<Entry[]> => {
|
||||||
|
const { data, error } = await client.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`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||||
|
image: new URL(`/Items/${item.Id!}/Images/Backdrop`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"jellyfin.listRandomItems",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getItem = query(
|
||||||
|
async (userId: string, itemId: string): Promise<Entry | undefined> => {
|
||||||
|
const { data, error } = await client.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`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
||||||
|
image: new URL(`/Items/${itemId}/Images/Backdrop`, baseUrl),
|
||||||
|
// ...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
"jellyfin.getItem",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getItemImage = query(
|
||||||
|
async (
|
||||||
|
itemId: string,
|
||||||
|
imageType: ItemImageType,
|
||||||
|
): Promise<any | undefined> => {
|
||||||
|
const { data, error } = await client.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> => {
|
||||||
|
const { data, error, response } = await client.GET(
|
||||||
|
"/Items/{itemId}/PlaybackInfo",
|
||||||
|
{
|
||||||
|
parseAs: "text",
|
||||||
|
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
itemId,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (data === undefined) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
},
|
||||||
|
"jellyfin.getItemPlaybackInfo",
|
||||||
return data.Items?.map(item => ({
|
);
|
||||||
id: item.Id!,
|
|
||||||
title: item.Name!,
|
|
||||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
|
||||||
})) ?? [];
|
|
||||||
}, "jellyfin.listItems");
|
|
||||||
|
|
||||||
export const getRandomItem = query(async (userId: string): Promise<Entry | undefined> => getRandomItems(userId, 1).then(items => items?.at(0)), "jellyfin.listRandomItem");
|
|
||||||
|
|
||||||
export const getRandomItems = query(async (userId: string, limit: number = 10): Promise<Entry[]> => {
|
|
||||||
const { data, error } = await client.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!,
|
|
||||||
title: item.Name!,
|
|
||||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
|
||||||
image: new URL(`/Items/${item.Id!}/Images/Backdrop`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
|
||||||
})) ?? [];
|
|
||||||
}, "jellyfin.listRandomItems");
|
|
||||||
|
|
||||||
export const getItem = query(async (userId: string, itemId: string): Promise<Entry | undefined> => {
|
|
||||||
console.log('baseUrl', baseUrl);
|
|
||||||
|
|
||||||
const { data, error } = await client.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!,
|
|
||||||
title: data.Name!,
|
|
||||||
synopsis: data.Overview!,
|
|
||||||
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
|
||||||
image: new URL(`/Items/${itemId}/Images/Backdrop`, baseUrl),
|
|
||||||
// ...data,
|
|
||||||
};
|
|
||||||
}, "jellyfin.getItem");
|
|
||||||
|
|
||||||
export const getItemImage = query(async (itemId: string, imageType: ItemImageType): Promise<any | undefined> => {
|
|
||||||
const { data, error } = await client.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> => {
|
|
||||||
const { data, error, response } = await client.GET("/Items/{itemId}/PlaybackInfo", {
|
|
||||||
parseAs: 'text',
|
|
||||||
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
itemId,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}, "jellyfin.getItemPlaybackInfo");
|
|
||||||
|
|
||||||
export const queryItems = query(async () => {
|
export const queryItems = query(async () => {
|
||||||
const { data, error } = await client.GET("/Items", {
|
const { data, error } = await client.GET("/Items", {
|
||||||
|
@ -204,35 +250,57 @@ export const queryItems = query(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
}, "jellyfin.queryItems");
|
||||||
|
|
||||||
}, 'jellyfin.queryItems');
|
export const getContinueWatching = query(
|
||||||
|
async (userId: string): Promise<Entry[]> => {
|
||||||
export const getContinueWatching = query(async (userId: string): Promise<Entry[]> => {
|
const { data, error } = await client.GET("/UserItems/Resume", {
|
||||||
const { data, error } = await client.GET("/UserItems/Resume", {
|
params: {
|
||||||
params: {
|
query: {
|
||||||
query: {
|
userId,
|
||||||
userId,
|
mediaTypes: ["Video"],
|
||||||
mediaTypes: ["Video"],
|
// fields: ["ProviderIds", "Genres"],
|
||||||
// fields: ["ProviderIds", "Genres"],
|
// includeItemTypes: ["Series", "Movie"]
|
||||||
// includeItemTypes: ["Series", "Movie"]
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(data?.Items) !== true) {
|
if (Array.isArray(data?.Items) !== true) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueIds = new Set<string>(data.Items.map(item => item.Type === 'Episode' ? item.SeriesId! : item.Id!));
|
const uniqueIds = new Set<string>(
|
||||||
const results = await Promise.allSettled(uniqueIds.values().map(id => getItem(userId, id)).toArray());
|
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);
|
assertNoErrors(results);
|
||||||
|
|
||||||
return results.filter((result): result is PromiseFulfilledResult<Entry> => result.value !== undefined).map(({ value }) => value);
|
return results
|
||||||
}, "jellyfin.continueWatching");
|
.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>[] {
|
function assertNoErrors<T>(
|
||||||
if (results.some(({ status }) => status !== 'fulfilled')) {
|
results: PromiseSettledResult<T>[],
|
||||||
throw new Error('one or more promices failed', { cause: results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map(r => r.reason) });
|
): 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),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
287
src/features/content/apis/tmdb.not.generated.ts
Normal file
287
src/features/content/apis/tmdb.not.generated.ts
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
export interface paths {
|
||||||
|
"/4/account/{account_object_id}/movie/recommendations": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["GetMovieRecommendations"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/3/movie/{movie_id}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["GetMovieById"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/3/series/{series_id}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["GetSeriesById"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/3/discover/movie": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["GetDiscovery_Movie"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/3/discover/tv": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["GetDiscovery_Serie"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type webhooks = Record<string, never>;
|
||||||
|
export interface components {
|
||||||
|
schemas: {
|
||||||
|
paginatedQueryResult: {
|
||||||
|
page: number;
|
||||||
|
results: components["schemas"]["entry"][];
|
||||||
|
total_pages: number;
|
||||||
|
total_results: number;
|
||||||
|
};
|
||||||
|
entry: {
|
||||||
|
backdrop_path: string;
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
overview: string;
|
||||||
|
poster_path: string;
|
||||||
|
media_type: string;
|
||||||
|
adult: boolean;
|
||||||
|
original_language: string;
|
||||||
|
gerne_ids: number[];
|
||||||
|
popularity: number;
|
||||||
|
release_date: string;
|
||||||
|
video: boolean;
|
||||||
|
vote_average: number;
|
||||||
|
vote_count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: never;
|
||||||
|
parameters: never;
|
||||||
|
requestBodies: never;
|
||||||
|
headers: never;
|
||||||
|
pathItems: never;
|
||||||
|
}
|
||||||
|
export type $defs = Record<string, never>;
|
||||||
|
export interface operations {
|
||||||
|
GetMovieRecommendations: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
page?: number;
|
||||||
|
language?: string;
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
account_object_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["paginatedQueryResult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
GetDiscovery_Movie: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
page?: number;
|
||||||
|
language?: string;
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["paginatedQueryResult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
GetDiscovery_Serie: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
page?: number;
|
||||||
|
language?: string;
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["paginatedQueryResult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
GetMovieById: {
|
||||||
|
parameters: {
|
||||||
|
query?: {};
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
movie_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["entry"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
GetSeriesById: {
|
||||||
|
parameters: {
|
||||||
|
query?: {};
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
series_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["entry"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
91
src/features/content/apis/tmdb.ts
Normal file
91
src/features/content/apis/tmdb.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import createClient from "openapi-fetch";
|
||||||
|
import { query } from "@solidjs/router";
|
||||||
|
import { Entry } from "../types";
|
||||||
|
import { paths } from "./tmdb.not.generated";
|
||||||
|
|
||||||
|
const baseUrl = process.env.TMDB_BASE_URL;
|
||||||
|
const client = createClient<paths>({
|
||||||
|
baseUrl,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.TMDB_TOKEN}`,
|
||||||
|
"Content-Type": "application/json;",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getEntry = query(
|
||||||
|
async (id: string): Promise<Entry | undefined> => {
|
||||||
|
const { data } = await client.GET("/3/movie/{movie_id}", {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
movie_id: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(data.id),
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getRecommendations = query(async (): Promise<Entry[]> => {
|
||||||
|
const account_object_id = "6668b76e419b28ec1a1c5aab";
|
||||||
|
|
||||||
|
const { data } = await client.GET(
|
||||||
|
"/4/account/{account_object_id}/movie/recommendations",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
path: { account_object_id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data?.results.map(
|
||||||
|
({ id, title, overview, poster_path, backdrop_path }) => ({
|
||||||
|
id: String(id),
|
||||||
|
title,
|
||||||
|
overview,
|
||||||
|
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||||
|
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, "tmdb.getRecommendations");
|
||||||
|
|
||||||
|
export const getDiscovery = query(async (): Promise<Entry[]> => {
|
||||||
|
const [{ data: movies }, { data: series }] = await Promise.all([
|
||||||
|
client.GET("/3/discover/movie"),
|
||||||
|
client.GET("/3/discover/movie"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (movies === undefined || series === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log({ movies: movies.results.length, series: series.results.length });
|
||||||
|
|
||||||
|
return movies?.results
|
||||||
|
.slice(0, 9)
|
||||||
|
.concat(series?.results.slice(0, 9))
|
||||||
|
.map(({ id, title, overview, poster_path, backdrop_path }) => ({
|
||||||
|
id: String(id),
|
||||||
|
title,
|
||||||
|
overview,
|
||||||
|
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||||
|
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||||
|
}));
|
||||||
|
}, "tmdb.getDiscovery");
|
|
@ -4,6 +4,11 @@ import type { Category, Entry } from "./types";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { entries } from "./data";
|
import { entries } from "./data";
|
||||||
import { getContinueWatching, getItem, getRandomItems } from "./apis/jellyfin";
|
import { getContinueWatching, getItem, getRandomItems } from "./apis/jellyfin";
|
||||||
|
import {
|
||||||
|
getDiscovery,
|
||||||
|
getRecommendations,
|
||||||
|
getEntry as getTmdbEntry,
|
||||||
|
} from "./apis/tmdb";
|
||||||
|
|
||||||
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||||
|
|
||||||
|
@ -13,65 +18,19 @@ export const getHighlights = () => getContinueWatching(jellyfinUserId);
|
||||||
export const listCategories = query(async (): Promise<Category[]> => {
|
export const listCategories = query(async (): Promise<Category[]> => {
|
||||||
return [
|
return [
|
||||||
// { label: "Continue", entries: await getContinueWatching(jellyfinUserId) },
|
// { label: "Continue", entries: await getContinueWatching(jellyfinUserId) },
|
||||||
|
{
|
||||||
|
label: "Recommendations (For you?)",
|
||||||
|
entries: await getRecommendations(),
|
||||||
|
},
|
||||||
|
{ label: "Discover", entries: await getDiscovery() },
|
||||||
{ label: "Random", entries: await getRandomItems(jellyfinUserId) },
|
{ label: "Random", entries: await getRandomItems(jellyfinUserId) },
|
||||||
{
|
|
||||||
label: "Popular",
|
|
||||||
entries: [
|
|
||||||
entries.get("1")!,
|
|
||||||
entries.get("2")!,
|
|
||||||
entries.get("3")!,
|
|
||||||
entries.get("4")!,
|
|
||||||
entries.get("1")!,
|
|
||||||
entries.get("2")!,
|
|
||||||
entries.get("3")!,
|
|
||||||
entries.get("4")!,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Drama",
|
|
||||||
entries: [
|
|
||||||
entries.get("5")!,
|
|
||||||
entries.get("6")!,
|
|
||||||
entries.get("7")!,
|
|
||||||
entries.get("8")!,
|
|
||||||
entries.get("1")!,
|
|
||||||
entries.get("2")!,
|
|
||||||
entries.get("3")!,
|
|
||||||
entries.get("4")!,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Now streaming",
|
|
||||||
entries: [
|
|
||||||
entries.get("1")!,
|
|
||||||
entries.get("2")!,
|
|
||||||
entries.get("3")!,
|
|
||||||
entries.get("4")!,
|
|
||||||
entries.get("1")!,
|
|
||||||
entries.get("2")!,
|
|
||||||
entries.get("3")!,
|
|
||||||
entries.get("4")!,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Sci-Fi & Fantasy",
|
|
||||||
entries: [
|
|
||||||
entries.get("9")!,
|
|
||||||
entries.get("11")!,
|
|
||||||
entries.get("12")!,
|
|
||||||
entries.get("13")!,
|
|
||||||
entries.get("1")!,
|
|
||||||
entries.get("2")!,
|
|
||||||
entries.get("3")!,
|
|
||||||
entries.get("4")!,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}, "series.categories.list");
|
}, "series.categories.list");
|
||||||
|
|
||||||
export const getEntry = query(
|
export const getEntry = query(
|
||||||
async (id: Entry["id"]): Promise<Entry | undefined> => {
|
async (id: Entry["id"]): Promise<Entry | undefined> => {
|
||||||
return getItem(jellyfinUserId, id);
|
return getTmdbEntry(id);
|
||||||
|
// return getItem(jellyfinUserId, id);
|
||||||
},
|
},
|
||||||
"series.get",
|
"series.get",
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
label: string;
|
label: string;
|
||||||
entries: Entry[];
|
entries: Entry[];
|
||||||
|
@ -7,7 +6,7 @@ export interface Category {
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
synopsis?: string;
|
overview?: string;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
sources?: Entry.Source[];
|
sources?: Entry.Source[];
|
||||||
thumbnail?: URL | string;
|
thumbnail?: URL | string;
|
||||||
|
@ -30,4 +29,3 @@ export namespace Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
grid: 100% / 100%;
|
grid: 100% / 100%;
|
||||||
place-items: start center;
|
place-items: start center;
|
||||||
position: relative;
|
position: relative;
|
||||||
inline-size: clamp(15em, 20vw, 30em);
|
inline-size: clamp(15em, 15cqi, 25em);
|
||||||
aspect-ratio: var(--ratio-portrait);
|
aspect-ratio: var(--ratio-portrait);
|
||||||
transform: translateY(calc(-2 * var(--padding)));
|
transform: translateY(calc(-2 * var(--padding)));
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
@ -21,20 +21,28 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
box-shadow: var(--shadow-2);
|
box-shadow: var(--shadow-2);
|
||||||
background:
|
background: radial-gradient(
|
||||||
radial-gradient(circle at 25% 30%, #7772, #7774 1em, transparent 1em),
|
circle at 25% 30%,
|
||||||
radial-gradient(circle at 85% 15%, #7772, #7774 1em, transparent 1em),
|
#7772,
|
||||||
linear-gradient(165deg, transparent 60%, #555 60%, #333),
|
#7774 1em,
|
||||||
radial-gradient(
|
transparent 1em
|
||||||
ellipse 5em 2.25em at 0.5em calc(50% - 1em), #333 100%, transparent 100%
|
),
|
||||||
),
|
radial-gradient(circle at 85% 15%, #7772, #7774 1em, transparent 1em),
|
||||||
radial-gradient(
|
linear-gradient(165deg, transparent 60%, #555 60%, #333),
|
||||||
ellipse 5em 2.25em at calc(100% - 0.5em) calc(50% + 1em), #555 100%, transparent 100%
|
radial-gradient(
|
||||||
),
|
ellipse 5em 2.25em at 0.5em calc(50% - 1em),
|
||||||
linear-gradient(to bottom, #333 50%, #555 50%);
|
#333 100%,
|
||||||
|
transparent 100%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
ellipse 5em 2.25em at calc(100% - 0.5em) calc(50% + 1em),
|
||||||
|
#555 100%,
|
||||||
|
transparent 100%
|
||||||
|
),
|
||||||
|
linear-gradient(to bottom, #333 50%, #555 50%);
|
||||||
|
|
||||||
transform-origin: 50% 0;
|
transform-origin: 50% 0;
|
||||||
transform: scale(1.1) translateY(calc(-4 * var(--padding)));
|
transform: scale(1.15) translateY(calc(-4 * var(--padding)));
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
321
src/index.css
321
src/index.css
|
@ -4,8 +4,8 @@
|
||||||
@import "open-props/normalize" layer(reset);
|
@import "open-props/normalize" layer(reset);
|
||||||
@import "open-props/durations" layer(base);
|
@import "open-props/durations" layer(base);
|
||||||
|
|
||||||
@import 'open-props/theme.light.switch.min.css' layer(tokens);
|
@import "open-props/theme.light.switch.min.css" layer(tokens);
|
||||||
@import 'open-props/theme.dark.switch.min.css' layer(tokens);
|
@import "open-props/theme.dark.switch.min.css" layer(tokens);
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
|
@ -44,13 +44,13 @@
|
||||||
|
|
||||||
@layer reset {
|
@layer reset {
|
||||||
@property --sibling-index {
|
@property --sibling-index {
|
||||||
syntax: '<integer>';
|
syntax: "<integer>";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 1;
|
initial-value: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property --sibling-count {
|
@property --sibling-count {
|
||||||
syntax: '<integer>';
|
syntax: "<integer>";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0;
|
initial-value: 0;
|
||||||
}
|
}
|
||||||
|
@ -94,26 +94,157 @@
|
||||||
:nth-child(10) {
|
:nth-child(10) {
|
||||||
--sibling-index: 10;
|
--sibling-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
:nth-child(11) {
|
:nth-child(11) {
|
||||||
--sibling-index: 11;
|
--sibling-index: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
:nth-child(12) {
|
:nth-child(12) {
|
||||||
--sibling-index: 12;
|
--sibling-index: 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
:nth-child(13) {
|
:nth-child(13) {
|
||||||
--sibling-index: 13;
|
--sibling-index: 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
:nth-child(14) {
|
:nth-child(14) {
|
||||||
--sibling-index: 14;
|
--sibling-index: 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
:nth-child(15) {
|
:nth-child(15) {
|
||||||
--sibling-index: 15;
|
--sibling-index: 15;
|
||||||
}
|
}
|
||||||
|
:nth-child(16) {
|
||||||
|
--sibling-index: 16;
|
||||||
|
}
|
||||||
|
:nth-child(17) {
|
||||||
|
--sibling-index: 17;
|
||||||
|
}
|
||||||
|
:nth-child(18) {
|
||||||
|
--sibling-index: 18;
|
||||||
|
}
|
||||||
|
:nth-child(19) {
|
||||||
|
--sibling-index: 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(20) {
|
||||||
|
--sibling-index: 20;
|
||||||
|
}
|
||||||
|
:nth-child(21) {
|
||||||
|
--sibling-index: 21;
|
||||||
|
}
|
||||||
|
:nth-child(22) {
|
||||||
|
--sibling-index: 22;
|
||||||
|
}
|
||||||
|
:nth-child(23) {
|
||||||
|
--sibling-index: 23;
|
||||||
|
}
|
||||||
|
:nth-child(24) {
|
||||||
|
--sibling-index: 24;
|
||||||
|
}
|
||||||
|
:nth-child(25) {
|
||||||
|
--sibling-index: 25;
|
||||||
|
}
|
||||||
|
:nth-child(26) {
|
||||||
|
--sibling-index: 26;
|
||||||
|
}
|
||||||
|
:nth-child(27) {
|
||||||
|
--sibling-index: 27;
|
||||||
|
}
|
||||||
|
:nth-child(28) {
|
||||||
|
--sibling-index: 28;
|
||||||
|
}
|
||||||
|
:nth-child(29) {
|
||||||
|
--sibling-index: 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(30) {
|
||||||
|
--sibling-index: 30;
|
||||||
|
}
|
||||||
|
:nth-child(31) {
|
||||||
|
--sibling-index: 31;
|
||||||
|
}
|
||||||
|
:nth-child(32) {
|
||||||
|
--sibling-index: 32;
|
||||||
|
}
|
||||||
|
:nth-child(33) {
|
||||||
|
--sibling-index: 33;
|
||||||
|
}
|
||||||
|
:nth-child(34) {
|
||||||
|
--sibling-index: 34;
|
||||||
|
}
|
||||||
|
:nth-child(35) {
|
||||||
|
--sibling-index: 35;
|
||||||
|
}
|
||||||
|
:nth-child(36) {
|
||||||
|
--sibling-index: 36;
|
||||||
|
}
|
||||||
|
:nth-child(37) {
|
||||||
|
--sibling-index: 37;
|
||||||
|
}
|
||||||
|
:nth-child(38) {
|
||||||
|
--sibling-index: 38;
|
||||||
|
}
|
||||||
|
:nth-child(39) {
|
||||||
|
--sibling-index: 39;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(40) {
|
||||||
|
--sibling-index: 40;
|
||||||
|
}
|
||||||
|
:nth-child(41) {
|
||||||
|
--sibling-index: 41;
|
||||||
|
}
|
||||||
|
:nth-child(42) {
|
||||||
|
--sibling-index: 42;
|
||||||
|
}
|
||||||
|
:nth-child(43) {
|
||||||
|
--sibling-index: 43;
|
||||||
|
}
|
||||||
|
:nth-child(44) {
|
||||||
|
--sibling-index: 44;
|
||||||
|
}
|
||||||
|
:nth-child(45) {
|
||||||
|
--sibling-index: 45;
|
||||||
|
}
|
||||||
|
:nth-child(46) {
|
||||||
|
--sibling-index: 46;
|
||||||
|
}
|
||||||
|
:nth-child(47) {
|
||||||
|
--sibling-index: 47;
|
||||||
|
}
|
||||||
|
:nth-child(48) {
|
||||||
|
--sibling-index: 48;
|
||||||
|
}
|
||||||
|
:nth-child(49) {
|
||||||
|
--sibling-index: 49;
|
||||||
|
}
|
||||||
|
|
||||||
|
:nth-child(50) {
|
||||||
|
--sibling-index: 50;
|
||||||
|
}
|
||||||
|
:nth-child(51) {
|
||||||
|
--sibling-index: 51;
|
||||||
|
}
|
||||||
|
:nth-child(52) {
|
||||||
|
--sibling-index: 52;
|
||||||
|
}
|
||||||
|
:nth-child(53) {
|
||||||
|
--sibling-index: 53;
|
||||||
|
}
|
||||||
|
:nth-child(54) {
|
||||||
|
--sibling-index: 54;
|
||||||
|
}
|
||||||
|
:nth-child(55) {
|
||||||
|
--sibling-index: 55;
|
||||||
|
}
|
||||||
|
:nth-child(56) {
|
||||||
|
--sibling-index: 56;
|
||||||
|
}
|
||||||
|
:nth-child(57) {
|
||||||
|
--sibling-index: 57;
|
||||||
|
}
|
||||||
|
:nth-child(58) {
|
||||||
|
--sibling-index: 58;
|
||||||
|
}
|
||||||
|
:nth-child(59) {
|
||||||
|
--sibling-index: 59;
|
||||||
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(1)) > * {
|
:has(> :last-child:nth-child(1)) > * {
|
||||||
--sibling-count: 1;
|
--sibling-count: 1;
|
||||||
|
@ -154,24 +285,186 @@
|
||||||
:has(> :last-child:nth-child(10)) > * {
|
:has(> :last-child:nth-child(10)) > * {
|
||||||
--sibling-count: 10;
|
--sibling-count: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(11)) > * {
|
:has(> :last-child:nth-child(11)) > * {
|
||||||
--sibling-count: 11;
|
--sibling-count: 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(12)) > * {
|
:has(> :last-child:nth-child(12)) > * {
|
||||||
--sibling-count: 12;
|
--sibling-count: 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(13)) > * {
|
:has(> :last-child:nth-child(13)) > * {
|
||||||
--sibling-count: 13;
|
--sibling-count: 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(14)) > * {
|
:has(> :last-child:nth-child(14)) > * {
|
||||||
--sibling-count: 14;
|
--sibling-count: 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(> :last-child:nth-child(15)) > * {
|
:has(> :last-child:nth-child(15)) > * {
|
||||||
--sibling-count: 15;
|
--sibling-count: 15;
|
||||||
}
|
}
|
||||||
|
:has(> :last-child:nth-child(16)) > * {
|
||||||
|
--sibling-count: 16;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(17)) > * {
|
||||||
|
--sibling-count: 17;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(18)) > * {
|
||||||
|
--sibling-count: 18;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(19)) > * {
|
||||||
|
--sibling-count: 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
:has(> :last-child:nth-child(20)) > * {
|
||||||
|
--sibling-count: 20;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(21)) > * {
|
||||||
|
--sibling-count: 21;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(22)) > * {
|
||||||
|
--sibling-count: 22;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(23)) > * {
|
||||||
|
--sibling-count: 23;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(24)) > * {
|
||||||
|
--sibling-count: 24;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(25)) > * {
|
||||||
|
--sibling-count: 25;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(26)) > * {
|
||||||
|
--sibling-count: 26;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(27)) > * {
|
||||||
|
--sibling-count: 27;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(28)) > * {
|
||||||
|
--sibling-count: 28;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(29)) > * {
|
||||||
|
--sibling-count: 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
:has(> :last-child:nth-child(30)) > * {
|
||||||
|
--sibling-count: 30;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(31)) > * {
|
||||||
|
--sibling-count: 31;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(32)) > * {
|
||||||
|
--sibling-count: 32;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(33)) > * {
|
||||||
|
--sibling-count: 33;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(34)) > * {
|
||||||
|
--sibling-count: 34;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(35)) > * {
|
||||||
|
--sibling-count: 35;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(36)) > * {
|
||||||
|
--sibling-count: 36;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(37)) > * {
|
||||||
|
--sibling-count: 37;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(38)) > * {
|
||||||
|
--sibling-count: 38;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(39)) > * {
|
||||||
|
--sibling-count: 39;
|
||||||
|
}
|
||||||
|
|
||||||
|
:has(> :last-child:nth-child(40)) > * {
|
||||||
|
--sibling-count: 40;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(41)) > * {
|
||||||
|
--sibling-count: 41;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(42)) > * {
|
||||||
|
--sibling-count: 42;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(43)) > * {
|
||||||
|
--sibling-count: 43;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(44)) > * {
|
||||||
|
--sibling-count: 44;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(45)) > * {
|
||||||
|
--sibling-count: 45;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(46)) > * {
|
||||||
|
--sibling-count: 46;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(47)) > * {
|
||||||
|
--sibling-count: 47;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(48)) > * {
|
||||||
|
--sibling-count: 48;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(49)) > * {
|
||||||
|
--sibling-count: 49;
|
||||||
|
}
|
||||||
|
|
||||||
|
:has(> :last-child:nth-child(50)) > * {
|
||||||
|
--sibling-count: 50;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(51)) > * {
|
||||||
|
--sibling-count: 51;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(52)) > * {
|
||||||
|
--sibling-count: 52;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(53)) > * {
|
||||||
|
--sibling-count: 53;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(54)) > * {
|
||||||
|
--sibling-count: 54;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(55)) > * {
|
||||||
|
--sibling-count: 55;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(56)) > * {
|
||||||
|
--sibling-count: 56;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(57)) > * {
|
||||||
|
--sibling-count: 57;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(58)) > * {
|
||||||
|
--sibling-count: 58;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(59)) > * {
|
||||||
|
--sibling-count: 59;
|
||||||
|
}
|
||||||
|
|
||||||
|
:has(> :last-child:nth-child(60)) > * {
|
||||||
|
--sibling-count: 60;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(61)) > * {
|
||||||
|
--sibling-count: 61;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(62)) > * {
|
||||||
|
--sibling-count: 62;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(63)) > * {
|
||||||
|
--sibling-count: 63;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(64)) > * {
|
||||||
|
--sibling-count: 64;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(65)) > * {
|
||||||
|
--sibling-count: 65;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(66)) > * {
|
||||||
|
--sibling-count: 66;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(67)) > * {
|
||||||
|
--sibling-count: 67;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(68)) > * {
|
||||||
|
--sibling-count: 68;
|
||||||
|
}
|
||||||
|
:has(> :last-child:nth-child(69)) > * {
|
||||||
|
--sibling-count: 69;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,7 @@
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { Overview } from "~/features/overview";
|
import { Overview } from "~/features/overview";
|
||||||
import {
|
import { getHighlights, listCategories } from "~/features/content";
|
||||||
getHighlights,
|
|
||||||
listCategories,
|
|
||||||
} from "~/features/content";
|
|
||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
|
|
||||||
export const route = {
|
export const route = {
|
||||||
|
|
|
@ -22,7 +22,8 @@ const healUrl = query(async (slug: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw redirect(`/watch/${actualSlug}`);
|
// Not entirely sure a permanent redirect is what we want in this case
|
||||||
|
throw redirect(`/watch/${actualSlug}`, { status: 308 });
|
||||||
}, "watch.heal");
|
}, "watch.heal");
|
||||||
|
|
||||||
interface ItemParams extends Params {
|
interface ItemParams extends Params {
|
||||||
|
|
|
@ -6,22 +6,24 @@ const CHUNK_SIZE = 1 * 1e6; // 1MB
|
||||||
export const GET = async ({ request, ...event }: APIEvent) => {
|
export const GET = async ({ request, ...event }: APIEvent) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const range = request.headers.get('range');
|
const range = request.headers.get("range");
|
||||||
|
|
||||||
if (range === null) {
|
if (range === null) {
|
||||||
return new Response('Requires Range header', { status: 400 })
|
return new Response("Requires Range header", { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const video = Bun.file(import.meta.dirname + '/SampleVideo_1280x720_10mb.mp4');
|
const video = Bun.file(
|
||||||
|
import.meta.dirname + "/SampleVideo_1280x720_10mb.mp4",
|
||||||
|
);
|
||||||
|
|
||||||
if ((await video.exists()) !== true) {
|
if ((await video.exists()) !== true) {
|
||||||
return new Response('File not found', { status: 404 });
|
return new Response("File not found", { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoSize = video.size;
|
const videoSize = video.size;
|
||||||
|
|
||||||
const start = Number.parseInt(range.replace(/\D/g, ''));
|
const start = Number.parseInt(range.replace(/\D/g, ""));
|
||||||
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
|
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
|
||||||
const contentLength = end - start + 1;
|
const contentLength = end - start + 1;
|
||||||
|
|
||||||
|
@ -40,8 +42,7 @@ export const GET = async ({ request, ...event }: APIEvent) => {
|
||||||
// 'Content-type': 'video/mp4',
|
// 'Content-type': 'video/mp4',
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue