diff --git a/bun.lock b/bun.lock
index 67e065e..1ed7c1e 100644
--- a/bun.lock
+++ b/bun.lock
@@ -7,30 +7,31 @@
"@solid-primitives/context": "^0.3.1",
"@solid-primitives/deep": "^0.3.2",
"@solid-primitives/event-listener": "^2.4.1",
+ "@solid-primitives/fullscreen": "^1.3.1",
"@solid-primitives/pagination": "^0.4.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.4",
- "better-auth": "^1.2.7",
+ "better-auth": "^1.2.8",
"bindings": "^1.5.0",
"open-props": "^1.7.15",
"openapi-fetch": "^0.13.8",
"sitemap": "^8.0.0",
"solid-icons": "^1.1.0",
- "solid-js": "^1.9.6",
+ "solid-js": "^1.9.7",
"vinxi": "^0.5.6",
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"browserslist": "^4.24.5",
- "bun-types": "^1.2.13",
+ "bun-types": "^1.2.14",
"lightningcss": "^1.30.1",
"openapi-typescript": "^7.8.0",
"solid-devtools": "^0.33.0",
"vite-plugin-solid-svg": "^0.8.1",
"vite-tsconfig-paths": "^5.1.4",
- "vitest": "^3.1.3",
+ "vitest": "^3.1.4",
},
},
},
@@ -344,6 +345,8 @@
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.1", "", { "dependencies": { "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Xc/lBCeuh9LwzR4lYbMDtopwWK7N9b4o+FmI4uoI8DOtVGYi0Ip20DG8PtwHk+g31lHgvwtFFVKfnUx2UaqZJg=="],
+ "@solid-primitives/fullscreen": ["@solid-primitives/fullscreen@1.3.1", "", { "dependencies": { "@solid-primitives/utils": "^6.3.1" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMHIPekaNuBQk8YVuHQa28ExZnC7xdGLhBI7MTEY38pYcIQGcTMQ4B6zpZhEkRcoYudaFHSGDBLCcHlELV7f3g=="],
+
"@solid-primitives/keyboard": ["@solid-primitives/keyboard@1.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-0QX9O3eUaQorNNmXZn8a4efSByayIScVq+iGSwheD7m3SL/ACLM5oZlCNpTPLcemnVVfUPAHFiViEj86XpN5qw=="],
"@solid-primitives/media": ["@solid-primitives/media@2.3.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/rootless": "^1.5.0", "@solid-primitives/static-store": "^0.1.0", "@solid-primitives/utils": "^6.3.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-7+C3wfbWnGE/WPoNsqcp/EeOP2aNNB92RCpsWhBth8E5lZo/J+rK6jMb7umVsK0zguT8HBpeXp1pFyFbcsHStA=="],
diff --git a/package.json b/package.json
index ccd3469..b234ce0 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@solid-primitives/context": "^0.3.1",
"@solid-primitives/deep": "^0.3.2",
"@solid-primitives/event-listener": "^2.4.1",
+ "@solid-primitives/fullscreen": "^1.3.1",
"@solid-primitives/pagination": "^0.4.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solidjs/meta": "^0.29.4",
diff --git a/public/favicon-dark.svg b/public/favicon-dark.svg
new file mode 100644
index 0000000..00fe969
--- /dev/null
+++ b/public/favicon-dark.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/public/favicon-light.svg b/public/favicon-light.svg
new file mode 100644
index 0000000..a16a7d5
--- /dev/null
+++ b/public/favicon-light.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/components/hero/hero.tsx b/src/components/hero/hero.tsx
index e3bc516..5e058da 100644
--- a/src/components/hero/hero.tsx
+++ b/src/components/hero/hero.tsx
@@ -21,10 +21,6 @@ export function Hero(props: HeroProps) {
const Page: Component<{ entry: Entry }> = (props) => {
const slug = createMemo(() => createSlug(props.entry));
- createEffect(() => {
- // console.log(props.entry);
- });
-
return (
(
-
+
+
{assets}
diff --git a/src/features/content/apis/jellyfin.ts b/src/features/content/apis/jellyfin.ts
index eb96c37..a0879ab 100644
--- a/src/features/content/apis/jellyfin.ts
+++ b/src/features/content/apis/jellyfin.ts
@@ -57,6 +57,41 @@ export const listUsers = query(async () => {
return data ?? [];
}, "jellyfin.listUsers");
+export const listItemIds = query(
+ async (): Promise
> => {
+ "use server";
+
+ const { data, error } = await getClient().GET("/Items", {
+ params: {
+ query: {
+ hasImdbId: true,
+ recursive: true,
+ includeItemTypes: ["Movie", "Series"],
+ fields: [
+ "ProviderIds",
+ "Genres",
+ "DateLastMediaAdded",
+ "DateCreated",
+ "MediaSources",
+ ],
+ },
+ },
+ });
+
+ if (data === undefined) {
+ return {};
+ }
+
+ return Object.fromEntries(
+ data.Items?.map((item) => ([
+ item.ProviderIds!["Tmdb"]!,
+ { jellyfin: item.Id! },
+ ])) ?? []
+ );
+ },
+ "jellyfin.listItemIds",
+);
+
export const listItems = query(
async (userId: string): Promise => {
"use server";
@@ -189,28 +224,28 @@ export const getItem = query(
export const getItemStream = query(
- async (userId: string, itemId: string): Promise => {
+ async (itemId: string, range: string): Promise => {
"use server";
- const item = await getItem(userId, itemId);
-
- console.log(item);
-
- if (item === undefined) {
- return undefined;
- }
-
- const { data, error } = await getClient().GET("/Videos/{itemId}/stream", {
+ // I don't really know what is the big difference between mp4 and mkv.
+ // But mkv is able to use ranges an can report the video's length, whereas mp4 doesn't
+ const { response } = await getClient().GET("/Videos/{itemId}/stream", {
params: {
path: {
- itemId: item.providers.jellyfin,
+ itemId,
},
query: {
+ static: true,
+ container: 'mkv',
},
},
+ parseAs: 'stream',
+ headers: {
+ Range: range
+ }
});
- return data;
+ return response;
},
"jellyfin.getItemStream",
);
@@ -267,7 +302,7 @@ export const getItemPlaybackInfo = query(
);
export const queryItems = query(async () => {
- "use server";
+ "use server";
const { data, error } = await getClient().GET("/Items", {
params: {
@@ -285,23 +320,6 @@ export const queryItems = query(async () => {
console.log(data);
}, "jellyfin.queryItems");
-export const getItemIds = query(async () => {
- "use server";
-
- const { data, error } = await getClient().GET("/Items", {
- params: {
- query: {
- mediaTypes: ["Video"],
- fields: ["ProviderIds"],
- includeItemTypes: ["Series", "Movie"],
- recursive: true,
- },
- },
- });
-
- console.log(data);
-}, "jellyfin.getItemIds");
-
export const getContinueWatching = query(
async (userId: string): Promise => {
"use server";
diff --git a/src/features/content/apis/tmdb.ts b/src/features/content/apis/tmdb.ts
index 1461ec7..3f0625c 100644
--- a/src/features/content/apis/tmdb.ts
+++ b/src/features/content/apis/tmdb.ts
@@ -41,8 +41,6 @@ export const getEntry = query(
},
});
- console.log(data);
-
if (data === undefined) {
return undefined;
}
diff --git a/src/features/content/service.ts b/src/features/content/service.ts
index 4e0d3a7..12d3af9 100644
--- a/src/features/content/service.ts
+++ b/src/features/content/service.ts
@@ -2,7 +2,7 @@
import type { Category, Entry } from "./types";
import { query } from "@solidjs/router";
-import { getContinueWatching, getItemStream, getRandomItems } from "./apis/jellyfin";
+import { listItemIds, getContinueWatching, getItemStream, getRandomItems } from "./apis/jellyfin";
import {
getDiscovery,
getRecommendations,
@@ -12,9 +12,15 @@ import {
const jellyfinUserId = "a9c51af84bf54578a99ab4dd0ebf0763";
+const lookupTable = query(async () => listItemIds(), 'content.lookupTable');
+
// export const getHighlights = () => getRandomItems(jellyfinUserId);
export const getHighlights = () => getContinueWatching(jellyfinUserId);
-export const getStream = (id: string) => getItemStream(jellyfinUserId, id);
+export const getStream = query(async (id: string, range: string) => {
+ const table = await lookupTable();
+
+ return getItemStream(table[id].jellyfin, range);
+}, 'content.stream');
export const listCategories = query(async (): Promise => {
return [
@@ -31,7 +37,6 @@ export const listCategories = query(async (): Promise => {
export const getEntry = query(
async (id: Entry["id"]): Promise => {
return getTmdbEntry(id);
- // return getItem(jellyfinUserId, id);
},
"content.get",
);
diff --git a/src/features/player/context.tsx b/src/features/player/context.tsx
index 6255b4d..83a64bc 100644
--- a/src/features/player/context.tsx
+++ b/src/features/player/context.tsx
@@ -6,6 +6,7 @@ import {
import { Accessor, createEffect, onMount, Setter } from "solid-js";
import { createStore } from "solid-js/store";
import { createEventListenerMap } from "@solid-primitives/event-listener";
+import { createFullscreen } from "@solid-primitives/fullscreen";
type State = "playing" | "paused";
@@ -15,9 +16,14 @@ type Volume = {
};
export interface VideoAPI {
+ readonly fullscreen: Accessor;
+ readonly setFullscreen: Setter;
+
+ readonly loading: Accessor;
readonly duration: Accessor;
readonly buffered: Accessor;
readonly currentTime: Accessor;
+
setTime(time: number): void;
readonly state: {
@@ -37,10 +43,13 @@ export interface VideoAPI {
}
interface VideoProviderProps extends ContextProviderProps {
+ root: HTMLElement | undefined;
video: HTMLVideoElement | undefined;
}
interface VideoStore {
+ fullscreen: boolean;
+ loading: boolean;
duration: number;
buffered: number;
currentTime: number;
@@ -55,6 +64,8 @@ export const [VideoProvider, useVideo] = createContextProvider<
(props) => {
const video = props.video;
const [store, setStore] = createStore({
+ fullscreen: false,
+ loading: true,
duration: 0,
buffered: 0,
currentTime: 0,
@@ -65,7 +76,17 @@ export const [VideoProvider, useVideo] = createContextProvider<
},
});
+ const fullscreen = createFullscreen(
+ () => props.root,
+ () => store.fullscreen
+ );
+
const api: VideoAPI = {
+ fullscreen,
+ setFullscreen: setStore.bind(null, "fullscreen"),
+
+ loading: () => store.loading,
+
duration: () => store.duration,
buffered: () => store.buffered,
currentTime: () => store.currentTime,
@@ -147,11 +168,22 @@ export const [VideoProvider, useVideo] = createContextProvider<
timeRanges.length > 0 ? timeRanges.end(timeRanges.length - 1) : 0
);
},
+ canplay() {
+ setStore("loading", false);
+ },
+ waiting() {
+ setStore("loading", true);
+ },
});
return api;
},
{
+ fullscreen: () => false,
+ setFullscreen() {},
+
+ loading: () => false,
+
duration: () => 0,
buffered: () => 0,
currentTime: () => 0,
diff --git a/src/features/player/controls/fullscreen.tsx b/src/features/player/controls/fullscreen.tsx
new file mode 100644
index 0000000..5c68808
--- /dev/null
+++ b/src/features/player/controls/fullscreen.tsx
@@ -0,0 +1,15 @@
+import { Component, Show } from "solid-js";
+import { useVideo } from "../context";
+import { FaSolidCompress, FaSolidExpand } from "solid-icons/fa";
+
+export const Fullscreen: Component<{}> = (props) => {
+ const video = useVideo();
+
+ return (
+
+ );
+};
diff --git a/src/features/player/controls/playState.module.css b/src/features/player/controls/playState.module.css
deleted file mode 100644
index 4d5a1ee..0000000
--- a/src/features/player/controls/playState.module.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.play {
- font-size: var(--size-7);
- text-shadow: 0 0 .5rem #000;
- aspect-ratio: 1;
- background-color: transparent;
- border-radius: var(--radius-2);
-
- transition: background-color .2s var(--ease-in-out-1);
-
- &:hover {
- background-color: rgba(from var(--gray-2) r g b / .25);
- }
-}
\ No newline at end of file
diff --git a/src/features/player/controls/playState.tsx b/src/features/player/controls/playState.tsx
index 65974f9..a91b629 100644
--- a/src/features/player/controls/playState.tsx
+++ b/src/features/player/controls/playState.tsx
@@ -1,14 +1,12 @@
import { Component, Show } from "solid-js";
import { useVideo } from "../context";
import { FaSolidPause, FaSolidPlay } from "solid-icons/fa";
-import css from "./playState.module.css";
export const PlayState: Component<{}> = (props) => {
const video = useVideo();
return (