This commit is contained in:
Chris Kruining 2025-04-16 00:43:31 +02:00
parent 0eb2e34e60
commit a15809f4fd
Signed by: chris
SSH key fingerprint: SHA256:nG82MUfuVdRVyCKKWqhY+pCrbz9nbX6uzUns4RKa1Pg
11 changed files with 32770 additions and 82 deletions

View file

@ -4,19 +4,33 @@ import { createAuthClient } from "better-auth/solid";
import { genericOAuthClient } from "better-auth/client/plugins";
export const auth = betterAuth({
appName: 'Streamarr',
basePath: '/api/auth',
appName: "Streamarr",
basePath: "/api/auth",
advanced: {
useSecureCookies: true,
crossSubDomainCookies: {
enabled: true,
},
},
logger: {
level: 'debug',
level: "info",
},
onAPIError: {
throw: true,
user: {
additionalFields: {
name: {
type: "string",
nullable: true,
},
preferred_username: {
type: "string",
nullable: true,
},
username: {
type: "string",
nullable: true,
},
profile: {
type: "string",
nullable: true,
},
},
},
plugins: [
genericOAuth({
@ -28,7 +42,14 @@ export const auth = betterAuth({
"ZPuiW2gpVV6MGXIJFk5P3EeSW8V_ICgqduF.hJVCKkrnVmRqIQXRk0o~HSA8ZdCf8joA4m_F",
discoveryUrl:
"https://auth.kruining.eu/.well-known/openid-configuration",
scopes: ["openid", "email", "picture", "profile", "groups"],
scopes: [
"offline_access",
"openid",
"email",
"picture",
"profile",
"groups",
],
accessType: "offline",
pkce: true,
},

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
import createClient from "openapi-fetch";
import type { paths } from "./jellyfin.generated"; // generated by openapi-typescript
import { query } from "@solidjs/router";
import { Entry } from "../types";
const baseUrl = "http://ulmo:8096/";
const client = createClient<paths>({
baseUrl,
headers: {
Authorization: `MediaBrowser DeviceId="Streamarr", Token="b3c44db1e31f4349b19d1ff0bc487da2"`,
},
});
export const getItem = query(async (userId: string, itemId: string) => {
const { data, error } = await client.GET("/Items/{itemId}", {
params: {
path: {
itemId,
},
query: {
userId,
hasTmdbInfo: true,
recursive: true,
includeItemTypes: ["Movie", "Series"],
fields: [
"ProviderIds",
"Genres",
"DateLastMediaAdded",
"DateCreated",
"MediaSources",
],
},
},
});
return data?.Items ?? [];
}, "jellyfin.getItem");
export const getContinueWatching = query(
async (userId: string): Promise<Entry[]> => {
const { data, error } = await client.GET("/Users/{userId}/Items/Resume", {
params: {
path: {
userId,
},
query: {
mediaTypes: ["Video"],
fields: ["ProviderIds", "Genres"],
},
},
});
const items = (data?.Items ?? []).map(({ Id, Name }) => ({
id: Id,
title: Name,
thumbnail: `${baseUrl}Items/${Id}/Images/Primary`,
}));
return items;
},
"jellyfin.continueWatching",
);

View file

@ -69,3 +69,5 @@ export const getEntry = query(
},
"series.get",
);
export { getContinueWatching } from "./apis/jellyfin";

View file

@ -3,6 +3,7 @@ import { signIn, signOut } from "~/auth";
import { hash } from "~/utilities";
import { Avatar, Profile, User } from "../user";
import css from "./top.module.css";
import { ColorSchemePicker } from "../theme";
interface TopProps {
user: User | undefined;
@ -53,7 +54,7 @@ export const Top: Component<TopProps> = (props) => {
</>
)}
</Show>
{/* <ColorSchemePicker /> */}
<ColorSchemePicker />
</aside>
);
};

View file

@ -1,80 +1,88 @@
import { ContextProviderProps, createContextProvider } from "@solid-primitives/context";
import {
ContextProviderProps,
createContextProvider,
} from "@solid-primitives/context";
import { action, createAsyncStore, query, useAction } from "@solidjs/router";
import { createStore } from "solid-js/store";
import { useSession } from "vinxi/http";
export enum ColorScheme {
Auto = 'light dark',
Light = 'light',
Dark = 'dark',
Auto = "light dark",
Light = "light",
Dark = "dark",
}
export interface State {
colorScheme: ColorScheme;
hue: number;
colorScheme: ColorScheme;
hue: number;
}
const getSession = async () => {
'use server';
"use server";
return useSession<State>({
password: process.env.SESSION_SECRET!,
});
return useSession<State>({
password: process.env.SESSION_SECRET!,
});
};
export const getState = query(async () => {
'use server';
"use server";
const session = await getSession();
const session = await getSession();
if (Object.getOwnPropertyNames(session.data).length === 0) {
await session.update({
colorScheme: ColorScheme.Auto,
hue: 0,
})
}
if (Object.getOwnPropertyNames(session.data).length === 0) {
await session.update({
colorScheme: ColorScheme.Auto,
hue: 0,
});
}
return session.data;
}, 'color-scheme');
return session.data;
}, "color-scheme");
const setState = action(async (state: State) => {
'use server';
"use server";
const session = await getSession();
await session.update(prev => ({ ...prev, ...state }));
}, 'color-scheme');
const session = await getSession();
await session.update((prev) => ({ ...prev, ...state }));
}, "color-scheme");
interface ThemeContextType {
readonly theme: State;
setColorScheme(colorScheme: ColorScheme): void;
setHue(colorScheme: number): void;
readonly theme: State;
setColorScheme(colorScheme: ColorScheme): void;
setHue(colorScheme: number): void;
}
const [ThemeContextProvider, useTheme] = createContextProvider<ThemeContextType, ContextProviderProps>((props) => {
const [ThemeContextProvider, useTheme] = createContextProvider<
ThemeContextType,
ContextProviderProps
>(
(props) => {
const updateState = useAction(setState);
const state = createAsyncStore(() => getState());
return {
get theme() {
return state.latest ?? { colorScheme: null };
},
get theme() {
return state.latest ?? { colorScheme: null };
},
setColorScheme(colorScheme) {
updateState({ colorScheme, hue: state.latest!.hue });
},
setHue(hue) {
updateState({ hue, colorScheme: state.latest!.colorScheme });
},
setColorScheme(colorScheme) {
// updateState({ colorScheme, hue: state.latest!.hue });
},
setHue(hue) {
// updateState({ hue, colorScheme: state.latest!.colorScheme });
},
};
}, {
},
{
theme: {
colorScheme: ColorScheme.Auto,
hue: 180,
colorScheme: ColorScheme.Auto,
hue: 180,
},
setColorScheme(colorScheme) { },
setHue(hue) { },
});
setColorScheme(colorScheme) {},
setHue(hue) {},
},
);
export { ThemeContextProvider, useTheme };
export { ThemeContextProvider, useTheme };

