worked on various things

This commit is contained in:
Chris Kruining 2025-06-17 16:13:40 +02:00
parent f3cb35653e
commit 3c35b89250
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
18 changed files with 156 additions and 126 deletions

View file

@ -1,14 +1,15 @@
import { defineConfig } from "@solidjs/start/config"; import { defineConfig } from "@solidjs/start/config";
import solidSvg from "vite-plugin-solid-svg"; import solidSvg from "vite-plugin-solid-svg";
import devtools from "solid-devtools/vite"; import devtools from "solid-devtools/vite";
import { build, fileURLToPath } from "bun";
export default defineConfig({ export default defineConfig({
vite: { vite: {
plugins: [ plugins: [
devtools({ // devtools({
autoname: true, // autoname: true,
}), // }),
solidSvg(), // solidSvg(),
], ],
}, },
solid: { solid: {
@ -19,7 +20,7 @@ export default defineConfig({
server: { server: {
preset: "bun", preset: "bun",
prerender: { prerender: {
routes: ["/sitemaps.xml"], routes: ["/sitemap.xml"],
}, },
}, },
}); });

Binary file not shown.

View file

@ -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);

23
flake.nix Normal file
View file

@ -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;
};
});
}

6
src/auth.client.ts Normal file
View file

@ -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()],
});

View file

@ -1,7 +1,6 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins"; import { genericOAuth } from "better-auth/plugins";
import { createAuthClient } from "better-auth/solid"; import { profile } from "bun:jsc";
import { genericOAuthClient } from "better-auth/client/plugins";
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
export const auth = betterAuth({ export const auth = betterAuth({
@ -20,18 +19,10 @@ export const auth = betterAuth({
type: "string", type: "string",
nullable: true, nullable: true,
}, },
preferred_username: {
type: "string",
nullable: true,
},
username: { username: {
type: "string", type: "string",
nullable: true, nullable: true,
}, },
profile: {
type: "string",
nullable: true,
},
}, },
}, },
plugins: [ plugins: [
@ -54,12 +45,10 @@ export const auth = betterAuth({
], ],
accessType: "offline", accessType: "offline",
pkce: true, 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()],
});

View file

