did a lot of syle work and started search and detail pages
This commit is contained in:
parent
7c5d2a25ff
commit
275fb87eeb
23 changed files with 301155 additions and 243 deletions
15
README.md
15
README.md
|
@ -1 +1,16 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Notes
|
||||||
|
|
||||||
|
## APIS
|
||||||
|
|
||||||
|
### Generate openapi client
|
||||||
|
|
||||||
|
- path to source yml or json
|
||||||
|
- path to output, will create a typescript file
|
||||||
|
|
||||||
|
example
|
||||||
|
```bash
|
||||||
|
bunx openapi-typescript .\src\features\content\apis\api.yml -o .\src\features\content\apis\api.generated.ts
|
||||||
|
```
|
12
bun.lock
12
bun.lock
|
@ -7,6 +7,8 @@
|
||||||
"@solid-primitives/context": "^0.3.1",
|
"@solid-primitives/context": "^0.3.1",
|
||||||
"@solid-primitives/deep": "^0.3.2",
|
"@solid-primitives/deep": "^0.3.2",
|
||||||
"@solid-primitives/event-listener": "^2.4.1",
|
"@solid-primitives/event-listener": "^2.4.1",
|
||||||
|
"@solid-primitives/pagination": "^0.4.1",
|
||||||
|
"@solid-primitives/scheduled": "^1.5.1",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "^0.29.4",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
"@solidjs/start": "^1.1.4",
|
"@solidjs/start": "^1.1.4",
|
||||||
|
@ -348,6 +350,8 @@
|
||||||
|
|
||||||
"@solid-primitives/memo": ["@solid-primitives/memo@1.4.2", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.1", "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-1w2MoD/25tZOImCI+dEL08n8dyyc6sg6o0zc14sZXBBa4XSz6TDuPYgQ24r+dQerXWoP6OgZ1VZz+Mo7c1Lmvg=="],
|
"@solid-primitives/memo": ["@solid-primitives/memo@1.4.2", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.1", "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-1w2MoD/25tZOImCI+dEL08n8dyyc6sg6o0zc14sZXBBa4XSz6TDuPYgQ24r+dQerXWoP6OgZ1VZz+Mo7c1Lmvg=="],
|
||||||
|
|
||||||
|
"@solid-primitives/pagination": ["@solid-primitives/pagination@0.4.1", "", { "dependencies": { "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Q/ZDa8qjKUCRW4Fvdunk8qlgf1geUAz5nfcJbI0sbIYf/9dhURMvESugpqFeF+/GJo784jNcfUwg5253/I7tOA=="],
|
||||||
|
|
||||||
"@solid-primitives/platform": ["@solid-primitives/platform@0.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sSxcZfuUrtxcwV0vdjmGnZQcflACzMfLriVeIIWXKp8hzaS3Or3tO6EFQkTd3L8T5dTq+kTtLvPscXIpL0Wzdg=="],
|
"@solid-primitives/platform": ["@solid-primitives/platform@0.1.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-sSxcZfuUrtxcwV0vdjmGnZQcflACzMfLriVeIIWXKp8hzaS3Or3tO6EFQkTd3L8T5dTq+kTtLvPscXIpL0Wzdg=="],
|
||||||
|
|
||||||
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-QJ3bTSQOlPdHBP2m6llrT13FvVzAwZfx41lTN8lQrRwwcZoWb7kfCAjhaohPnwkAsQ6nJpLjtGfT5GOyuCA4tA=="],
|
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-QJ3bTSQOlPdHBP2m6llrT13FvVzAwZfx41lTN8lQrRwwcZoWb7kfCAjhaohPnwkAsQ6nJpLjtGfT5GOyuCA4tA=="],
|
||||||
|
@ -356,7 +360,7 @@
|
||||||
|
|
||||||
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-YJ+EveQeDv9DLqfDKfsPAAGy2x3vBruoD23yn+nD2dT84QjoBxWT1T0qA0TMFjek6/xuN3flqnHtQ4r++4zdjg=="],
|
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-YJ+EveQeDv9DLqfDKfsPAAGy2x3vBruoD23yn+nD2dT84QjoBxWT1T0qA0TMFjek6/xuN3flqnHtQ4r++4zdjg=="],
|
||||||
|
|
||||||
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-RVw24IRNh1FQ4DCMb3OahB70tXIwc5vH8nhR4nNPsXwUPQeuOkLsDI5BlxaPk0vyZgqw9lDpufgI3HnPwplgDw=="],
|
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-WKg/zvAyDIgQ/Xo48YaUY7ISaPyWTZNDzIVWP2R84CuLH+nZN/2O0aFn/gQlWY6y/Bfi/LdDt6Og2/PRzPY7mA=="],
|
||||||
|
|
||||||
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.0.8", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg=="],
|
"@solid-primitives/static-store": ["@solid-primitives/static-store@0.0.8", "", { "dependencies": { "@solid-primitives/utils": "^6.2.3" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg=="],
|
||||||
|
|
||||||
|
@ -1722,10 +1726,14 @@
|
||||||
|
|
||||||
"@solid-devtools/debugger/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
"@solid-devtools/debugger/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||||
|
|
||||||
|
"@solid-devtools/debugger/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-RVw24IRNh1FQ4DCMb3OahB70tXIwc5vH8nhR4nNPsXwUPQeuOkLsDI5BlxaPk0vyZgqw9lDpufgI3HnPwplgDw=="],
|
||||||
|
|
||||||
"@solid-devtools/debugger/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
"@solid-devtools/debugger/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||||
|
|
||||||
"@solid-devtools/shared/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
"@solid-devtools/shared/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||||
|
|
||||||
|
"@solid-devtools/shared/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-RVw24IRNh1FQ4DCMb3OahB70tXIwc5vH8nhR4nNPsXwUPQeuOkLsDI5BlxaPk0vyZgqw9lDpufgI3HnPwplgDw=="],
|
||||||
|
|
||||||
"@solid-devtools/shared/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
"@solid-devtools/shared/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||||
|
|
||||||
"@solid-primitives/bounds/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
"@solid-primitives/bounds/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||||
|
@ -1744,8 +1752,6 @@
|
||||||
|
|
||||||
"@solid-primitives/media/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
"@solid-primitives/media/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||||
|
|
||||||
"@solid-primitives/memo/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-WKg/zvAyDIgQ/Xo48YaUY7ISaPyWTZNDzIVWP2R84CuLH+nZN/2O0aFn/gQlWY6y/Bfi/LdDt6Og2/PRzPY7mA=="],
|
|
||||||
|
|
||||||
"@solid-primitives/refs/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
"@solid-primitives/refs/@solid-primitives/utils": ["@solid-primitives/utils@6.3.0", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-e7hTlJ1Ywh2+g/Qug+n4L1mpfxsikoIS4/sHE2EK9WatQt8UJqop/vE6bsLnXlU1xuhb/jo94Ah5Y27rd4wP7A=="],
|
||||||
|
|
||||||
"@solid-primitives/resize-observer/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
"@solid-primitives/resize-observer/@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.0", "", { "dependencies": { "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TSfR1PNTfojFEYGSxSMCnUhXsaYWBo4p+cm73QmWODa9YnaQAk6PB7VjzG2bOT2D817VlvuOqTj0Qdq+MZrdGg=="],
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
"@solid-primitives/context": "^0.3.1",
|
"@solid-primitives/context": "^0.3.1",
|
||||||
"@solid-primitives/deep": "^0.3.2",
|
"@solid-primitives/deep": "^0.3.2",
|
||||||
"@solid-primitives/event-listener": "^2.4.1",
|
"@solid-primitives/event-listener": "^2.4.1",
|
||||||
|
"@solid-primitives/pagination": "^0.4.1",
|
||||||
|
"@solid-primitives/scheduled": "^1.5.1",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "^0.29.4",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
"@solidjs/start": "^1.1.4",
|
"@solidjs/start": "^1.1.4",
|
||||||
|
|
31
src/components/details/details.module.css
Normal file
31
src/components/details/details.module.css
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
.container {
|
||||||
|
isolation: isolate;
|
||||||
|
display: block grid;
|
||||||
|
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: relative;
|
||||||
|
block-size: 80cqb;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: block;
|
||||||
|
background: linear-gradient(182.5deg, transparent 20%, var(--surface-2) 90%),
|
||||||
|
linear-gradient(transparent 50%, #0007 75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .background {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
19
src/components/details/details.tsx
Normal file
19
src/components/details/details.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Component } from 'solid-js';
|
||||||
|
import { Entry } from '~/features/content';
|
||||||
|
import css from './details.module.css';
|
||||||
|
|
||||||
|
interface DetailsProps {
|
||||||
|
entry: Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Details: Component<DetailsProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<div class={css.container}>
|
||||||
|
<header class={css.header}>
|
||||||
|
<img class={css.background} src={props.entry.image} />
|
||||||
|
|
||||||
|
<h1>{props.entry.title}</h1>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
3
src/components/details/index.ts
Normal file
3
src/components/details/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
export { Details } from './details';
|
|
@ -29,7 +29,7 @@
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
|
||||||
padding-inline: 2rem;
|
padding-inline: var(--size-6);
|
||||||
|
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: 8.333333em;
|
block-size: 8.333333em;
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
align-content: end;
|
align-content: end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 2rem;
|
padding: var(--size-6);
|
||||||
block-size: 80vh;
|
block-size: 80vh;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
container-type: scroll-state;
|
container-type: scroll-state;
|
||||||
|
@ -114,6 +114,11 @@
|
||||||
text-decoration-color: var(--gray-8);
|
text-decoration-color: var(--gray-8);
|
||||||
padding: var(--size-3);
|
padding: var(--size-3);
|
||||||
font-weight: var(--font-weight-9);
|
font-weight: var(--font-weight-9);
|
||||||
|
outline-offset: var(--size-1);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 1px solid var(--gray-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
|
@ -152,9 +157,6 @@
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
/* 80% {
|
|
||||||
opacity: 0;
|
|
||||||
} */
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.container {
|
.container {
|
||||||
|
--_space: var(--size-6);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid: auto auto / auto auto;
|
grid: auto auto / auto auto;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
|
@ -6,16 +7,21 @@
|
||||||
"list list";
|
"list list";
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
|
|
||||||
|
padding-inline: var(--_space);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
grid-area: heading;
|
grid-area: heading;
|
||||||
font-size: 2em;
|
font-size: var(--size-7);
|
||||||
|
color: var(--text-1);
|
||||||
|
|
||||||
|
padding-inline: var(--_space);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata {
|
.metadata {
|
||||||
grid-area: metadata;
|
grid-area: metadata;
|
||||||
opacity: 0.6;
|
color: var(--text-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
|
@ -26,10 +32,10 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
|
|
||||||
gap: 2em;
|
gap: var(--_space);
|
||||||
padding: 12em 4em 5em;
|
padding: calc(8 * var(--_space)) calc(2 * var(--_space)) calc(2.5 * var(--_space));
|
||||||
scroll-padding: 4em;
|
scroll-padding: calc(2 * var(--_space));
|
||||||
margin: -10em -4em 0em;
|
margin: calc(-7 * var(--_space)) calc(-1 * var(--_space)) 0em;
|
||||||
|
|
||||||
overflow: visible auto;
|
overflow: visible auto;
|
||||||
scroll-snap-type: inline mandatory;
|
scroll-snap-type: inline mandatory;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import type { paths } from "./jellyfin.generated"; // generated by openapi-typescript
|
import type { paths } from "./jellyfin.generated"; // generated by openapi-typescript
|
||||||
import createClient from "openapi-fetch";
|
import createClient from "openapi-fetch";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
|
@ -20,42 +18,29 @@ type ItemImageType =
|
||||||
| "BoxRear"
|
| "BoxRear"
|
||||||
| "Profile";
|
| "Profile";
|
||||||
|
|
||||||
const baseUrl = process.env.JELLYFIN_BASE_URL;
|
const getBaseUrl = () => {
|
||||||
const client = createClient<paths>({
|
"use server";
|
||||||
baseUrl,
|
|
||||||
headers: {
|
|
||||||
Authorization: `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`,
|
|
||||||
"Content-Type": 'application/json; profile="CamelCase"',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const TEST = query(async () => {
|
return process.env.JELLYFIN_BASE_URL;
|
||||||
const userId = "a9c51af8-4bf5-4578-a99a-b4dd0ebf0763";
|
};
|
||||||
const itemId = "919dfa97-e4da-d275-8a92-5d056e590a28";
|
|
||||||
const seriesId = "5230ddbcd-9400-733d-c07e-5b8cb7a4f49";
|
|
||||||
|
|
||||||
const { data: seriesData } = await client.GET(
|
|
||||||
"/UserItems/{itemId}/UserData",
|
const getClient = () => {
|
||||||
{
|
"use server";
|
||||||
params: {
|
|
||||||
path: { itemId: seriesId },
|
return createClient<paths>({
|
||||||
query: { userId },
|
baseUrl: getBaseUrl(),
|
||||||
},
|
headers: {
|
||||||
|
Authorization: `MediaBrowser DeviceId="Streamarr", Token="${process.env.JELLYFIN_API_KEY}"`,
|
||||||
|
"Content-Type": 'application/json; profile="CamelCase"',
|
||||||
},
|
},
|
||||||
);
|
})
|
||||||
|
};
|
||||||
const { data: epData } = await client.GET("/UserItems/{itemId}/UserData", {
|
|
||||||
params: {
|
|
||||||
path: { itemId },
|
|
||||||
query: { userId },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(seriesData, epData);
|
|
||||||
}, "jellyfin.TEST");
|
|
||||||
|
|
||||||
export const getCurrentUser = query(async () => {
|
export const getCurrentUser = query(async () => {
|
||||||
const { data, error, response } = await client.GET("/Users/Public", {
|
"use server";
|
||||||
|
|
||||||
|
const { data, error, response } = await getClient().GET("/Users/Public", {
|
||||||
params: {},
|
params: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,7 +48,9 @@ export const getCurrentUser = query(async () => {
|
||||||
}, "jellyfin.getCurrentUser");
|
}, "jellyfin.getCurrentUser");
|
||||||
|
|
||||||
export const listUsers = query(async () => {
|
export const listUsers = query(async () => {
|
||||||
const { data, error } = await client.GET("/Users", {
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/Users", {
|
||||||
params: {},
|
params: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -72,7 +59,9 @@ export const listUsers = query(async () => {
|
||||||
|
|
||||||
export const listItems = query(
|
export const listItems = query(
|
||||||
async (userId: string): Promise<Entry[] | undefined> => {
|
async (userId: string): Promise<Entry[] | undefined> => {
|
||||||
const { data, error } = await client.GET("/Items", {
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/Items", {
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
userId,
|
userId,
|
||||||
|
@ -99,7 +88,7 @@ export const listItems = query(
|
||||||
// id: item.Id!,
|
// id: item.Id!,
|
||||||
id: item.ProviderIds!["Tmdb"]!,
|
id: item.ProviderIds!["Tmdb"]!,
|
||||||
title: item.Name!,
|
title: item.Name!,
|
||||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||||
})) ?? []
|
})) ?? []
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -107,14 +96,19 @@ export const listItems = query(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getRandomItem = query(
|
export const getRandomItem = query(
|
||||||
async (userId: string): Promise<Entry | undefined> =>
|
async (userId: string): Promise<Entry | undefined> => {
|
||||||
getRandomItems(userId, 1).then((items) => items?.at(0)),
|
"use server";
|
||||||
|
|
||||||
|
return getRandomItems(userId, 1).then((items) => items?.at(0));
|
||||||
|
},
|
||||||
"jellyfin.listRandomItem",
|
"jellyfin.listRandomItem",
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getRandomItems = query(
|
export const getRandomItems = query(
|
||||||
async (userId: string, limit: number = 10): Promise<Entry[]> => {
|
async (userId: string, limit: number = 20): Promise<Entry[]> => {
|
||||||
const { data, error } = await client.GET("/Items", {
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/Items", {
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
userId,
|
userId,
|
||||||
|
@ -140,8 +134,8 @@ export const getRandomItems = query(
|
||||||
// id: item.Id!,
|
// id: item.Id!,
|
||||||
id: item.ProviderIds!["Tmdb"]!,
|
id: item.ProviderIds!["Tmdb"]!,
|
||||||
title: item.Name!,
|
title: item.Name!,
|
||||||
thumbnail: new URL(`/Items/${item.Id!}/Images/Primary`, baseUrl), //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`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
image: new URL(`/Items/${item.Id!}/Images/Backdrop`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||||
})) ?? []
|
})) ?? []
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -150,7 +144,9 @@ export const getRandomItems = query(
|
||||||
|
|
||||||
export const getItem = query(
|
export const getItem = query(
|
||||||
async (userId: string, itemId: string): Promise<Entry | undefined> => {
|
async (userId: string, itemId: string): Promise<Entry | undefined> => {
|
||||||
const { data, error } = await client.GET("/Items/{itemId}", {
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/Items/{itemId}", {
|
||||||
params: {
|
params: {
|
||||||
path: {
|
path: {
|
||||||
itemId,
|
itemId,
|
||||||
|
@ -180,8 +176,8 @@ export const getItem = query(
|
||||||
id: data.ProviderIds!["Tmdb"]!,
|
id: data.ProviderIds!["Tmdb"]!,
|
||||||
title: data.Name!,
|
title: data.Name!,
|
||||||
overview: data.Overview!,
|
overview: data.Overview!,
|
||||||
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, baseUrl), //await getItemImage(data.Id!, 'Primary'),
|
thumbnail: new URL(`/Items/${itemId}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'),
|
||||||
image: new URL(`/Items/${itemId}/Images/Backdrop`, baseUrl),
|
image: new URL(`/Items/${itemId}/Images/Backdrop`, getBaseUrl()),
|
||||||
// ...data,
|
// ...data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -193,7 +189,9 @@ export const getItemImage = query(
|
||||||
itemId: string,
|
itemId: string,
|
||||||
imageType: ItemImageType,
|
imageType: ItemImageType,
|
||||||
): Promise<any | undefined> => {
|
): Promise<any | undefined> => {
|
||||||
const { data, error } = await client.GET(
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET(
|
||||||
"/Items/{itemId}/Images/{imageType}",
|
"/Items/{itemId}/Images/{imageType}",
|
||||||
{
|
{
|
||||||
parseAs: "blob",
|
parseAs: "blob",
|
||||||
|
@ -214,7 +212,9 @@ export const getItemImage = query(
|
||||||
|
|
||||||
export const getItemPlaybackInfo = query(
|
export const getItemPlaybackInfo = query(
|
||||||
async (userId: string, itemId: string): Promise<any | undefined> => {
|
async (userId: string, itemId: string): Promise<any | undefined> => {
|
||||||
const { data, error, response } = await client.GET(
|
"use server";
|
||||||
|
|
||||||
|
const { data, error, response } = await getClient().GET(
|
||||||
"/Items/{itemId}/PlaybackInfo",
|
"/Items/{itemId}/PlaybackInfo",
|
||||||
{
|
{
|
||||||
parseAs: "text",
|
parseAs: "text",
|
||||||
|
@ -236,7 +236,9 @@ export const getItemPlaybackInfo = query(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const queryItems = query(async () => {
|
export const queryItems = query(async () => {
|
||||||
const { data, error } = await client.GET("/Items", {
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/Items", {
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
mediaTypes: ["Video"],
|
mediaTypes: ["Video"],
|
||||||
|
@ -254,7 +256,9 @@ export const queryItems = query(async () => {
|
||||||
|
|
||||||
export const getContinueWatching = query(
|
export const getContinueWatching = query(
|
||||||
async (userId: string): Promise<Entry[]> => {
|
async (userId: string): Promise<Entry[]> => {
|
||||||
const { data, error } = await client.GET("/UserItems/Resume", {
|
"use server";
|
||||||
|
|
||||||
|
const { data, error } = await getClient().GET("/UserItems/Resume", {
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
userId,
|
userId,
|
||||||
|
|
106409
src/features/content/apis/tmdb.generated.ts
Normal file
106409
src/features/content/apis/tmdb.generated.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
export interface paths {
|
export interface paths {
|
||||||
"/4/account/{account_object_id}/movie/recommendations": {
|
"/account/{account_object_id}/movie/recommendations": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
|
@ -15,70 +15,6 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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 type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
|
|
|
@ -1,25 +1,42 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import createClient from "openapi-fetch";
|
import createClient from "openapi-fetch";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { Entry } from "../types";
|
import { Entry, SearchResult } from "../types";
|
||||||
import { paths } from "./tmdb.not.generated";
|
import { paths as pathsV3 } from "./tmdb.generated";
|
||||||
|
import { paths as pathsV4 } from "./tmdb.not.generated";
|
||||||
|
|
||||||
const baseUrl = process.env.TMDB_BASE_URL;
|
const getClients = () => {
|
||||||
const client = createClient<paths>({
|
"use server";
|
||||||
baseUrl,
|
|
||||||
headers: {
|
const baseUrl = process.env.TMDB_BASE_URL;
|
||||||
Authorization: `Bearer ${process.env.TMDB_TOKEN}`,
|
const clientV3 = createClient<pathsV3>({
|
||||||
"Content-Type": "application/json;",
|
baseUrl: `${baseUrl}/3`,
|
||||||
},
|
headers: {
|
||||||
});
|
Authorization: `Bearer ${process.env.TMDB_TOKEN}`,
|
||||||
|
"Content-Type": "application/json;",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientV4 = createClient<pathsV4>({
|
||||||
|
baseUrl: `${baseUrl}/4`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${process.env.TMDB_TOKEN}`,
|
||||||
|
"Content-Type": "application/json;",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return [clientV3, clientV4] as const;
|
||||||
|
};
|
||||||
|
|
||||||
export const getEntry = query(
|
export const getEntry = query(
|
||||||
async (id: string): Promise<Entry | undefined> => {
|
async (id: string): Promise<Entry | undefined> => {
|
||||||
const { data } = await client.GET("/3/movie/{movie_id}", {
|
"use server";
|
||||||
|
|
||||||
|
const [ clientV3 ] = getClients();
|
||||||
|
|
||||||
|
const { data } = await clientV3.GET("/movie/{movie_id}", {
|
||||||
params: {
|
params: {
|
||||||
path: {
|
path: {
|
||||||
movie_id: id,
|
movie_id: Number.parseInt(id),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -29,8 +46,8 @@ export const getEntry = query(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(data.id),
|
id: String(data.id ?? -1),
|
||||||
title: data.title,
|
title: data.title!,
|
||||||
overview: data.overview,
|
overview: data.overview,
|
||||||
thumbnail: `http://image.tmdb.org/t/p/w342${data.poster_path}`,
|
thumbnail: `http://image.tmdb.org/t/p/w342${data.poster_path}`,
|
||||||
image: `http://image.tmdb.org/t/p/original${data.backdrop_path}`,
|
image: `http://image.tmdb.org/t/p/original${data.backdrop_path}`,
|
||||||
|
@ -40,10 +57,14 @@ export const getEntry = query(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getRecommendations = query(async (): Promise<Entry[]> => {
|
export const getRecommendations = query(async (): Promise<Entry[]> => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
const [ ,clientV4 ] = getClients();
|
||||||
|
|
||||||
const account_object_id = "6668b76e419b28ec1a1c5aab";
|
const account_object_id = "6668b76e419b28ec1a1c5aab";
|
||||||
|
|
||||||
const { data } = await client.GET(
|
const { data } = await clientV4.GET(
|
||||||
"/4/account/{account_object_id}/movie/recommendations",
|
"/account/{account_object_id}/movie/recommendations",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
path: { account_object_id },
|
path: { account_object_id },
|
||||||
|
@ -57,8 +78,8 @@ export const getRecommendations = query(async (): Promise<Entry[]> => {
|
||||||
|
|
||||||
return data?.results.map(
|
return data?.results.map(
|
||||||
({ id, title, overview, poster_path, backdrop_path }) => ({
|
({ id, title, overview, poster_path, backdrop_path }) => ({
|
||||||
id: String(id),
|
id: String(id ?? -1),
|
||||||
title,
|
title: 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}`,
|
||||||
|
@ -67,25 +88,71 @@ export const getRecommendations = query(async (): Promise<Entry[]> => {
|
||||||
}, "tmdb.getRecommendations");
|
}, "tmdb.getRecommendations");
|
||||||
|
|
||||||
export const getDiscovery = query(async (): Promise<Entry[]> => {
|
export const getDiscovery = query(async (): Promise<Entry[]> => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
const [ clientV3 ] = getClients();
|
||||||
|
|
||||||
const [{ data: movies }, { data: series }] = await Promise.all([
|
const [{ data: movies }, { data: series }] = await Promise.all([
|
||||||
client.GET("/3/discover/movie"),
|
clientV3.GET("/discover/movie"),
|
||||||
client.GET("/3/discover/movie"),
|
clientV3.GET("/discover/tv"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (movies === undefined || series === undefined) {
|
if (movies === undefined || series === undefined) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log({ movies: movies.results.length, series: series.results.length });
|
const movieEntries = movies?.results?.slice(0, 10)
|
||||||
|
|
||||||
return movies?.results
|
|
||||||
.slice(0, 9)
|
|
||||||
.concat(series?.results.slice(0, 9))
|
|
||||||
.map(({ id, title, overview, poster_path, backdrop_path }) => ({
|
.map(({ id, title, overview, poster_path, backdrop_path }) => ({
|
||||||
id: String(id),
|
id: String(id ?? -1),
|
||||||
title,
|
title: 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}`,
|
||||||
}));
|
})) ?? []
|
||||||
|
|
||||||
|
const seriesEntries = series?.results?.slice(0, 10)
|
||||||
|
.map(({ id, name, overview, poster_path, backdrop_path }) => ({
|
||||||
|
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);
|
||||||
}, "tmdb.getDiscovery");
|
}, "tmdb.getDiscovery");
|
||||||
|
|
||||||
|
export const searchMulti = query(async (query: string, page: number = 1): Promise<SearchResult> => {
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
if (query.length === 0) {
|
||||||
|
return { count: 0, pages: 0, results: [] };
|
||||||
|
}
|
||||||
|
const [ clientV3 ] = getClients();
|
||||||
|
|
||||||
|
|
||||||
|
const { data } = await clientV3.GET("/search/multi", {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
query,
|
||||||
|
page,
|
||||||
|
include_adult: false,
|
||||||
|
language: 'en-US'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data === undefined) {
|
||||||
|
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?.map(({ id, name, title, media_type, overview, backdrop_path, poster_path }) => ({
|
||||||
|
id: String(id),
|
||||||
|
title: `${name ?? title ?? ''} (${media_type})`,
|
||||||
|
overview,
|
||||||
|
thumbnail: `http://image.tmdb.org/t/p/w342${poster_path}`,
|
||||||
|
image: `http://image.tmdb.org/t/p/original${backdrop_path}`,
|
||||||
|
})) ?? [] };
|
||||||
|
}, "tmdb.search.multi");
|
194262
src/features/content/apis/tmdb.yml
Normal file
194262
src/features/content/apis/tmdb.yml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
import type { Category, Entry } from "./types";
|
import type { Category, Entry } from "./types";
|
||||||
import { query } from "@solidjs/router";
|
import { query } from "@solidjs/router";
|
||||||
import { entries } from "./data";
|
import { getContinueWatching, getRandomItems } from "./apis/jellyfin";
|
||||||
import { getContinueWatching, getItem, getRandomItems } from "./apis/jellyfin";
|
|
||||||
import {
|
import {
|
||||||
getDiscovery,
|
getDiscovery,
|
||||||
getRecommendations,
|
getRecommendations,
|
||||||
getEntry as getTmdbEntry,
|
getEntry as getTmdbEntry,
|
||||||
|
searchMulti,
|
||||||
} from "./apis/tmdb";
|
} from "./apis/tmdb";
|
||||||
|
|
||||||
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
|
||||||
|
@ -19,20 +19,25 @@ 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?)",
|
label: "For you",
|
||||||
entries: await getRecommendations(),
|
entries: await getRecommendations(),
|
||||||
},
|
},
|
||||||
{ label: "Discover", entries: await getDiscovery() },
|
{ label: "Discover", entries: await getDiscovery() },
|
||||||
{ label: "Random", entries: await getRandomItems(jellyfinUserId) },
|
{ label: "Random", entries: await getRandomItems(jellyfinUserId) },
|
||||||
];
|
];
|
||||||
}, "series.categories.list");
|
}, "content.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 getTmdbEntry(id);
|
return getTmdbEntry(id);
|
||||||
// return getItem(jellyfinUserId, id);
|
// return getItem(jellyfinUserId, id);
|
||||||
},
|
},
|
||||||
"series.get",
|
"content.get",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const search = query(async (query: string, page: number = 1) => {
|
||||||
|
"use server";
|
||||||
|
return searchMulti(query, page);
|
||||||
|
}, 'content.search');
|
||||||
|
|
||||||
export { listUsers, getContinueWatching, listItems } from "./apis/jellyfin";
|
export { listUsers, getContinueWatching, listItems } from "./apis/jellyfin";
|
||||||
|
|
|
@ -29,3 +29,9 @@ export namespace Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
count: number;
|
||||||
|
pages: number;
|
||||||
|
results: Entry[];
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
gap: 2em;
|
gap: var(--size-6);
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
|
|
||||||
& > .hero {
|
|
||||||
border-radius: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .list {
|
|
||||||
padding-inline: 4em;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -3,10 +3,10 @@ import {
|
||||||
createEventSignal,
|
createEventSignal,
|
||||||
} from "@solid-primitives/event-listener";
|
} from "@solid-primitives/event-listener";
|
||||||
import { createAsync, json, query } from "@solidjs/router";
|
import { createAsync, json, query } from "@solidjs/router";
|
||||||
import { Component, createEffect, createMemo, createSignal } from "solid-js";
|
import { Component, createEffect, createMemo, createSignal, on } from "solid-js";
|
||||||
import css from "./player.module.css";
|
import css from "./player.module.css";
|
||||||
import { Volume } from "./controls/volume";
|
import { Volume } from "./controls/volume";
|
||||||
import { getEntry } from "../content";
|
import { Entry, getEntry } from "../content";
|
||||||
|
|
||||||
const metadata = query(async (id: string) => {
|
const metadata = query(async (id: string) => {
|
||||||
"use server";
|
"use server";
|
||||||
|
@ -36,7 +36,7 @@ const metadata = query(async (id: string) => {
|
||||||
}, "player.metadata");
|
}, "player.metadata");
|
||||||
|
|
||||||
interface PlayerProps {
|
interface PlayerProps {
|
||||||
id: string;
|
entry: Entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Player: Component<PlayerProps> = (props) => {
|
export const Player: Component<PlayerProps> = (props) => {
|
||||||
|
@ -44,9 +44,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
undefined as unknown as HTMLVideoElement,
|
undefined as unknown as HTMLVideoElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
const entry = createAsync(() => getEntry(props.id));
|
const data = createAsync(() => metadata(props.entry.id), {
|
||||||
|
|
||||||
const data = createAsync(() => metadata(props.id), {
|
|
||||||
deferStream: true,
|
deferStream: true,
|
||||||
initialValue: {} as any,
|
initialValue: {} as any,
|
||||||
});
|
});
|
||||||
|
@ -65,25 +63,12 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
: "";
|
: "";
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(on(thumbnails, (thumbnails) => {
|
||||||
const metadata = data();
|
// console.log(thumbnails, video()!.textTracks.getTrackById("thumbnails")?.cues);
|
||||||
const el = video();
|
|
||||||
|
|
||||||
if (metadata === undefined || el === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(metadata);
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
thumbnails();
|
|
||||||
|
|
||||||
console.log(video()!.textTracks.getTrackById("thumbnails")?.cues);
|
|
||||||
|
|
||||||
// const captions = el.addTextTrack("captions", "English", "en");
|
// const captions = el.addTextTrack("captions", "English", "en");
|
||||||
// captions.
|
// captions.
|
||||||
});
|
}));
|
||||||
|
|
||||||
const onDurationChange = createEventSignal(video, "durationchange");
|
const onDurationChange = createEventSignal(video, "durationchange");
|
||||||
const onTimeUpdate = createEventSignal(video, "timeupdate");
|
const onTimeUpdate = createEventSignal(video, "timeupdate");
|
||||||
|
@ -102,53 +87,53 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
|
|
||||||
createEventListenerMap(() => video()!, {
|
createEventListenerMap(() => video()!, {
|
||||||
durationchange(e) {
|
durationchange(e) {
|
||||||
console.log("durationchange", e);
|
// console.log("durationchange", e);
|
||||||
},
|
},
|
||||||
loadeddata(e) {
|
loadeddata(e) {
|
||||||
console.log("loadeddata", e);
|
// console.log("loadeddata", e);
|
||||||
},
|
},
|
||||||
loadedmetadata(e) {
|
loadedmetadata(e) {
|
||||||
console.log("loadedmetadata", e);
|
// console.log("loadedmetadata", e);
|
||||||
},
|
},
|
||||||
ratechange(e) {
|
ratechange(e) {
|
||||||
console.log("ratechange", e);
|
// console.log("ratechange", e);
|
||||||
},
|
},
|
||||||
seeked(e) {
|
seeked(e) {
|
||||||
console.log("seeked", e);
|
// console.log("seeked", e);
|
||||||
},
|
},
|
||||||
seeking(e) {
|
seeking(e) {
|
||||||
console.log("seeking", e);
|
// console.log("seeking", e);
|
||||||
},
|
},
|
||||||
stalled(e) {
|
stalled(e) {
|
||||||
console.log(
|
// console.log(
|
||||||
"stalled (meaning downloading data failed)",
|
// "stalled (meaning downloading data failed)",
|
||||||
e,
|
// e,
|
||||||
video()!.error,
|
// video()!.error,
|
||||||
);
|
// );
|
||||||
},
|
},
|
||||||
|
|
||||||
play(e) {
|
play(e) {
|
||||||
console.log("play", e);
|
// console.log("play", e);
|
||||||
},
|
},
|
||||||
canplay(e) {
|
canplay(e) {
|
||||||
console.log("canplay", e);
|
// console.log("canplay", e);
|
||||||
},
|
},
|
||||||
playing(e) {
|
playing(e) {
|
||||||
console.log("playing", e);
|
// console.log("playing", e);
|
||||||
},
|
},
|
||||||
pause(e) {
|
pause(e) {
|
||||||
console.log("pause", e);
|
// console.log("pause", e);
|
||||||
},
|
},
|
||||||
suspend(e) {
|
suspend(e) {
|
||||||
// console.log("suspend", e);
|
// console.log("suspend", e);
|
||||||
},
|
},
|
||||||
|
|
||||||
volumechange(e) {
|
volumechange(e) {
|
||||||
console.log("volumechange", e);
|
// console.log("volumechange", e);
|
||||||
},
|
},
|
||||||
|
|
||||||
waiting(e) {
|
waiting(e) {
|
||||||
console.log("waiting", e);
|
// console.log("waiting", e);
|
||||||
},
|
},
|
||||||
|
|
||||||
progress(e) {
|
progress(e) {
|
||||||
|
@ -172,7 +157,7 @@ export const Player: Component<PlayerProps> = (props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<figure class={css.player}>
|
<figure class={css.player}>
|
||||||
<h1>{entry()?.title}</h1>
|
<h1>{props.entry?.title}</h1>
|
||||||
|
|
||||||
<video
|
<video
|
||||||
ref={setVideo}
|
ref={setVideo}
|
||||||
|
|
|
@ -26,15 +26,12 @@
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-start: 100%;
|
inset-inline-start: 100%;
|
||||||
inset-block: 0;
|
inset-block: -1em;
|
||||||
inline-size: 20vw;
|
inline-size: 40vw;
|
||||||
/* background:
|
background-image: linear-gradient(to right, rgb(from var(--surface-1) r g b / .9) 50%, transparent);
|
||||||
radial-gradient(ellipse at left center 100% 100%, #f00, transparent),
|
|
||||||
linear-gradient(to right, #0003, transparent); */
|
|
||||||
background-image: linear-gradient(to right, #0003, transparent);
|
|
||||||
mask: radial-gradient(
|
mask: radial-gradient(
|
||||||
ellipse 20vw 100% at left center,
|
ellipse 40vw 100% at left center,
|
||||||
black,
|
black 25%,
|
||||||
transparent
|
transparent
|
||||||
);
|
);
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
|
@ -53,24 +50,24 @@
|
||||||
transition:
|
transition:
|
||||||
transform 2s var(--ease-spring-5),
|
transform 2s var(--ease-spring-5),
|
||||||
opacity 0.3s var(--ease-3);
|
opacity 0.3s var(--ease-3);
|
||||||
color: var(--stone-4);
|
color: var(--text-2);
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s var(--ease-3);
|
transition: opacity 0.3s var(--ease-3);
|
||||||
text-shadow: 0 0 1em #000;
|
text-shadow: 0 0 .5em var(--surface-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
fill: var(--stone-4);
|
fill: var(--text-2);
|
||||||
inline-size: 2.5rem;
|
inline-size: 2.5rem;
|
||||||
block-size: 2.5rem;
|
block-size: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: var(--yellow-4);
|
color: var(--yellow-5);
|
||||||
list-style: disc;
|
list-style: disc;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -80,29 +77,29 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
fill: var(--yellow-4);
|
fill: var(--yellow-5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus))::before {
|
&:has(a:is(:hover, :focus)) {
|
||||||
opacity: 1;
|
&::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
transform: scale(max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index))))));
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
|
&:has(a:is(:hover, :focus)) > a:not(:is(:hover, :focus)) {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus)) > a {
|
|
||||||
transform: scale(
|
|
||||||
max(1, calc(1.5 - (0.2 * abs(var(--target) - var(--sibling-index)))))
|
|
||||||
);
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(a:is(:hover, :focus):nth-child(1)) {
|
&:has(a:is(:hover, :focus):nth-child(1)) {
|
||||||
--target: 1;
|
--target: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,10 +67,10 @@ const [ThemeContextProvider, useTheme] = createContextProvider<
|
||||||
},
|
},
|
||||||
|
|
||||||
setColorScheme(colorScheme) {
|
setColorScheme(colorScheme) {
|
||||||
// updateState({ colorScheme, hue: state.latest!.hue });
|
updateState({ colorScheme, hue: state.latest!.hue });
|
||||||
},
|
},
|
||||||
setHue(hue) {
|
setHue(hue) {
|
||||||
// updateState({ hue, colorScheme: state.latest!.colorScheme });
|
updateState({ hue, colorScheme: state.latest!.colorScheme });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
59
src/routes/(shell)/details/[slug].tsx
Normal file
59
src/routes/(shell)/details/[slug].tsx
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import {
|
||||||
|
createAsync,
|
||||||
|
json,
|
||||||
|
Params,
|
||||||
|
query,
|
||||||
|
redirect,
|
||||||
|
RouteDefinition,
|
||||||
|
useParams,
|
||||||
|
} from "@solidjs/router";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
import { Details } from "~/components/details";
|
||||||
|
import { createSlug, Entry, getEntry } from "~/features/content";
|
||||||
|
|
||||||
|
const healUrl = async (slug: string, entry: Entry) => {
|
||||||
|
const actualSlug = createSlug(entry);
|
||||||
|
|
||||||
|
if (slug !== actualSlug) {
|
||||||
|
// Not entirely sure a permanent redirect is what we want in this case
|
||||||
|
throw redirect(`/details/${actualSlug}`, { status: 308 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ItemParams extends Params {
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const route = {
|
||||||
|
async preload({ params }) {
|
||||||
|
const slug = params.slug;
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await getEntry(slug.slice(slug.lastIndexOf("-") + 1));
|
||||||
|
|
||||||
|
if (entry === undefined) {
|
||||||
|
return json(null, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
healUrl(slug, entry);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
},
|
||||||
|
} satisfies RouteDefinition;
|
||||||
|
|
||||||
|
export default function Item() {
|
||||||
|
const { slug } = useParams<ItemParams>();
|
||||||
|
const id = slug.slice(slug.lastIndexOf("-") + 1);
|
||||||
|
const entry = createAsync(() => getEntry(id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={entry()} fallback="Some kind of pretty 404 page I guess">{
|
||||||
|
entry => <Details entry={entry()} />
|
||||||
|
}</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
46
src/routes/(shell)/search/index.module.css
Normal file
46
src/routes/(shell)/search/index.module.css
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
.container {
|
||||||
|
display: block grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
|
||||||
|
padding: var(--size-7);
|
||||||
|
gap: var(--size-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
inset-block-start: 0;
|
||||||
|
|
||||||
|
padding: var(--size-7);
|
||||||
|
padding-block-end: var(--size-2);
|
||||||
|
margin: calc(-1 * var(--size-7));
|
||||||
|
margin-block-end: calc(-1 * var(--size-2));
|
||||||
|
background-color: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
inline-size: 100%;
|
||||||
|
display: block grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: var(--size-6);
|
||||||
|
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& > .item {
|
||||||
|
inline-size: 100%;
|
||||||
|
display: block grid;
|
||||||
|
grid: 100% / 100%;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
background-color: var(--surface-3);
|
||||||
|
border-radius: var(--size-2);
|
||||||
|
|
||||||
|
aspect-ratio: 3 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,67 @@
|
||||||
|
import { createInfiniteScroll } from "@solid-primitives/pagination";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
|
import { createEffect, createSignal, For, on, onMount, Show, createComputed, batch, createMemo, untrack } from "solid-js";
|
||||||
|
import { createSlug, search } from "~/features/content";
|
||||||
|
import { AiOutlineLoading } from "solid-icons/ai";
|
||||||
|
import css from './index.module.css';
|
||||||
|
import { debounce } from "@solid-primitives/scheduled";
|
||||||
|
|
||||||
|
const getResults = async (query: string, page: number) => {
|
||||||
|
const { results } = await search(query, page + 1);
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
|
const [ query, setQuery ] = createSignal(""); // lord of the rings
|
||||||
|
const [ ref, setRef ] = createSignal<HTMLInputElement>();
|
||||||
|
|
||||||
|
const KAAS = createMemo(() => {
|
||||||
|
const q = query();
|
||||||
|
const [pages, setEl, { end }] = createInfiniteScroll((page) => getResults(q, page));
|
||||||
|
|
||||||
|
return { pages, setEl, end };
|
||||||
|
});
|
||||||
|
// const result = createAsync(() => search(query()), { initialValue: { count: 0, pages: 0, results: [] } });
|
||||||
|
|
||||||
const title = 'Search';
|
const title = 'Search';
|
||||||
return <>
|
|
||||||
|
createEffect(() => {
|
||||||
|
KAAS();
|
||||||
|
|
||||||
|
untrack(ref)?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div class={css.container}>
|
||||||
<Title>{title}</Title>
|
<Title>{title}</Title>
|
||||||
<h1>{title}</h1>
|
|
||||||
</>;
|
<header class={css.header}>
|
||||||
|
<input ref={setRef} type="search" placeholder={title} value={query()} oninput={debounce(e => setQuery(e.target.value), 300)} />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ul class={css.grid}>
|
||||||
|
<For each={KAAS().pages()}>{
|
||||||
|
item => <a id={`item:${item.id}`} href={`/details/${createSlug(item)}`}>
|
||||||
|
<img class={css.item} src={item.thumbnail} title={item.title} />
|
||||||
|
</a>
|
||||||
|
}</For>
|
||||||
|
|
||||||
|
<Show when={!KAAS().end()}>
|
||||||
|
<AiOutlineLoading ref={KAAS().setEl} />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={KAAS().pages().length === 0}>
|
||||||
|
<p>No results</p>
|
||||||
|
</Show>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* <output>
|
||||||
|
<p>{result().count}</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<For each={result().results}>{
|
||||||
|
result => <li>{result.title}</li>
|
||||||
|
}</For>
|
||||||
|
</ul>
|
||||||
|
</output> */}
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
createAsync,
|
||||||
json,
|
json,
|
||||||
Params,
|
Params,
|
||||||
query,
|
query,
|
||||||
|
@ -47,10 +48,11 @@ export const route = {
|
||||||
export default function Item() {
|
export default function Item() {
|
||||||
const { slug } = useParams<ItemParams>();
|
const { slug } = useParams<ItemParams>();
|
||||||
const id = slug.slice(slug.lastIndexOf("-") + 1);
|
const id = slug.slice(slug.lastIndexOf("-") + 1);
|
||||||
|
const entry = createAsync(() => getEntry(id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Player id={id} />
|
<Player entry={entry} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue