diff --git a/app.config.ts b/app.config.ts index 2efc67a..8a04eb4 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,14 +1,15 @@ import { defineConfig } from "@solidjs/start/config"; import solidSvg from "vite-plugin-solid-svg"; import devtools from "solid-devtools/vite"; +import { build, fileURLToPath } from "bun"; export default defineConfig({ vite: { plugins: [ - devtools({ - autoname: true, - }), - solidSvg(), + // devtools({ + // autoname: true, + // }), + // solidSvg(), ], }, solid: { @@ -19,7 +20,7 @@ export default defineConfig({ server: { preset: "bun", prerender: { - routes: ["/sitemaps.xml"], + routes: ["/sitemap.xml"], }, }, }); diff --git a/auth.sqlite b/auth.sqlite index ce85926..e9519d1 100644 Binary files a/auth.sqlite and b/auth.sqlite differ diff --git a/better-auth_migrations/2025-06-17T07-59-07.464Z.sql b/better-auth_migrations/2025-06-17T07-59-07.464Z.sql new file mode 100644 index 0000000..b1c5a3e --- /dev/null +++ b/better-auth_migrations/2025-06-17T07-59-07.464Z.sql @@ -0,0 +1,7 @@ +create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null, "preferred_username" text not null, "username" text not null); + +create table "session" ("id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id")); + +create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id"), "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" date, "refreshTokenExpiresAt" date, "scope" text, "password" text, "createdAt" date not null, "updatedAt" date not null); + +create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date, "updatedAt" date); \ No newline at end of file diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4c6af4f --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + description = "Streamarr"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgsnixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + bix = { + url = "github:knarkzel/bix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + packages = bix.buildBunPackage { + src = ./.; + packages = ./package.json; + }; + }); +} diff --git a/src/auth.client.ts b/src/auth.client.ts new file mode 100644 index 0000000..7f0e2e6 --- /dev/null +++ b/src/auth.client.ts @@ -0,0 +1,6 @@ +import { createAuthClient } from "better-auth/solid"; +import { genericOAuthClient } from "better-auth/client/plugins"; + +export const { signIn, signOut, useSession, ...client } = createAuthClient({ + plugins: [genericOAuthClient()], +}); \ No newline at end of file diff --git a/src/auth.ts b/src/auth.server.ts similarity index 75% rename from src/auth.ts rename to src/auth.server.ts index d8d8de8..caa7b69 100644 --- a/src/auth.ts +++ b/src/auth.server.ts @@ -1,7 +1,6 @@ import { betterAuth } from "better-auth"; import { genericOAuth } from "better-auth/plugins"; -import { createAuthClient } from "better-auth/solid"; -import { genericOAuthClient } from "better-auth/client/plugins"; +import { profile } from "bun:jsc"; import { Database } from "bun:sqlite"; export const auth = betterAuth({ @@ -20,18 +19,10 @@ export const auth = betterAuth({ type: "string", nullable: true, }, - preferred_username: { - type: "string", - nullable: true, - }, username: { type: "string", nullable: true, }, - profile: { - type: "string", - nullable: true, - }, }, }, plugins: [ @@ -54,12 +45,10 @@ export const auth = betterAuth({ ], accessType: "offline", pkce: true, + mapProfileToUser: ({ id, name, email, image, preferred_username, emailVerified }) => + ({ id, name, email, emailVerified, image, username: preferred_username }), }, ], }), ], }); - -export const { signIn, signOut, useSession, ...client } = createAuthClient({ - plugins: [genericOAuthClient()], -}); diff --git a/src/features/content/apis/jellyfin.ts b/src/features/content/apis/jellyfin.ts index 7233b96..a307660 100644 --- a/src/features/content/apis/jellyfin.ts +++ b/src/features/content/apis/jellyfin.ts @@ -219,7 +219,7 @@ export const getItemStream = query( itemId, }, query: { - startTimeTicks: userData?.playbackPositionTicks, + // startTimeTicks: userData?.playbackPositionTicks, }, }, parseAs: 'stream', @@ -392,11 +392,11 @@ const toEntry = (item: components['schemas']['BaseItemDto']): Entry => { id: `${type}${item.ProviderIds!["Tmdb"]!}`, title: item.Name!, overview: item.Overview!, - thumbnail: new URL(`/Items/${item.Id}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), + thumbnail: new URL(`/Items/${item.Id}/Images/Primary`, getBaseUrl()), image: new URL(`/Items/${item.Id}/Images/Backdrop`, getBaseUrl()), providers: { jellyfin: item.Id - }, + }, trailer: item.RemoteTrailers?.at(-1)?.Url ?? undefined, }; }; \ No newline at end of file diff --git a/src/features/content/service.ts b/src/features/content/service.ts index bd8e929..9f29678 100644 --- a/src/features/content/service.ts +++ b/src/features/content/service.ts @@ -1,5 +1,3 @@ -"use server"; - import type { Category, Entry } from "./types"; import { query, revalidate } from "@solidjs/router"; import { listItemIds, getContinueWatching, getItemStream, getRandomItems, getItemUserData } from "./apis/jellyfin"; @@ -26,44 +24,47 @@ const lookupTable = query(async () => { export const getHighlights = () => getContinueWatching(jellyfinUserId); export const getStream = query(async (id: string, range: string) => { - const table = await lookupTable(); - const ids = table[id]; - const manager = id[0] === 'm' ? 'radarr' : 'sonarr' + 'use server'; - if (ids?.jellyfin) { - return getItemStream(jellyfinUserId, ids.jellyfin, range); - } + const table = await lookupTable(); + const ids = table[id]; + const manager = id[0] === 'm' ? 'radarr' : 'sonarr' - if (ids?.radarr) { - console.log(`radarr has the entry '${ids.radarr}', but jellyfin does not (yet) have the file`); - console.log(await TEST(ids.radarr)) - return; - } + if (ids?.jellyfin) { + return getItemStream(jellyfinUserId, ids.jellyfin, range); + } - if (ids?.sonarr) { - console.log('sonarr has the entry, but jellyfin does not (yet) have the file'); - return; - } + if (ids?.radarr) { + console.log(`radarr has the entry '${ids.radarr}', but jellyfin does not (yet) have the file`); + console.log(await TEST(ids.radarr)) + return; + } - // - If the lookup table has no entry - // than this means that we do not have the requested entry at all, - // neither in trackers nor in the media server - // - // - If the lookup table contains a jellyfin id, - // than we have the content and can stream straight away - // - // - If we have the radarr or sonarr id, - // than we are tracking the entry, - // but it is not available for use yet - console.log(`request the item '${id}' at ${manager}`); + if (ids?.sonarr) { + console.log('sonarr has the entry, but jellyfin does not (yet) have the file'); + return; + } - const res = await ((manager === 'radarr' ? addMovie : addSeries)(id.slice(1))); + // - If the lookup table has no entry + // than this means that we do not have the requested entry at all, + // neither in trackers nor in the media server + // + // - If the lookup table contains a jellyfin id, + // than we have the content and can stream straight away + // + // - If we have the radarr or sonarr id, + // than we are tracking the entry, + // but it is not available for use yet + console.log(`request the item '${id}' at ${manager}`); - revalidate(lookupTable.keyFor()) - + const res = await ((manager === 'radarr' ? addMovie : addSeries)(id.slice(1))); + + revalidate(lookupTable.keyFor()); }, 'content.stream'); export const listCategories = query(async (): Promise => { + 'use server'; + return [ // { label: "Continue", entries: await getContinueWatching(jellyfinUserId) }, { @@ -76,21 +77,27 @@ export const listCategories = query(async (): Promise => { }, "content.categories.list"); export const getEntryFromSlug = query( - async (slug: string): Promise => getEntry(slug.match(/\w+$/)![0]), + async (slug: string): Promise => { + 'use server'; + + return getEntry(slug.match(/\w+$/)![0]); + }, "content.getFromSlug", ); export const getEntry = query( async (id: Entry["id"]): Promise => { + 'use server'; + const [entry, userData] = await Promise.all([ getTmdbEntry(id), getEntryUserData(id) ] as const); - if (entry && userData) { + if (entry !== undefined && userData !== undefined) { entry['offset'] = ticksToSeconds(userData.playbackPositionTicks ?? 0); } - + return entry; }, "content.get", @@ -98,6 +105,8 @@ export const getEntry = query( export const getEntryUserData = query( async (id: string): ReturnType => { + 'use server'; + const table = await lookupTable(); const { jellyfin } = table[id] ?? {}; diff --git a/src/features/player/context.tsx b/src/features/player/context.tsx index deaee6e..6bb65a3 100644 --- a/src/features/player/context.tsx +++ b/src/features/player/context.tsx @@ -5,7 +5,10 @@ import { } from "@solid-primitives/context"; import { Accessor, createEffect, on, onMount, Setter } from "solid-js"; import { createStore } from "solid-js/store"; -import { createEventListenerMap } from "@solid-primitives/event-listener"; +import { + createEventListener, + createEventListenerMap, +} from "@solid-primitives/event-listener"; import { createFullscreen } from "@solid-primitives/fullscreen"; type State = "playing" | "paused"; @@ -93,6 +96,7 @@ export const [VideoProvider, useVideo] = createContextProvider< currentTime: () => store.currentTime, setTime(time) { + console.log("time is set via api!", time); video!.currentTime = time; }, @@ -124,8 +128,6 @@ export const [VideoProvider, useVideo] = createContextProvider< }, }; - console.log(props.root, props.video); - if (isServer || video === undefined) { return api; } @@ -147,15 +149,6 @@ export const [VideoProvider, useVideo] = createContextProvider< video.volume = store.volume.value; }); - createEffect(() => { - console.log(store.currentTime, props.offset); - }); - - onMount(() => { - setStore("duration", video.duration); - setStore("currentTime", video.currentTime); - }); - createEventListenerMap(video, { play(e) { setStore("state", "playing"); @@ -167,6 +160,8 @@ export const [VideoProvider, useVideo] = createContextProvider< setStore("duration", video.duration); }, timeupdate(e) { + console.log("time update", video.currentTime, e); + setStore("currentTime", video.currentTime); }, volumeChange() { @@ -175,14 +170,15 @@ export const [VideoProvider, useVideo] = createContextProvider< progress(e) { const timeRanges = video.buffered; - setStore( - "buffered", - timeRanges.length > 0 ? timeRanges.end(timeRanges.length - 1) : 0 - ); - }, - canplay() { - console.log("can play!"); - // setStore("loading", false); + if (timeRanges.length === 0) { + return; + } + + const range = timeRanges.end(timeRanges.length - 1); + + console.log(range); + + setStore("buffered", range); }, canplaythrough() { console.log("can play through!"); @@ -191,6 +187,10 @@ export const [VideoProvider, useVideo] = createContextProvider< waiting() { setStore("loading", true); }, + loadedmetadata() { + console.log("metadata loaded"); + video.currentTime = props.offset ?? 0; + }, }); return api; diff --git a/src/features/player/controls/seekBar.tsx b/src/features/player/controls/seekBar.tsx index bc83143..a78ce2d 100644 --- a/src/features/player/controls/seekBar.tsx +++ b/src/features/player/controls/seekBar.tsx @@ -1,4 +1,4 @@ -import { Component } from "solid-js"; +import { Component, createEffect, on } from "solid-js"; import { useVideo } from "../context"; import css from "./seekBar.module.css"; @@ -7,6 +7,15 @@ interface SeekBarProps {} export const SeekBar: Component = () => { const video = useVideo(); + createEffect( + on( + () => [video.duration(), video.buffered(), video.currentTime()] as const, + ([duration, buffered, currentTime]) => { + console.log({ duration, buffered, currentTime }); + } + ) + ); + return (
{formatTime(video.currentTime())} diff --git a/src/features/player/player.tsx b/src/features/player/player.tsx index aab9adf..b426e07 100644 --- a/src/features/player/player.tsx +++ b/src/features/player/player.tsx @@ -75,7 +75,14 @@ export const Player: Component = (props) => { // : ""; // }); - // createEffect(on(thumbnails, (thumbnails) => {})); + // createEffect( + // on( + // () => props.entry, + // (entry) => { + // console.log(entry); + // } + // ) + // ); return ( <> @@ -86,8 +93,6 @@ export const Player: Component = (props) => { props.entry["offset"] ? `#t=${props.entry["offset"]}` : "" }`} poster={props.entry.image} - lang="en" - autoplay > {/* => { "use server"; - const cookies = Object.fromEntries( - getRequestEvent()! - .request.headers.get("cookie")! - .split(";") - .map((c) => c.trim()) - .map((cookie) => { - const index = cookie.indexOf("="); - - return [ - cookie.slice(0, index), - decodeURIComponent(cookie.slice(index + 1)), - ]; - }) - ); - const session = await auth.api.getSession(getRequestEvent()!.request); if (session === null) { return undefined; } - const { - preferred_username, - name, - email, - image = null, - ...user - } = session.user; + const { username, name, email, image = null } = session.user; - return { username: preferred_username, name, email, image }; + return { username, name, email, image }; }, "session"); export const route = { @@ -52,18 +31,14 @@ export default function ShellPage(props: ParentProps) { const user = createAsync(() => load()); const themeContext = useTheme(); - createEffect(() => { - console.log(user()); - }); - - createEffect( - on( - () => themeContext.theme.colorScheme, - (colorScheme) => { - document.documentElement.dataset.theme = colorScheme; - } - ) - ); + // createEffect( + // on( + // () => themeContext.theme.colorScheme, + // (colorScheme) => { + // document.documentElement.dataset.theme = colorScheme; + // } + // ) + // ); return ( diff --git a/src/routes/(shell)/play/[slug].tsx b/src/routes/(shell)/play/[slug].tsx index 1900a29..cc08380 100644 --- a/src/routes/(shell)/play/[slug].tsx +++ b/src/routes/(shell)/play/[slug].tsx @@ -7,7 +7,7 @@ import { RouteDefinition, useParams, } from "@solidjs/router"; -import { Show } from "solid-js"; +import { createEffect, Show } from "solid-js"; import { createSlug, getEntryFromSlug } from "~/features/content"; import { Player } from "~/features/player"; import { Title } from "@solidjs/meta"; @@ -54,10 +54,10 @@ export default function Item() { return (
- {entry()?.title} {(entry) => ( <> + {entry().title} )} diff --git a/src/routes/api/auth/[...action].ts b/src/routes/api/auth/[...action].ts index 826d1ce..dfae5e8 100644 --- a/src/routes/api/auth/[...action].ts +++ b/src/routes/api/auth/[...action].ts @@ -1,4 +1,4 @@ -import { auth } from "~/auth"; +import { auth } from "~/auth.server"; import { toSolidStartHandler } from "better-auth/solid-start"; export const { GET, POST } = toSolidStartHandler(auth); diff --git a/src/routes/sitemap.xml.ts b/src/routes/sitemap.xml.ts index 4cdfeea..898a3e8 100644 --- a/src/routes/sitemap.xml.ts +++ b/src/routes/sitemap.xml.ts @@ -1,7 +1,7 @@ import { SitemapStream, streamToPromise } from 'sitemap' -import { App } from 'vinxi'; +import { App, } from 'vinxi'; -const BASE_URL = 'https://ca-euw-prd-calque-app.purplecoast-f5b7f657.westeurope.azurecontainerapps.io'; +const BASE_URL = 'http://localhost:3000'; export async function GET() { @@ -22,7 +22,13 @@ export async function GET() { } const getRoutes = async () => { - const router = ((globalThis as any).app as App).getRouter('client').internals.routes; + const app = (globalThis as any).app as App; + + const kaas = app.getRouter('client'); + + console.log(kaas.internals); + + const router = app.getRouter('client').internals?.routes; if (router === undefined) { return []; diff --git a/tsconfig.json b/tsconfig.json index 4b4fbe5..3709ff6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "jsx": "preserve",