@ -219,7 +219,7 @@ export const getItemStream = query(
itemId, itemId,
}, },
query: { query: {
startTimeTicks: userData?.playbackPositionTicks, // startTimeTicks: userData?.playbackPositionTicks,
}, },
}, },
parseAs: 'stream', parseAs: 'stream',
@ -392,7 +392,7 @@ const toEntry = (item: components['schemas']['BaseItemDto']): Entry => {
id: `${type}${item.ProviderIds!["Tmdb"]!}`, id: `${type}${item.ProviderIds!["Tmdb"]!}`,
title: item.Name!, title: item.Name!,
overview: item.Overview!, overview: item.Overview!,
thumbnail: new URL(`/Items/${item.Id}/Images/Primary`, getBaseUrl()), //await getItemImage(data.Id!, 'Primary'), thumbnail: new URL(`/Items/${item.Id}/Images/Primary`, getBaseUrl()),
image: new URL(`/Items/${item.Id}/Images/Backdrop`, getBaseUrl()), image: new URL(`/Items/${item.Id}/Images/Backdrop`, getBaseUrl()),
providers: { providers: {
jellyfin: item.Id jellyfin: item.Id

View file

@ -1,5 +1,3 @@
"use server";
import type { Category, Entry } from "./types"; import type { Category, Entry } from "./types";
import { query, revalidate } from "@solidjs/router"; import { query, revalidate } from "@solidjs/router";
import { listItemIds, getContinueWatching, getItemStream, getRandomItems, getItemUserData } from "./apis/jellyfin"; import { listItemIds, getContinueWatching, getItemStream, getRandomItems, getItemUserData } from "./apis/jellyfin";
@ -26,6 +24,8 @@ const lookupTable = query(async () => {
export const getHighlights = () => getContinueWatching(jellyfinUserId); export const getHighlights = () => getContinueWatching(jellyfinUserId);
export const getStream = query(async (id: string, range: string) => { export const getStream = query(async (id: string, range: string) => {
'use server';
const table = await lookupTable(); const table = await lookupTable();
const ids = table[id]; const ids = table[id];
const manager = id[0] === 'm' ? 'radarr' : 'sonarr' const manager = id[0] === 'm' ? 'radarr' : 'sonarr'
@ -59,11 +59,12 @@ export const getStream = query(async (id: string, range: string) => {
const res = await ((manager === 'radarr' ? addMovie : addSeries)(id.slice(1))); const res = await ((manager === 'radarr' ? addMovie : addSeries)(id.slice(1)));
revalidate(lookupTable.keyFor()) revalidate(lookupTable.keyFor());
}, 'content.stream'); }, 'content.stream');
export const listCategories = query(async (): Promise<Category[]> => { export const listCategories = query(async (): Promise<Category[]> => {
'use server';
return [ return [
// { label: "Continue", entries: await getContinueWatching(jellyfinUserId) }, // { label: "Continue", entries: await getContinueWatching(jellyfinUserId) },
{ {
@ -76,18 +77,24 @@ export const listCategories = query(async (): Promise<Category[]> => {
}, "content.categories.list"); }, "content.categories.list");
export const getEntryFromSlug = query( export const getEntryFromSlug = query(
async (slug: string): Promise<Entry | undefined> => getEntry(slug.match(/\w+$/)![0]), async (slug: string): Promise<Entry | undefined> => {
'use server';
return getEntry(slug.match(/\w+$/)![0]);
},
"content.getFromSlug", "content.getFromSlug",
); );
export const getEntry = query( export const getEntry = query(
async (id: Entry["id"]): Promise<Entry | undefined> => { async (id: Entry["id"]): Promise<Entry | undefined> => {
'use server';
const [entry, userData] = await Promise.all([ const [entry, userData] = await Promise.all([
getTmdbEntry(id), getTmdbEntry(id),
getEntryUserData(id) getEntryUserData(id)
] as const); ] as const);
if (entry && userData) { if (entry !== undefined && userData !== undefined) {
entry['offset'] = ticksToSeconds(userData.playbackPositionTicks ?? 0); entry['offset'] = ticksToSeconds(userData.playbackPositionTicks ?? 0);
} }
@ -98,6 +105,8 @@ export const getEntry = query(
export const getEntryUserData = query( export const getEntryUserData = query(
async (id: string): ReturnType<typeof getItemUserData> => { async (id: string): ReturnType<typeof getItemUserData> => {
'use server';
const table = await lookupTable(); const table = await lookupTable();
const { jellyfin } = table[id] ?? {}; const { jellyfin } = table[id] ?? {};

View file

@ -5,7 +5,10 @@ import {
} from "@solid-primitives/context"; } from "@solid-primitives/context";
import { Accessor, createEffect, on, onMount, Setter } from "solid-js"; import { Accessor, createEffect, on, onMount, Setter } from "solid-js";
import { createStore } from "solid-js/store"; 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"; import { createFullscreen } from "@solid-primitives/fullscreen";
type State = "playing" | "paused"; type State = "playing" | "paused";
@ -93,6 +96,7 @@ export const [VideoProvider, useVideo] = createContextProvider<
currentTime: () => store.currentTime, currentTime: () => store.currentTime,
setTime(time) { setTime(time) {
console.log("time is set via api!", time);
video!.currentTime = time; video!.currentTime = time;
}, },
@ -124,8 +128,6 @@ export const [VideoProvider, useVideo] = createContextProvider<
}, },
}; };
console.log(props.root, props.video);
if (isServer || video === undefined) { if (isServer || video === undefined) {
return api; return api;
} }
@ -147,15 +149,6 @@ export const [VideoProvider, useVideo] = createContextProvider<
video.volume = store.volume.value; video.volume = store.volume.value;
}); });
createEffect(() => {
console.log(store.currentTime, props.offset);
});
onMount(() => {
setStore("duration", video.duration);
setStore("currentTime", video.currentTime);
});
createEventListenerMap(video, { createEventListenerMap(video, {
play(e) { play(e) {
setStore("state", "playing"); setStore("state", "playing");
@ -167,6 +160,8 @@ export const [VideoProvider, useVideo] = createContextProvider<
setStore("duration", video.duration); setStore("duration", video.duration);
}, },
timeupdate(e) { timeupdate(e) {
console.log("time update", video.currentTime, e);
setStore("currentTime", video.currentTime); setStore("currentTime", video.currentTime);
}, },
volumeChange() { volumeChange() {
@ -175,14 +170,15 @@ export const [VideoProvider, useVideo] = createContextProvider<
progress(e) { progress(e) {
const timeRanges = video.buffered; const timeRanges = video.buffered;
setStore( if (timeRanges.length === 0) {
"buffered", return;
timeRanges.length > 0 ? timeRanges.end(timeRanges.length - 1) : 0 }
);
}, const range = timeRanges.end(timeRanges.length - 1);
canplay() {
console.log("can play!"); console.log(range);
// setStore("loading", false);
setStore("buffered", range);
}, },
canplaythrough() { canplaythrough() {
console.log("can play through!"); console.log("can play through!");
@ -191,6 +187,10 @@ export const [VideoProvider, useVideo] = createContextProvider<
waiting() { waiting() {
setStore("loading", true); setStore("loading", true);
}, },
loadedmetadata() {
console.log("metadata loaded");
video.currentTime = props.offset ?? 0;
},
}); });
return api; return api;

View file

@ -1,4 +1,4 @@
import { Component } from "solid-js"; import { Component, createEffect, on } from "solid-js";
import { useVideo } from "../context"; import { useVideo } from "../context";
import css from "./seekBar.module.css"; import css from "./seekBar.module.css";
@ -7,6 +7,15 @@ interface SeekBarProps {}
export const SeekBar: Component<SeekBarProps> = () => { export const SeekBar: Component<SeekBarProps> = () => {
const video = useVideo(); const video = useVideo();
createEffect(
on(
() => [video.duration(), video.buffered(), video.currentTime()] as const,
([duration, buffered, currentTime]) => {
console.log({ duration, buffered, currentTime });
}
)
);
return ( return (
<div class={css.container}> <div class={css.container}>
<span class={css.time}>{formatTime(video.currentTime())}</span> <span class={css.time}>{formatTime(video.currentTime())}</span>

View file

@ -75,7 +75,14 @@ export const Player: Component<PlayerProps> = (props) => {
// : ""; // : "";
// }); // });
// createEffect(on(thumbnails, (thumbnails) => {})); // createEffect(
// on(
// () => props.entry,
// (entry) => {
// console.log(entry);
// }
// )
// );
return ( return (
<> <>
@ -86,8 +93,6 @@ export const Player: Component<PlayerProps> = (props) => {
props.entry["offset"] ? `#t=${props.entry["offset"]}` : "" props.entry["offset"] ? `#t=${props.entry["offset"]}` : ""
}`} }`}
poster={props.entry.image} poster={props.entry.image}
lang="en"
autoplay
> >
{/* <track {/* <track
default default

View file

@ -1,5 +1,5 @@
import { Component, Show } from "solid-js"; import { Component, Show } from "solid-js";
import { signIn, signOut, client } from "~/auth"; import { signIn, signOut, client } from "~/auth.client";
import { Avatar, Profile, User } from "../user"; import { Avatar, Profile, User } from "../user";
import { ColorSchemePicker } from "../theme"; import { ColorSchemePicker } from "../theme";
import css from "./top.module.css"; import css from "./top.module.css";

View file

@ -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 });
}, },
}; };
}, },

View file

@ -2,7 +2,7 @@ import { Meta } from "@solidjs/meta";
import { query, createAsync } from "@solidjs/router"; import { query, createAsync } from "@solidjs/router";
import { createEffect, on, ParentProps } from "solid-js"; import { createEffect, on, ParentProps } from "solid-js";
import { getRequestEvent } from "solid-js/web"; import { getRequestEvent } from "solid-js/web";
import { auth } from "~/auth"; import { auth } from "~/auth.server";
import { Shell } from "~/features/shell"; import { Shell } from "~/features/shell";
import { useTheme } from "~/features/theme"; import { useTheme } from "~/features/theme";
import { User } from "~/features/user"; import { User } from "~/features/user";
@ -10,36 +10,15 @@ import { User } from "~/features/user";
const load = query(async (): Promise<User | undefined> => { const load = query(async (): Promise<User | undefined> => {
"use server"; "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); const session = await auth.api.getSession(getRequestEvent()!.request);
if (session === null) { if (session === null) {
return undefined; return undefined;
} }
const { const { username, name, email, image = null } = session.user;
preferred_username,
name,
email,
image = null,
...user
} = session.user;
return { username: preferred_username, name, email, image }; return { username, name, email, image };
}, "session"); }, "session");
export const route = { export const route = {
@ -52,18 +31,14 @@ export default function ShellPage(props: ParentProps) {
const user = createAsync(() => load()); const user = createAsync(() => load());
const themeContext = useTheme(); const themeContext = useTheme();
createEffect(() => { // createEffect(
console.log(user()); // on(
}); // () => themeContext.theme.colorScheme,
// (colorScheme) => {
createEffect( // document.documentElement.dataset.theme = colorScheme;
on( // }
() => themeContext.theme.colorScheme, // )
(colorScheme) => { // );
document.documentElement.dataset.theme = colorScheme;
}
)
);
return ( return (
<Shell user={user()}> <Shell user={user()}>

View file

@ -7,7 +7,7 @@ import {
RouteDefinition, RouteDefinition,
useParams, useParams,
} from "@solidjs/router"; } from "@solidjs/router";
import { Show } from "solid-js"; import { createEffect, Show } from "solid-js";
import { createSlug, getEntryFromSlug } from "~/features/content"; import { createSlug, getEntryFromSlug } from "~/features/content";
import { Player } from "~/features/player"; import { Player } from "~/features/player";
import { Title } from "@solidjs/meta"; import { Title } from "@solidjs/meta";
@ -54,10 +54,10 @@ export default function Item() {
return ( return (
<div class={css.page}> <div class={css.page}>
<Title>{entry()?.title}</Title>
<Show when={entry()} fallback="Some kind of pretty 404 page I guess"> <Show when={entry()} fallback="Some kind of pretty 404 page I guess">
{(entry) => ( {(entry) => (
<> <>
<Title>{entry().title}</Title>
<Player entry={entry()} /> <Player entry={entry()} />
</> </>
)} )}

View file

@ -1,4 +1,4 @@
import { auth } from "~/auth"; import { auth } from "~/auth.server";
import { toSolidStartHandler } from "better-auth/solid-start"; import { toSolidStartHandler } from "better-auth/solid-start";
export const { GET, POST } = toSolidStartHandler(auth); export const { GET, POST } = toSolidStartHandler(auth);

View file

@ -1,7 +1,7 @@
import { SitemapStream, streamToPromise } from 'sitemap' 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() { export async function GET() {
@ -22,7 +22,13 @@ export async function GET() {
} }
const getRoutes = async () => { 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) { if (router === undefined) {
return []; return [];

View file

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"jsx": "preserve", "jsx": "preserve",