View file

@ -1,4 +1,5 @@
export interface User {
username: string;
name: string;
email: string;
image: string | null;

View file

@ -18,9 +18,15 @@ const load = query(async (): Promise<User | undefined> => {
return undefined;
}
const { name, email, image = null } = session.user;
const {
preferred_username,
name,
email,
image = null,
...user
} = session.user;
return { name, email, image };
return { username: preferred_username, name, email, image };
}, "session");
export const route = {

View file

@ -1,34 +1,40 @@
import { Title } from "@solidjs/meta";
import { createAsync, query } from "@solidjs/router";
import { createAsync } from "@solidjs/router";
import { Overview } from "~/features/overview";
import { listCategories, getEntry } from "~/features/content";
import { createEffect, Show } from "solid-js";
const load = query(async () => {
"use server";
// const response =
}, "home.data");
import {
listCategories,
getEntry,
getContinueWatching,
} from "~/features/content";
import { Show } from "solid-js";
import { List } from "~/components/list";
import { ListItem } from "~/features/overview/list-item";
export const route = {
preload: async () => ({
highlight: await getEntry("14"),
categories: await listCategories(),
continue: await getContinueWatching("a9c51af84bf54578a99ab4dd0ebf0763"),
}),
};
export default function Home() {
const highlight = createAsync(() => getEntry("14"));
const categories = createAsync(() => listCategories());
createEffect(() => {
console.log(highlight(), categories());
});
const continueWatching = createAsync(() =>
getContinueWatching("a9c51af84bf54578a99ab4dd0ebf0763"),
);
return (
<>
<Title>Home</Title>
<Show when={continueWatching()}>
<List label="Continue watching" items={continueWatching()}>
{(item) => <ListItem entry={item()} />}
</List>
</Show>
<Show when={highlight() && categories()}>
<Overview highlight={highlight()!} categories={categories()!} />
</Show>