Compare commits

...

5 Commits

Author SHA1 Message Date
Max Richter
47d32d68c7 fix: lazily import sharp to fix commonjs error 2026-01-10 20:13:15 +01:00
Max Richter
c74a19b527 fix(ci): make dockerfile work 2026-01-10 19:49:07 +01:00
Max Richter
8d712322c0 fix: make it work with new vite 2026-01-10 19:28:09 +01:00
Max Richter
694feb083d fix: remove all linter errors 2026-01-10 15:06:43 +01:00
Max Richter
e55f787a29 feat: trying to add hashes to scripts 2026-01-10 13:03:29 +01:00
132 changed files with 4184 additions and 1959 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,3 @@
# dotenv environment variable files
.env
.env.development.local
.env.test.local
@@ -10,3 +9,4 @@ data-dev/
_fresh/
node_modules/
mise.toml

View File

@@ -1,10 +1,10 @@
FROM denoland/deno:2.5.4 AS build
FROM denoland/deno:2.6.4 AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ffmpeg && \
deno run -A npm:playwright install --with-deps firefox &&\
apt-get clean && \
rm -rf /var/lib/apt/lists/*
curl ffmpeg && \
deno run -A npm:playwright install --with-deps firefox &&\
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
@@ -15,11 +15,11 @@ COPY . .
ENV DATA_DIR=/app/data
RUN mkdir -p $DATA_DIR && \
deno install --allow-import --allow-ffi --allow-scripts=npm:sharp -e main.ts &&\
sed -i -e 's/"deno"/"no-deno"/' node_modules/@libsql/client/package.json &&\
deno task build
deno install --allow-import --allow-ffi -e main.ts &&\
sed -i -e 's/"deno"/"no-deno"/' node_modules/@libsql/client/package.json &&\
deno install npm:@libsql/linux-x64-gnu &&\
deno task build
EXPOSE 8000
CMD ["run", "-A", "main.ts"]
CMD ["task", "start"]

View File

@@ -5,18 +5,13 @@ Started" guide here: https://fresh.deno.dev/docs/getting-started
### Usage
Make sure to install Deno: https://deno.land/manual/getting_started/installation
Make sure to install Deno:
https://docs.deno.com/runtime/getting_started/installation
Then start the project:
Then start the project in development mode:
```
deno task start
deno task dev
```
This will watch the project directory and restart as necessary.
## FIX
```
sed -i -e 's/"deno"/"no-deno"/' node_modules/@libsql/client/package.json
```

1
assets/styles.css Normal file
View File

@@ -0,0 +1 @@
@import "tailwindcss";

2
client.ts Normal file
View File

@@ -0,0 +1,2 @@
// Import CSS files here for hot module reloading to work.
import "./assets/styles.css";

View File

@@ -1,12 +1,10 @@
import { JSX } from "preact";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { ButtonHTMLAttributes } from "preact";
export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
export function Button(props: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
disabled={!IS_BROWSER || props.disabled}
class={`px-2 py-1 ${props.class ? props.class : " "}`}
class={`cursor-pointer px-2 py-1 ${props.class ? props.class : ""}`}
/>
);
}

View File

@@ -1,7 +1,6 @@
import { isYoutubeLink } from "@lib/string.ts";
import { IconBrandYoutube } from "@components/icons.tsx";
import { SmallRating } from "@components/Rating.tsx";
import { Link } from "@islands/Link.tsx";
import { parseRating } from "@lib/helpers.ts";
import { GenericResource, getNameOfResource } from "@lib/marka/schema.ts";
@@ -24,7 +23,7 @@ export function Card(
rating?: number;
},
) {
const backgroundStyle: preact.JSX.CSSProperties = {
const backgroundStyle: preact.CSSProperties = {
backgroundSize: "cover",
backgroundColor: backgroundColor,
};
@@ -36,7 +35,7 @@ export function Card(
}
return (
<Link
<a
href={link}
style={backgroundStyle}
data-thumb={thumbhash}
@@ -88,7 +87,7 @@ export function Card(
)}
</div>
<div class="absolute inset-x-0 bottom-0 h-3/4" />
</Link>
</a>
);
}

View File

@@ -1,67 +1,5 @@
import { Signal, useSignal } from "@preact/signals";
import { useId, useState } from "preact/hooks";
interface CheckboxProps {
label: string;
isChecked?: boolean;
onChange: (isChecked: boolean) => void;
}
const Checkbox2: preact.FunctionalComponent<CheckboxProps> = (
{ label, isChecked = false, onChange },
) => {
const [checked, setChecked] = useState(isChecked);
const toggleCheckbox = () => {
const newChecked = !checked;
setChecked(newChecked);
onChange(newChecked);
};
return (
<div
class="flex items-center rounded-xl p-1 pl-4"
style={{ background: "var(--background)", color: "var(--foreground)" }}
>
<span>
{label}
</span>
<label
class="relative flex cursor-pointer items-center rounded-full p-3"
for="checkbox"
data-ripple-dark="true"
>
<input
type="checkbox"
class="before:content[''] peer relative h-5 w-5 cursor-pointer appearance-none rounded-md border border-blue-gray-200 transition-all before:absolute before:top-2/4 before:left-2/4 before:block before:h-12 before:w-12 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full before:bg-blue-gray-500 before:opacity-0 before:transition-opacity hover:before:opacity-10"
id="checkbox"
checked
/>
<div
class={`pointer-events-none absolute top-2/4 left-2/4 -translate-y-2/4 -translate-x-2/4 text-white opacity-${
checked ? 100 : 0
} transition-opacity`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5"
viewBox="0 0 20 20"
fill="white"
stroke="currentColor"
stroke-width="1"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
>
</path>
</svg>
</div>
</label>
</div>
);
};
import { useId } from "preact/hooks";
const Checkbox = (
{ label, checked = useSignal(false) }: {

View File

@@ -1,4 +1,4 @@
import { asset } from "$fresh/runtime.ts";
import { asset } from "fresh/runtime";
export const Emoji = (props: { class?: string; name: string }) => {
return props.name

View File

@@ -1,5 +1,4 @@
import { asset } from "$fresh/runtime.ts";
import * as CSS from "csstype";
import { asset } from "fresh/runtime";
interface ResponsiveAttributes {
srcset: string;
@@ -38,7 +37,7 @@ const Image = (
fill?: boolean;
width?: number | string;
height?: number | string;
style?: CSS.HtmlAttributes;
style?: preact.CSSProperties;
},
) => {
const responsiveAttributes = generateResponsiveAttributes(

View File

@@ -1,4 +1,4 @@
import { Head } from "$fresh/runtime.ts";
import { Head } from "fresh/runtime";
import { GenericResource, getNameOfResource } from "@lib/marka/schema.ts";
import { formatDate } from "@lib/string.ts";
@@ -60,6 +60,7 @@ export function MetaTags({ resource }: { resource: GenericResource }) {
/>
<script
type="application/ld+json"
// deno-lint-ignore react-no-danger
dangerouslySetInnerHTML={{ __html: jsonLd }}
/>
</Head>

View File

@@ -21,7 +21,7 @@ function Wrapper(
return (
<div
class={`flex justify-between flex-col relative w-full ${
image ? "min-h-[400px]" : "min-h-[200px]"
image ? "min-h-100" : "min-h-50"
} rounded-3xl overflow-hidden`}
>
<HeroContext.Provider value={{ image }}>
@@ -62,7 +62,7 @@ function Title(
>
{children}
{link &&
<IconExternalLink />}
<IconExternalLink class="h-6 w-6" />}
</h2>
</OuterTag>
);
@@ -110,7 +110,7 @@ function Subline(
const ctx = useContext(HeroContext);
return (
<div
class={`relative flex items-center z-10 flex gap-5 font-sm text-light mt-3`}
class={`relative items-center z-10 flex gap-5 font-sm text-light mt-3`}
style={{ color: ctx.image ? "#1F1F1F" : "white" }}
>
{children}

View File

@@ -10,9 +10,9 @@ export const Star = (
>
{Array.from({ length: max }).map((_, i) => {
if ((i + 1) <= rating) {
return <IconStarFilled class="w-4 h-4" />;
return <IconStarFilled key={i} class="w-4 h-4" />;
}
return <IconStar class="w-4 h-4" />;
return <IconStar key={i} class="w-4 h-4" />;
})}
</div>
);

View File

@@ -1,21 +1,23 @@
export { default as IconStar } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/star.tsx";
export { default as IconStarFilled } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/star-filled.tsx";
export { default as IconExternalLink } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/external-link.tsx";
export { default as IconArrowNarrowLeft } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-narrow-left.tsx";
export { default as IconEdit } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/edit.tsx";
export { default as IconArrowLeft } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx";
export { default as IconError404 } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/error-404.tsx";
export { default as IconSquareRoundedPlus } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/square-rounded-plus.tsx";
export { default as IconReportSearch } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/report-search.tsx";
export { default as IconRefresh } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/refresh.tsx";
export { default as IconCirclePlus } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/circle-plus.tsx";
export { default as IconCircleMinus } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/circle-minus.tsx";
export { default as IconLoader2 } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/loader-2.tsx";
export { default as IconLogin } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/login.tsx";
export { default as IconLogout } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/logout.tsx";
export { default as IconSearch } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/search.tsx";
export { default as IconGhost } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/ghost.tsx";
export { default as IconBrandYoutube } from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/brand-youtube.tsx";
export { default as IconWand } from "https://deno.land/x/tabler_icons_tsx@0.0.5/tsx/wand.tsx";
export { default as IconMenu2 } from "https://deno.land/x/tabler_icons_tsx@0.0.5/tsx/menu-2.tsx";
export { default as IconAlertCircle } from "https://deno.land/x/tabler_icons_tsx@0.0.5/tsx/alert-circle.tsx";
export {
TbAlertCircle as IconAlertCircle,
TbArrowLeft as IconArrowLeft,
TbArrowNarrowLeft as IconArrowNarrowLeft,
TbBrandYoutube as IconBrandYoutube,
TbCircleMinus as IconCircleMinus,
TbCirclePlus as IconCirclePlus,
TbEdit as IconEdit,
TbError404 as IconError404,
TbExternalLink as IconExternalLink,
TbGhost as IconGhost,
TbLoader2 as IconLoader2,
TbLogin as IconLogin,
TbLogout as IconLogout,
TbMenu2 as IconMenu2,
TbRefresh as IconRefresh,
TbReportSearch as IconReportSearch,
TbSearch as IconSearch,
TbSquareRoundedPlus as IconSquareRoundedPlus,
TbStar as IconStar,
TbStarFilled as IconStarFilled,
TbWand as IconWand,
} from "@preact-icons/tb";

View File

@@ -36,5 +36,5 @@ export const MainLayout = (
);
}
return children;
return <>{children}</>;
};

View File

@@ -4,10 +4,10 @@ services:
context: .
dockerfile: Dockerfile
volumes:
- .:/app # Mount the local directory to /app in the container
working_dir: /app # Set the working directory inside the container to /app
command: run --env-file -A --watch=static/,routes/ dev.ts # Custom start command
- .:/app
working_dir: /app
command: deno task dev --host 0.0.0.0
ports:
- "8000:8000" # Expose the container port
- "8000:8000"
environment:
- DATA_DIR=/app/data # Set the environment variable inside the container
- DATA_DIR=/app/data

115
deno.json
View File

@@ -1,58 +1,99 @@
{
"lock": false,
"nodeModulesDir": "auto",
"unstable": ["cron"],
"nodeModulesDir": "manual",
"tasks": {
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"dev": "deno run --env-file -A --watch=static/,routes/ dev.ts",
"start": "deno run --env-file -A main.ts",
"check": "deno fmt --check . && deno lint . && deno check",
"dev": "vite",
"db": "deno run --env-file -A npm:drizzle-kit",
"build": "deno run -A dev.ts build",
"preview": "deno run -A main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
"build": "vite build",
"start": "deno serve -A _fresh/server.js",
"update": "deno run -A -r jsr:@fresh/update ."
},
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
"lint": {
"rules": {
"tags": [
"fresh",
"recommended"
]
}
},
"exclude": [
"**/_fresh/*"
],
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.7.3/",
"@cmd-johnson/oauth2-client": "jsr:@cmd-johnson/oauth2-client@^2.0.0",
"@components": "./components",
"@components/": "./components/",
"@denosaurs/emoji": "jsr:@denosaurs/emoji@^0.3.1",
"@deno/gfm": "jsr:@deno/gfm@^0.11.0",
"@islands": "./islands",
"@islands/": "./islands/",
"@lib": "./lib",
"@lib/": "./lib/",
"@libsql/client": "npm:@libsql/client@^0.14.0",
"@openai/openai": "jsr:@openai/openai@^6.7.0",
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"@std/http": "jsr:@std/http@^1.0.12",
"@std/yaml": "jsr:@std/yaml@^1.0.5",
"csstype": "npm:csstype@^3.1.3",
"@/": "./",
"@libsql/client": "npm:@libsql/client@^0.17.0",
"@libsql/linux-x64-gnu": "npm:@libsql/linux-x64-gnu@^0.5.22",
"@openai/openai": "jsr:@openai/openai@^6.16.0",
"@preact-icons/tb": "jsr:@preact-icons/tb@^1.0.14",
"@std/http": "jsr:@std/http@^1.0.23",
"@std/media-types": "jsr:@std/media-types@^1.1.0",
"@zaubrik/djwt": "jsr:@zaubrik/djwt@^3.0.2",
"defuddle": "npm:defuddle@^0.6.6",
"drizzle-kit": "npm:drizzle-kit@^0.30.1",
"drizzle-orm": "npm:drizzle-orm@^0.38.3",
"drizzle-kit": "npm:drizzle-kit@^0.31.8",
"drizzle-orm": "npm:drizzle-orm@^0.45.1",
"fresh": "jsr:@fresh/core@^2.2.0",
"fuzzysort": "npm:fuzzysort@^3.1.0",
"jsdom": "npm:jsdom@^24.1.3",
"gfm": "jsr:@deno/gfm@0.11.0",
"jsdom": "npm:jsdom@^27.4.0",
"moviedb-promise": "npm:moviedb-promise@^4.0.7",
"parse-ingredient": "npm:parse-ingredient@^1.3.1",
"playwright": "npm:playwright@^1.49.1",
"playwright-extra": "npm:playwright-extra@^4.3.6",
"preact": "https://esm.sh/preact@10.22.0",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
"preact/": "https://esm.sh/preact@10.22.0/",
"gfm": "jsr:@deno/gfm",
"preact": "npm:preact@^10.27.2",
"@preact/signals": "npm:@preact/signals@^2.5.0",
"@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.0.8",
"puppeteer-extra-plugin-stealth": "npm:puppeteer-extra-plugin-stealth@^2.11.2",
"tailwindcss": "npm:tailwindcss@^3.4.17",
"tailwindcss/": "npm:/tailwindcss@^3.4.17/",
"tailwindcss/plugin": "npm:/tailwindcss@^3.4.17/plugin.js",
"camelcase-css": "npm:camelcase-css",
"sharp": "npm:sharp@^0.34.5",
"thumbhash": "npm:thumbhash@^0.1.1",
"tsx": "npm:tsx@^4.19.2",
"turndown": "npm:turndown@^7.2.2",
"yaml": "https://deno.land/std@0.197.0/yaml/mod.ts",
"zod": "npm:zod@^3.24.1",
"fs": "https://deno.land/std/fs/mod.ts"
"vite": "npm:vite@^7.1.3",
"tailwindcss": "npm:tailwindcss@^4.1.10",
"@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.12",
"zod": "npm:zod@^4.3.5"
},
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
"exclude": ["**/_fresh/*"]
"compilerOptions": {
"lib": [
"dom",
"dom.asynciterable",
"dom.iterable",
"deno.ns"
],
"jsx": "precompile",
"jsxImportSource": "preact",
"jsxPrecompileSkipElements": [
"a",
"img",
"source",
"body",
"html",
"head",
"title",
"meta",
"script",
"link",
"style",
"base",
"noscript",
"template"
],
"types": [
"vite/client"
]
},
"allowScripts": {
"allow": [
"npm:sharp@0.34.5",
"npm:esbuild@0.27.2",
"npm:esbuild@0.18.20",
"npm:esbuild@0.25.12",
"npm:esbuild@0.25.7"
]
}
}

2912
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

8
dev.ts
View File

@@ -1,8 +0,0 @@
#!/usr/bin/env -S deno run -A --watch=static/,routes/
import dev from "$fresh/dev.ts";
import config from "./fresh.config.ts";
await dev(import.meta.url, "./main.ts", config);
Deno.exit(0);

View File

@@ -1,20 +1,22 @@
CREATE TABLE `performance` (
`path` text NOT NULL,
`search` text,
`time` integer NOT NULL,
`created_at` integer DEFAULT (current_timestamp)
`path` text NOT NULL,
`search` text,
`time` integer NOT NULL,
`created_at` integer DEFAULT (CURRENT_TIMESTAMP)
);
--> statement-breakpoint
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`created_at` integer DEFAULT (current_timestamp),
`expires_at` integer NOT NULL,
`user_id` text NOT NULL
`id` text PRIMARY KEY NOT NULL,
`created_at` integer DEFAULT (CURRENT_TIMESTAMP),
`expires_at` integer NOT NULL,
`user_id` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`created_at` integer DEFAULT (current_timestamp) NOT NULL,
`email` text NOT NULL,
`name` text NOT NULL
`id` text PRIMARY KEY NOT NULL,
`created_at` integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
`email` text NOT NULL,
`name` text NOT NULL
);

View File

@@ -1 +1,4 @@
ALTER TABLE `performance` ALTER COLUMN "created_at" TO "created_at" integer DEFAULT (STRFTIME('%s', 'now') * 1000);
ALTER TABLE
`performance`
ALTER COLUMN
"created_at" TO "created_at" integer DEFAULT (STRFTIME('%s', 'now') * 1000);

View File

@@ -1,7 +1,7 @@
CREATE TABLE `image` (
`created_at` integer DEFAULT (current_timestamp),
`url` text NOT NULL,
`average` text NOT NULL,
`blurhash` text NOT NULL,
`mime` text NOT NULL
`created_at` integer DEFAULT (CURRENT_TIMESTAMP),
`url` text NOT NULL,
`average` text NOT NULL,
`blurhash` text NOT NULL,
`mime` text NOT NULL
);

View File

@@ -1,7 +1,7 @@
CREATE TABLE `document` (
`name` text NOT NULL,
`last_modified` integer NOT NULL,
`contentType` text NOT NULL,
`size` integer NOT NULL,
`perm` text NOT NULL
`name` text NOT NULL,
`last_modified` integer NOT NULL,
`contentType` text NOT NULL,
`size` integer NOT NULL,
`perm` text NOT NULL
);

View File

@@ -1,13 +1,38 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_document` (
`name` text PRIMARY KEY NOT NULL,
`last_modified` integer NOT NULL,
`contentType` text NOT NULL,
`size` integer NOT NULL,
`perm` text NOT NULL
);
PRAGMA foreign_keys = OFF;
--> statement-breakpoint
INSERT INTO `__new_document`("name", "last_modified", "contentType", "size", "perm") SELECT "name", "last_modified", "contentType", "size", "perm" FROM `document`;--> statement-breakpoint
DROP TABLE `document`;--> statement-breakpoint
ALTER TABLE `__new_document` RENAME TO `document`;--> statement-breakpoint
PRAGMA foreign_keys=ON;
CREATE TABLE `__new_document` (
`name` text PRIMARY KEY NOT NULL,
`last_modified` integer NOT NULL,
`contentType` text NOT NULL,
`size` integer NOT NULL,
`perm` text NOT NULL
);
--> statement-breakpoint
INSERT INTO
`__new_document`(
"name",
"last_modified",
"contentType",
"size",
"perm"
)
SELECT
"name",
"last_modified",
"contentType",
"size",
"perm"
FROM
`document`;
--> statement-breakpoint
DROP TABLE `document`;
--> statement-breakpoint
ALTER TABLE
`__new_document` RENAME TO `document`;
--> statement-breakpoint
PRAGMA foreign_keys = ON;

View File

@@ -1 +1,2 @@
ALTER TABLE `document` RENAME COLUMN "contentType" TO "content_type";
ALTER TABLE
`document` RENAME COLUMN "contentType" TO "content_type";

View File

@@ -1 +1,4 @@
ALTER TABLE `document` ADD `content` text;
ALTER TABLE
`document`
ADD
`content` text;

View File

@@ -1 +1,4 @@
ALTER TABLE `document` ALTER COLUMN "content" TO "content" text NOT NULL;
ALTER TABLE
`document`
ALTER COLUMN
"content" TO "content" text NOT NULL;

View File

@@ -1 +1,4 @@
ALTER TABLE `document` ALTER COLUMN "content" TO "content" text;
ALTER TABLE
`document`
ALTER COLUMN
"content" TO "content" text;

View File

@@ -1,12 +1,17 @@
CREATE TABLE `cache` (
`scope` text NOT NULL,
`key` text PRIMARY KEY NOT NULL,
`json` text,
`binary` blob,
`created_at` integer DEFAULT (current_timestamp),
`expires_at` integer
`scope` text NOT NULL,
`key` text PRIMARY KEY NOT NULL,
`json` text,
`binary` blob,
`created_at` integer DEFAULT (CURRENT_TIMESTAMP),
`expires_at` integer
);
--> statement-breakpoint
CREATE INDEX `key_idx` ON `cache` (`key`);
--> statement-breakpoint
CREATE INDEX `scope_idx` ON `cache` (`scope`);
--> statement-breakpoint
CREATE INDEX `key_idx` ON `cache` (`key`);--> statement-breakpoint
CREATE INDEX `scope_idx` ON `cache` (`scope`);--> statement-breakpoint
CREATE INDEX `name_idx` ON `document` (`name`);

View File

@@ -1 +1,2 @@
ALTER TABLE `image` RENAME COLUMN "blurhash" TO "thumbhash";
ALTER TABLE
`image` RENAME COLUMN "blurhash" TO "thumbhash";

View File

@@ -0,0 +1,22 @@
DROP INDEX "key_idx";
--> statement-breakpoint
DROP INDEX "scope_idx";
--> statement-breakpoint
DROP INDEX "name_idx";
--> statement-breakpoint
ALTER TABLE
`image`
ALTER COLUMN
"created_at" TO "created_at" integer DEFAULT (unixepoch());
--> statement-breakpoint
CREATE INDEX `key_idx` ON `cache` (`key`);
--> statement-breakpoint
CREATE INDEX `scope_idx` ON `cache` (`scope`);
--> statement-breakpoint
CREATE INDEX `name_idx` ON `document` (`name`);

View File

@@ -0,0 +1,309 @@
{
"version": "6",
"dialect": "sqlite",
"id": "11bbbc9d-3c0c-4fb9-893f-c87d5b8660d0",
"prevId": "685b57ca-45e0-4373-baee-fc3abb4f2d74",
"tables": {
"cache": {
"name": "cache",
"columns": {
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"json": {
"name": "json",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"binary": {
"name": "binary",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(current_timestamp)"
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"key_idx": {
"name": "key_idx",
"columns": [
"key"
],
"isUnique": false
},
"scope_idx": {
"name": "scope_idx",
"columns": [
"scope"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"document": {
"name": "document",
"columns": {
"name": {
"name": "name",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"last_modified": {
"name": "last_modified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"content_type": {
"name": "content_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"perm": {
"name": "perm",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"name_idx": {
"name": "name_idx",
"columns": [
"name"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"image": {
"name": "image",
"columns": {
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(unixepoch())"
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"average": {
"name": "average",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"thumbhash": {
"name": "thumbhash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"mime": {
"name": "mime",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"performance": {
"name": "performance",
"columns": {
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"search": {
"name": "search",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time": {
"name": "time",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(STRFTIME('%s', 'now') * 1000)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(current_timestamp)"
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(current_timestamp)"
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -78,6 +78,13 @@
"when": 1762099260474,
"tag": "0010_youthful_tyrannus",
"breakpoints": true
},
{
"idx": 11,
"version": "6",
"when": 1768058169808,
"tag": "0011_reflective_frank_castle",
"breakpoints": true
}
]
}

View File

@@ -1,5 +0,0 @@
import { defineConfig } from "$fresh/server.ts";
import tailwind from "$fresh/plugins/tailwind.ts";
export default defineConfig({
plugins: [tailwind()],
});

View File

@@ -1,145 +0,0 @@
// DO NOT EDIT. This file is generated by Fresh.
// This file SHOULD be checked into source version control.
// This file is automatically updated during development when running `dev.ts`.
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $_layout from "./routes/_layout.tsx";
import * as $_middleware from "./routes/_middleware.ts";
import * as $admin_cache_index from "./routes/admin/cache/index.tsx";
import * as $admin_log_index from "./routes/admin/log/index.tsx";
import * as $admin_performance_index from "./routes/admin/performance/index.tsx";
import * as $api_articles_name_ from "./routes/api/articles/[name].ts";
import * as $api_articles_create_index from "./routes/api/articles/create/index.ts";
import * as $api_articles_enhance_name_ from "./routes/api/articles/enhance/[name].ts";
import * as $api_articles_index from "./routes/api/articles/index.ts";
import * as $api_auth_callback from "./routes/api/auth/callback.ts";
import * as $api_auth_login from "./routes/api/auth/login.ts";
import * as $api_auth_logout from "./routes/api/auth/logout.ts";
import * as $api_cache from "./routes/api/cache.ts";
import * as $api_images_index from "./routes/api/images/index.ts";
import * as $api_index from "./routes/api/index.ts";
import * as $api_logs from "./routes/api/logs.ts";
import * as $api_movies_name_ from "./routes/api/movies/[name].ts";
import * as $api_movies_enhance_name_ from "./routes/api/movies/enhance/[name].ts";
import * as $api_movies_index from "./routes/api/movies/index.ts";
import * as $api_query_index from "./routes/api/query/index.ts";
import * as $api_recipes_name_ from "./routes/api/recipes/[name].ts";
import * as $api_recipes_create_index from "./routes/api/recipes/create/index.ts";
import * as $api_recipes_create_parseJsonLd from "./routes/api/recipes/create/parseJsonLd.ts";
import * as $api_recipes_index from "./routes/api/recipes/index.ts";
import * as $api_recommendation_all from "./routes/api/recommendation/all.ts";
import * as $api_recommendation_data from "./routes/api/recommendation/data.ts";
import * as $api_recommendation_index from "./routes/api/recommendation/index.ts";
import * as $api_recommendation_movie_id_ from "./routes/api/recommendation/movie/[id].ts";
import * as $api_series_name_ from "./routes/api/series/[name].ts";
import * as $api_series_enhance_name_ from "./routes/api/series/enhance/[name].ts";
import * as $api_series_index from "./routes/api/series/index.ts";
import * as $api_tmdb_id_ from "./routes/api/tmdb/[id].ts";
import * as $api_tmdb_credits_id_ from "./routes/api/tmdb/credits/[id].ts";
import * as $api_tmdb_query from "./routes/api/tmdb/query.ts";
import * as $articles_name_ from "./routes/articles/[name].tsx";
import * as $articles_index from "./routes/articles/index.tsx";
import * as $index from "./routes/index.tsx";
import * as $movies_name_ from "./routes/movies/[name].tsx";
import * as $movies_index from "./routes/movies/index.tsx";
import * as $recipes_name_ from "./routes/recipes/[name].tsx";
import * as $recipes_index from "./routes/recipes/index.tsx";
import * as $series_name_ from "./routes/series/[name].tsx";
import * as $series_index from "./routes/series/index.tsx";
import * as $Counter from "./islands/Counter.tsx";
import * as $IngredientsList from "./islands/IngredientsList.tsx";
import * as $KMenu from "./islands/KMenu.tsx";
import * as $KMenu_commands from "./islands/KMenu/commands.ts";
import * as $KMenu_commands_add_movie_infos from "./islands/KMenu/commands/add_movie_infos.ts";
import * as $KMenu_commands_add_series_infos from "./islands/KMenu/commands/add_series_infos.ts";
import * as $KMenu_commands_create_article from "./islands/KMenu/commands/create_article.ts";
import * as $KMenu_commands_create_movie from "./islands/KMenu/commands/create_movie.ts";
import * as $KMenu_commands_create_recipe from "./islands/KMenu/commands/create_recipe.ts";
import * as $KMenu_commands_create_recommendations from "./islands/KMenu/commands/create_recommendations.ts";
import * as $KMenu_commands_create_series from "./islands/KMenu/commands/create_series.ts";
import * as $KMenu_commands_enhance_article_infos from "./islands/KMenu/commands/enhance_article_infos.ts";
import * as $KMenu_types from "./islands/KMenu/types.ts";
import * as $KMenuButton from "./islands/KMenuButton.tsx";
import * as $Link from "./islands/Link.tsx";
import * as $Recommendations from "./islands/Recommendations.tsx";
import * as $Search from "./islands/Search.tsx";
import type { Manifest } from "$fresh/server.ts";
const manifest = {
routes: {
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/_layout.tsx": $_layout,
"./routes/_middleware.ts": $_middleware,
"./routes/admin/cache/index.tsx": $admin_cache_index,
"./routes/admin/log/index.tsx": $admin_log_index,
"./routes/admin/performance/index.tsx": $admin_performance_index,
"./routes/api/articles/[name].ts": $api_articles_name_,
"./routes/api/articles/create/index.ts": $api_articles_create_index,
"./routes/api/articles/enhance/[name].ts": $api_articles_enhance_name_,
"./routes/api/articles/index.ts": $api_articles_index,
"./routes/api/auth/callback.ts": $api_auth_callback,
"./routes/api/auth/login.ts": $api_auth_login,
"./routes/api/auth/logout.ts": $api_auth_logout,
"./routes/api/cache.ts": $api_cache,
"./routes/api/images/index.ts": $api_images_index,
"./routes/api/index.ts": $api_index,
"./routes/api/logs.ts": $api_logs,
"./routes/api/movies/[name].ts": $api_movies_name_,
"./routes/api/movies/enhance/[name].ts": $api_movies_enhance_name_,
"./routes/api/movies/index.ts": $api_movies_index,
"./routes/api/query/index.ts": $api_query_index,
"./routes/api/recipes/[name].ts": $api_recipes_name_,
"./routes/api/recipes/create/index.ts": $api_recipes_create_index,
"./routes/api/recipes/create/parseJsonLd.ts":
$api_recipes_create_parseJsonLd,
"./routes/api/recipes/index.ts": $api_recipes_index,
"./routes/api/recommendation/all.ts": $api_recommendation_all,
"./routes/api/recommendation/data.ts": $api_recommendation_data,
"./routes/api/recommendation/index.ts": $api_recommendation_index,
"./routes/api/recommendation/movie/[id].ts": $api_recommendation_movie_id_,
"./routes/api/series/[name].ts": $api_series_name_,
"./routes/api/series/enhance/[name].ts": $api_series_enhance_name_,
"./routes/api/series/index.ts": $api_series_index,
"./routes/api/tmdb/[id].ts": $api_tmdb_id_,
"./routes/api/tmdb/credits/[id].ts": $api_tmdb_credits_id_,
"./routes/api/tmdb/query.ts": $api_tmdb_query,
"./routes/articles/[name].tsx": $articles_name_,
"./routes/articles/index.tsx": $articles_index,
"./routes/index.tsx": $index,
"./routes/movies/[name].tsx": $movies_name_,
"./routes/movies/index.tsx": $movies_index,
"./routes/recipes/[name].tsx": $recipes_name_,
"./routes/recipes/index.tsx": $recipes_index,
"./routes/series/[name].tsx": $series_name_,
"./routes/series/index.tsx": $series_index,
},
islands: {
"./islands/Counter.tsx": $Counter,
"./islands/IngredientsList.tsx": $IngredientsList,
"./islands/KMenu.tsx": $KMenu,
"./islands/KMenu/commands.ts": $KMenu_commands,
"./islands/KMenu/commands/add_movie_infos.ts":
$KMenu_commands_add_movie_infos,
"./islands/KMenu/commands/add_series_infos.ts":
$KMenu_commands_add_series_infos,
"./islands/KMenu/commands/create_article.ts":
$KMenu_commands_create_article,
"./islands/KMenu/commands/create_movie.ts": $KMenu_commands_create_movie,
"./islands/KMenu/commands/create_recipe.ts": $KMenu_commands_create_recipe,
"./islands/KMenu/commands/create_recommendations.ts":
$KMenu_commands_create_recommendations,
"./islands/KMenu/commands/create_series.ts": $KMenu_commands_create_series,
"./islands/KMenu/commands/enhance_article_infos.ts":
$KMenu_commands_enhance_article_infos,
"./islands/KMenu/types.ts": $KMenu_types,
"./islands/KMenuButton.tsx": $KMenuButton,
"./islands/Link.tsx": $Link,
"./islands/Recommendations.tsx": $Recommendations,
"./islands/Search.tsx": $Search,
},
baseUrl: import.meta.url,
} satisfies Manifest;
export default manifest;

View File

@@ -1,6 +1,6 @@
import type { Signal } from "@preact/signals";
import { Button } from "@components/Button.tsx";
import { IconCircleMinus, IconCirclePlus } from "@components/icons.tsx";
import { TbCircleMinus, TbCirclePlus } from "@preact-icons/tb";
interface CounterProps {
count: Signal<number>;
@@ -10,21 +10,21 @@ export default function Counter(props: CounterProps) {
props.count.value = Math.max(1, props.count.value);
return (
<div class="flex items-center px-1 py-2 rounded-xl">
<Button
class=""
onClick={() => props.count.value -= 1}
>
<IconCircleMinus />
<Button onClick={() => props.count.value -= 1}>
<TbCircleMinus class="h-6 w-6" />
</Button>
<input
class="text-3xl bg-transparent inline text-center -mx-4"
type="number"
size={props.count.toString().length}
value={props.count}
onInput={(ev) => props.count.value = ev.target?.value}
onInput={(ev) => {
const target = ev.target as HTMLInputElement;
props.count.value = Math.max(1, Number(target.value));
}}
/>
<Button onClick={() => props.count.value += 1}>
<IconCirclePlus />
<TbCirclePlus class="h-6 w-6" />
</Button>
</div>
);

View File

@@ -1,6 +1,5 @@
import { Signal } from "@preact/signals";
import type { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
import { FunctionalComponent } from "preact";
import { unitsOfMeasure } from "@lib/parseIngredient.ts";
function formatAmount(num: number) {
@@ -50,14 +49,12 @@ const Ingredient = (
);
};
export const IngredientsList: FunctionalComponent<
{
export const IngredientsList = (
{ ingredients, amount, portion }: {
ingredients: (Ingredient | IngredientGroup)[];
amount: Signal<number>;
portion?: number;
}
> = (
{ ingredients, amount, portion },
},
) => {
return (
<table class="w-full border-collapse table-auto">

View File

@@ -4,7 +4,6 @@ import { useEventListener } from "@lib/hooks/useEventListener.ts";
import { menus } from "@islands/KMenu/commands.ts";
import { MenuEntry } from "@islands/KMenu/types.ts";
import * as icons from "@components/icons.tsx";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { isKMenuOpen } from "@lib/kmenu.ts";
const KMenuEntry = (
{ entry, activeIndex, index }: {
@@ -155,17 +154,17 @@ export const KMenu = (
} else {
input.current?.blur();
}
}, IS_BROWSER ? document?.body : undefined);
}, typeof document !== "undefined" ? document?.body : undefined);
return (
<div
class={`${visible.value ? "opacity-100" : "opacity-0"} pointer-events-${
visible.value ? "auto" : "none"
class={`${visible.value ? "opacity-100" : "opacity-0"} ${
visible.value ? "pointer-events-auto" : "pointer-events-none"
} transition grid place-items-center w-full h-full fixed top-0 left-0 z-50`}
style={{ background: "#141217ee" }}
>
<div
class={`relative w-1/2 max-h-64 max-w-[400px] rounded-2xl shadow-2xl nnoisy-gradient overflow-hidden after:opacity-10 bg-gray-700`}
class={`relative w-1/2 max-h-64 max-w-100 rounded-2xl shadow-2xl nnoisy-gradient overflow-hidden after:opacity-10 bg-gray-700`}
style={{ background: "#2B2930", color: "#818181" }}
>
<div

View File

@@ -5,7 +5,6 @@ import { createNewArticle } from "@islands/KMenu/commands/create_article.ts";
import { getCookie } from "@lib/string.ts";
import { addSeriesInfo } from "@islands/KMenu/commands/add_series_infos.ts";
import { createNewSeries } from "@islands/KMenu/commands/create_series.ts";
import { updateAllRecommendations } from "@islands/KMenu/commands/create_recommendations.ts";
import { createNewRecipe } from "@islands/KMenu/commands/create_recipe.ts";
import { enhanceArticleInfo } from "@islands/KMenu/commands/enhance_article_infos.ts";

View File

@@ -42,7 +42,11 @@ export const addMovieInfos: MenuEntry = {
globalThis.location.reload();
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
})),
@@ -53,7 +57,11 @@ export const addMovieInfos: MenuEntry = {
state.activeState.value = "normal";
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
visible: () => {

View File

@@ -42,7 +42,11 @@ export const addSeriesInfo: MenuEntry = {
//window.location.reload();
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
})),
@@ -53,7 +57,11 @@ export const addSeriesInfo: MenuEntry = {
state.activeState.value = "normal";
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
visible: () => {

View File

@@ -66,7 +66,11 @@ export const createNewMovie: MenuEntry = {
globalThis.location.href = "/movies/" + movie.name;
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
};
@@ -75,7 +79,11 @@ export const createNewMovie: MenuEntry = {
state.activeMenu.value = "input_link";
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
}, 500);

View File

@@ -68,7 +68,11 @@ export const createNewSeries: MenuEntry = {
globalThis.location.href = "/series/" + series.name;
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
},
};
@@ -78,7 +82,11 @@ export const createNewSeries: MenuEntry = {
state.activeMenu.value = "input_link";
} catch (e) {
state.activeState.value = "error";
state.loadingText.value = e.message;
if (e instanceof Error) {
if ("message" in e) {
state.loadingText.value = e.message;
}
}
}
}, 500);

View File

@@ -8,7 +8,7 @@ export function Link(
props: {
href?: string;
class?: string;
style?: preact.JSX.CSSProperties;
style?: preact.CSSProperties;
children: preact.ComponentChildren;
"data-thumb"?: string;
},

View File

@@ -4,7 +4,7 @@ import { IconLoader2, IconSearch } from "@components/icons.tsx";
import { useEventListener } from "@lib/hooks/useEventListener.ts";
import { resources } from "@lib/resources.ts";
import { getCookie } from "@lib/string.ts";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { IS_BROWSER } from "fresh/runtime";
import Checkbox from "@components/Checkbox.tsx";
import { Rating } from "@components/Rating.tsx";
import { useSignal } from "@preact/signals";

View File

@@ -1,4 +1,4 @@
import { OAuth2Client } from "https://deno.land/x/oauth2_client@v1.0.2/mod.ts";
import { OAuth2Client } from "@cmd-johnson/oauth2-client";
import {
GITEA_CLIENT_ID,
GITEA_CLIENT_SECRET,

View File

@@ -12,7 +12,9 @@ export const userTable = sqliteTable("user", {
id: text()
.primaryKey(),
createdAt: integer("created_at", { mode: "timestamp" })
.default(sql`(current_timestamp)`)
.default(sql`
(CURRENT_TIMESTAMP)
`)
.notNull(),
email: text()
.notNull(),
@@ -24,7 +26,9 @@ export const sessionTable = sqliteTable("session", {
id: text("id")
.primaryKey(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(
sql`(current_timestamp)`,
sql`
(CURRENT_TIMESTAMP)
`,
),
expiresAt: integer("expires_at", { mode: "timestamp" })
.notNull(),
@@ -38,12 +42,16 @@ export const performanceTable = sqliteTable("performance", {
time: int().notNull(),
createdAt: integer("created_at", {
mode: "timestamp_ms",
}).default(sql`(STRFTIME('%s', 'now') * 1000)`),
}).default(sql`
(STRFTIME('%s', 'now') * 1000)
`),
});
export const imageTable = sqliteTable("image", {
createdAt: integer("created_at", { mode: "timestamp" }).default(
sql`(current_timestamp)`,
sql`
(unixepoch())
`,
),
url: text().notNull(),
average: text().notNull(),
@@ -70,7 +78,9 @@ export const cacheTable = sqliteTable("cache", {
json: text({ mode: "json" }),
binary: blob(),
createdAt: integer("created_at", { mode: "timestamp" }).default(
sql`(current_timestamp)`,
sql`
(CURRENT_TIMESTAMP)
`,
),
expiresAt: integer("expires_at", { mode: "timestamp" }),
}, (table) => {

View File

@@ -4,7 +4,6 @@ import path from "node:path";
const DB_FILE = "file:" + path.resolve(DATA_DIR, "db.sqlite");
// You can specify any property from the libsql connection options
export const db = drizzle({
connection: {
url: DB_FILE,

View File

@@ -1,8 +1,9 @@
import { FreshContext } from "$fresh/server.ts";
import { Context } from "fresh";
import { State } from "../utils.ts";
class DomainError extends Error {
status = 500;
render?: (ctx: FreshContext) => void;
render?: (ctx: Context<State>) => void;
constructor(public statusText = "Internal Server Error") {
super();
}

View File

@@ -1,14 +1,13 @@
import { rgbToHex } from "@lib/string.ts";
import { createLogger } from "@lib/log/index.ts";
import { generateThumbhash } from "@lib/thumbhash.ts";
import { parseMediaType } from "https://deno.land/std@0.224.0/media_types/parse_media_type.ts";
import { parseMediaType } from "@std/media-types";
import path from "node:path";
import { mkdir } from "node:fs/promises";
import { DATA_DIR } from "@lib/env.ts";
import { db } from "@lib/db/sqlite.ts";
import { imageTable } from "@lib/db/schema.ts";
import { eq } from "drizzle-orm";
import sharp from "npm:sharp@next";
const log = createLogger("cache/image");
@@ -158,6 +157,8 @@ async function resizeImage(
mediaType: string;
},
) {
const sharp = (await import("sharp")).default;
try {
log.debug("Resizing image", { params });
@@ -211,6 +212,8 @@ async function resizeImage(
async function createThumbhash(
image: Uint8Array,
): Promise<{ hash: string; average: string }> {
const sharp = (await import("sharp")).default;
try {
const resizedImage = await sharp(image)
.resize(100, 100, { fit: "cover" }) // Keep aspect ratio within bounds
@@ -219,7 +222,11 @@ async function createThumbhash(
.raw()
.toBuffer();
const [hash, average] = generateThumbhash(resizedImage, 100, 100);
const [hash, average] = generateThumbhash(
new Uint8Array(resizedImage),
100,
100,
);
return {
hash: btoa(String.fromCharCode(...hash)),
@@ -234,6 +241,8 @@ async function createThumbhash(
* Verifies that an image buffer contains valid image data
*/
async function verifyImage(imageBuffer: Uint8Array): Promise<boolean> {
const sharp = (await import("sharp")).default;
try {
const metadata = await sharp(imageBuffer).metadata();
return !!(metadata.width && metadata.height && metadata.format);

View File

@@ -36,9 +36,10 @@ export async function getLogs() {
.map((line) => {
const [date, ...rest] = line.split(" | ");
const parsed = JSON.parse(rest.join(" | ")) as Log;
const dateObj = new Date(date);
return {
...parsed,
date: new Date(date),
date: isNaN(dateObj.getTime()) ? new Date() : dateObj,
} as Log;
});

View File

@@ -1,7 +1,7 @@
import { render } from "gfm";
import "https://esm.sh/prismjs@1.29.0/components/prism-typescript?no-check";
import "https://esm.sh/prismjs@1.29.0/components/prism-bash?no-check";
import "https://esm.sh/prismjs@1.29.0/components/prism-rust?no-check";
import "prismjs/components/prism-typescript.js";
import "prismjs/components/prism-bash.js";
import "prismjs/components/prism-rust.js";
export type Document = {
name: string;

View File

@@ -8,7 +8,7 @@ import { articleMetadataSchema } from "./marka/schema.ts";
const openAI = OPENAI_API_KEY && new OpenAI({ apiKey: OPENAI_API_KEY });
interface MovieRecommendation {
export interface MovieRecommendation {
year: number;
title: string;
}
@@ -181,14 +181,14 @@ respond with a plain unordered list each item starting with the year the movie w
if (!res) return;
const recommendations = res.split("\n").map((entry) => {
const recommendations = res.split("\n").map((entry: string) => {
const [year, ...title] = entry.split("-");
return {
year: parseInt(year.trim()),
title: title.join(" ").replaceAll('"', "").trim(),
};
}).filter((y) => !Number.isNaN(y.year));
}).filter((y: { year: number }) => !Number.isNaN(y.year));
cache.set(cacheId, recommendations);

View File

@@ -1,6 +1,6 @@
import { firefox } from "npm:playwright-extra";
import { firefox } from "playwright-extra";
import { createStreamResponse } from "@lib/helpers.ts";
import StealthPlugin from "npm:puppeteer-extra-plugin-stealth";
import StealthPlugin from "puppeteer-extra-plugin-stealth";
import * as env from "@lib/env.ts";
firefox.use(StealthPlugin());

View File

@@ -1,105 +0,0 @@
/**
* Interface zur Beschreibung eines eingereihten Promises in der `PromiseQueue`.
*/
interface QueuedPromise<T = any> {
promise: () => Promise<T>;
resolve: (value: T) => void;
reject: (reason?: any) => void;
}
/**
* Eine einfache Promise Queue, die es ermöglicht mehrere Aufgaben in kontrollierter
* Reihenfolge abzuarbeiten.
*
* Lizenz: CC BY-NC-SA 4.0
* (c) Peter Müller <peter@crycode.de> (https://crycode.de/promise-queue-in-typescript)
*/
export class PromiseQueue {
/**
* Eingereihte Promises.
*/
private queue: QueuedPromise[] = [];
/**
* Indikator, dass aktuell ein Promise abgearbeitet wird.
*/
private working = false;
/**
* Ein Promise einreihen.
* Dies fügt das Promise der Warteschlange hinzu. Wenn die Warteschlange leer
* ist, dann wird das Promise sofort gestartet.
* @param promise Funktion, die das Promise zurückgibt.
* @returns Ein Promise, welches eingelöst (oder zurückgewiesen) wird sobald das eingereihte Promise abgearbeitet ist.
*/
public enqueue<T = void>(promise: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({
promise,
resolve,
reject,
});
this.dequeue();
});
}
/**
* Das erste Promise aus der Warteschlange holen und starten, sofern nicht
* bereits ein Promise aktiv ist.
* @returns `true` wenn ein Promise aus der Warteschlange gestartet wurde oder `false` wenn bereits ein Promise aktiv oder die Warteschlange leer ist.
*/
private dequeue(): boolean {
if (this.working) {
return false;
}
const item = this.queue.shift();
if (!item) {
return false;
}
try {
this.working = true;
item.promise()
.then((value) => {
item.resolve(value);
})
.catch((err) => {
item.reject(err);
})
.finally(() => {
this.working = false;
this.dequeue();
});
} catch (err) {
item.reject(err);
this.working = false;
this.dequeue();
}
return true;
}
}
export class ConcurrentPromiseQueue {
/**
* Eingereihte Promises.
*/
private queues: PromiseQueue[] = [];
constructor(concurrency: number = 1) {
this.queues = Array.from({ length: concurrency }).map(() => {
return new PromiseQueue();
});
}
private queueIndex = 0;
private getQueue() {
this.queueIndex = (this.queueIndex + 1) % this.queues.length;
return this.queues[this.queueIndex];
}
public enqueue<T = void>(promise: () => Promise<T>): Promise<T> {
return this.getQueue().enqueue(promise);
}
}

View File

@@ -1,4 +1,4 @@
import { z } from "npm:zod";
import { z } from "zod";
import { RecipeResource } from "./marka/schema.ts";
export const IngredientSchema = z.object({

View File

@@ -1,4 +1,5 @@
import * as openai from "@lib/openai.ts";
import { type MovieRecommendation } from "@lib/openai.ts";
import * as tmdb from "@lib/tmdb.ts";
import { parseRating } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts";
@@ -60,7 +61,9 @@ export async function createRecommendationResource(
const d = typeof datePublished === "string"
? new Date(datePublished)
: datePublished;
resource.year = d.getFullYear();
if (!isNaN(d.getTime())) {
resource.year = d.getFullYear();
}
}
cache.set(cacheId, JSON.stringify(resource));
@@ -83,10 +86,12 @@ export async function getSimilarMovies(id: string) {
);
if (!recommendations) return;
const movies = await Promise.all(recommendations.map(async (rec) => {
const m = await tmdb.searchMovie(rec.title, rec.year);
return m?.results?.[0];
}));
const movies = await Promise.all(
recommendations.map(async (rec: MovieRecommendation) => {
const m = await tmdb.searchMovie(rec.title, rec.year);
return m?.results?.[0];
}),
);
return movies.filter(Boolean);
}
@@ -96,5 +101,7 @@ export async function getAllRecommendations(): Promise<
> {
const keys = cache.keys();
const res = await Promise.all(keys.map((k) => cache.get(k)));
return res.filter((s) => !!s).map((r) => JSON.parse(r));
return res.filter((s) => !!s).map((r) =>
typeof r === "string" ? JSON.parse(r) : r
);
}

View File

@@ -186,3 +186,7 @@ export function removeMarkdownFormatting(text: string): string {
return text;
}
export function fileExtension(fname: string) {
return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2);
}

View File

@@ -1,110 +0,0 @@
import { transcribe } from "@lib/openai.ts";
import { createResource } from "@lib/marka/index.ts";
import { createLogger } from "./log/index.ts";
import { convertOggToMp3 } from "./helpers.ts";
const log = createLogger("taskManager");
// In-memory task state
const activeTasks: Record<
string,
{
noteName: string;
entries: Array<
{ type: string; content: string | ArrayBufferLike; fileName?: string }
>;
}
> = {};
export function startTask(chatId: string, noteName: string) {
activeTasks[chatId] = { noteName, entries: [] };
log.info(`Started note: ${noteName}`);
}
export async function endTask(chatId: string): Promise<string | null> {
const task = activeTasks[chatId];
if (!task) return null;
log.info("Ending note", task.noteName);
let finalNote = `# ${task.noteName}\n\n`;
const photoTasks: { content: ArrayBuffer; path: string }[] = [];
let photoIndex = 0;
for (const entry of task.entries) {
if (entry.type === "text") {
finalNote += entry.content + "\n\n";
} else if (entry.type === "voice") {
try {
log.info("Converting OGG to MP3");
const mp3Data = await convertOggToMp3(entry.content as ArrayBuffer);
log.info("Finished converting OGG to MP3, transcribing...");
const transcript = await transcribe(mp3Data);
finalNote += `**Voice Transcript:**\n${transcript}\n\n`;
log.info("Finished transcribing");
} catch (error) {
log.error(error);
finalNote += "**[Voice message could not be transcribed]**\n\n";
}
} else if (entry.type === "photo") {
const photoUrl = `${
task.noteName.replace(/\.md$/, "")
}/photo-${photoIndex++}.jpg`;
finalNote += `**Photo**:\n ${photoUrl}\n\n`;
photoTasks.push({
content: entry.content as ArrayBuffer,
path: photoUrl,
});
}
}
try {
for (const entry of photoTasks) {
await createResource(entry.path, entry.content);
}
} catch (err) {
log.error("Error creating photo document:", err);
}
try {
await createResource(task.noteName, finalNote);
} catch (error) {
log.error("Error creating document:", error);
return error instanceof Error
? error.toString()
: "Error creating document";
}
delete activeTasks[chatId];
log.debug({ finalNote });
return finalNote;
}
export function addTextEntry(chatId: string, text: string) {
const task = activeTasks[chatId];
if (!task) return;
const entry = { type: "text", content: text };
log.debug("New Entry", entry);
task.entries.push(entry);
}
export function addVoiceEntry(chatId: string, buffer: ArrayBufferLike) {
const task = activeTasks[chatId];
if (!task) return;
const entry = { type: "voice", content: buffer };
log.debug("New Entry", entry);
task.entries.push(entry);
}
export function addPhotoEntry(
chatId: string,
buffer: ArrayBufferLike,
fileName: string,
) {
const task = activeTasks[chatId];
if (!task) return;
const entry = { type: "photo", content: buffer, fileName };
log.debug("New Entry", entry);
task.entries.push(entry);
}

View File

@@ -1,65 +0,0 @@
import { Bot } from "https://deno.land/x/grammy@v1.36.1/mod.ts";
import { TELEGRAM_API_KEY } from "@lib/env.ts";
import { createLogger } from "./log/index.ts";
const bot = new Bot(TELEGRAM_API_KEY);
const log = createLogger("telegram");
import * as manager from "./taskManager.ts";
async function downloadFile(filePath: string): Promise<Uint8Array> {
log.info(`Downloading file from path: ${filePath}`);
const url = `https://api.telegram.org/file/bot${bot.token}/${filePath}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("Failed to download file: " + response.statusText);
}
log.info("File downloaded successfully");
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
}
bot.command("start", async (ctx) => {
log.info("Received /start command");
const [_, noteName] = ctx.message?.text?.split(" ") || [];
if (!noteName) {
return ctx.reply("Please provide a note name. Usage: /start NoteName");
}
manager.startTask(ctx.chat.id.toString(), noteName);
await ctx.reply(`Started note: ${noteName}`);
});
bot.command("end", async (ctx) => {
log.info("Received /end command");
const finalNote = await manager.endTask(ctx.chat.id.toString());
if (!finalNote) return ctx.reply("No active note found.");
try {
await ctx.reply("Note complete. Here is your markdown:");
await ctx.reply(finalNote, { parse_mode: "MarkdownV2" });
} catch (err) {
console.error("Error sending final note:", err);
}
});
bot.on("message:text", (ctx) => {
log.info("Received text message");
manager.addTextEntry(ctx.chat.id.toString(), ctx.message.text);
});
bot.on("message:voice", async (ctx) => {
log.info("Received photo message");
log.info("Received voice message");
const file = await ctx.getFile();
const buffer = await downloadFile(file.file_path!);
manager.addVoiceEntry(ctx.chat.id.toString(), buffer.buffer);
});
bot.on("message:photo", async (ctx) => {
const file = await ctx.getFile();
const buffer = await downloadFile(file.file_path!);
const fileName = file.file_path!.split("/").pop()!;
manager.addPhotoEntry(ctx.chat.id.toString(), buffer.buffer, fileName);
});
bot.start();

View File

@@ -1,6 +1,10 @@
import * as thumbhash from "thumbhash";
export function generateThumbhash(buffer: Uint8Array, w: number, h: number) {
export function generateThumbhash(
buffer: ArrayLike<number>,
w: number,
h: number,
) {
const hash = thumbhash.rgbaToThumbHash(w, h, buffer);
return [hash, thumbhash.thumbHashToAverageRGBA(hash)] as const;
}

View File

@@ -47,7 +47,7 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void {
document
.querySelectorAll("img[srcset], source[srcset]")
.forEach((el: HTMLImageElement) => {
.forEach((el) => {
const v = el.getAttribute("srcset");
if (!v) return;
const abs = absolutizeSrcset(v, base);
@@ -55,7 +55,7 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void {
});
document.querySelectorAll("[style]").forEach(
(el: HTMLElement) => {
(el) => {
const v = el.getAttribute("style");
if (!v) return;
const abs = absolutizeCssUrls(v, base);
@@ -73,7 +73,7 @@ export function absolutizeDomUrls(dom: JSDOM, domain: string): void {
document
.querySelectorAll('meta[http-equiv="refresh" i][content]')
.forEach((meta: HTMLMetaElement) => {
.forEach((meta) => {
const content = meta.getAttribute("content") || "";
const abs = absolutizeMetaRefresh(content, base);
if (abs !== content) meta.setAttribute("content", abs);
@@ -168,10 +168,20 @@ function absolutizeMetaRefresh(content: string, base: string): string {
const turndownService = new TurndownService();
export interface WebScrapeResult {
title?: string;
image?: string;
published?: string;
content: string;
schemaOrgData?: { author?: { name?: string } };
markdown: string;
dom: JSDOM["window"]["document"];
}
export async function webScrape(
url: string,
streamResponse: ReturnType<typeof createStreamResponse>,
): JSDOM {
) {
const u = new URL(url);
const html = await fetchHtmlWithPlaywright(url, streamResponse);
const dom = new JSDOM(html);

View File

@@ -56,12 +56,10 @@ export interface ContentDetails {
definition: string;
caption: string;
licensedContent: boolean;
contentRating: ContentRating;
contentRating: Record<string, unknown>;
projection: string;
}
export interface ContentRating {}
export interface Statistics {
viewCount: string;
likeCount: string;

16
main.ts
View File

@@ -1,12 +1,8 @@
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import { App, staticFiles } from "fresh";
import { type State } from "./utils.ts";
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import config from "./fresh.config.ts";
// import "@lib/telegram.ts";
export const app = new App<State>();
await start(manifest, config);
app.use(staticFiles());
app.fsRoutes();

View File

@@ -1,24 +1,16 @@
// deno-lint-ignore-file react-no-danger
import { PageProps } from "$fresh/server.ts";
import { Partial } from "$fresh/runtime.ts";
export default function App({ Component }: PageProps) {
const globalCss = Deno.readTextFileSync("./static/global.css");
import { define } from "../utils.ts";
export default define.page(function ({ Component }) {
return (
<html>
<head>
<link rel="stylesheet" href="/prism-material-dark.css" />
<link rel="stylesheet" href="/styles.css" />
<link
rel="icon"
type="image/png"
href="/favicon.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#141218" />
<link
rel="preload"
href="/fonts/work-sans-v18-latin-regular.woff2"
@@ -31,15 +23,25 @@ export default function App({ Component }: PageProps) {
as="font"
type="font/woff2"
/>
<style dangerouslySetInnerHTML={{ __html: globalCss }} />
<link rel="stylesheet" href="/global.css" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#141218" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style
dangerouslySetInnerHTML={{
__html: Deno.readTextFileSync("./static/global.css"),
}}
>
</style>
<title>Memorium</title>
</head>
<body f-client-nav>
<Partial name="body">
<Component />
</Partial>
<body>
<Component />
</body>
<script src="/thumbhash.js" type="module" async defer />
</html>
);
}
});

View File

@@ -1,7 +1,18 @@
import { Head } from "$fresh/runtime.ts";
import { Head } from "fresh/runtime";
import { MainLayout } from "@components/layouts/main.tsx";
import { HttpError, PageProps } from "fresh";
export default function ErrorPage(props: PageProps) {
const error = props.error; // Contains the thrown Error or HTTPError
if (error instanceof HttpError) {
const status = error.status; // HTTP status code
// Render a 404 not found page
if (status === 404) {
return <h1>404 - Page not found</h1>;
}
}
export default function Error404() {
return (
<>
<Head>
@@ -9,7 +20,7 @@ export default function Error404() {
</Head>
<MainLayout url="">
<div class="px-8 text-white mt-10">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<div class="max-w-3xl mx-auto flex flex-col items-center justify-center">
<h1 class="text-4xl font-bold">404 - Page not found</h1>
<p class="my-4">
The page you were looking for doesn't exist.

View File

@@ -1,10 +1,10 @@
import { PageProps } from "$fresh/server.ts";
import { resources } from "@lib/resources.ts";
import { Link } from "@islands/Link.tsx";
import { Emoji } from "@components/Emoji.tsx";
import KMenuButton from "@islands/KMenuButton.tsx";
import { PageProps } from "fresh";
import { define } from "../utils.ts";
export default function MyLayout({ Component }: PageProps) {
export default define.layout(function ({ Component }: PageProps) {
return (
<div
class="md:grid mx-auto"
@@ -14,12 +14,12 @@ export default function MyLayout({ Component }: PageProps) {
<nav class="min-h-fit rounded-3xl p-3 grid gap-3 fixed t-0">
{Object.values(resources).map((m) => {
return (
<Link
<a
href={m.link}
class="flex items-center gap-2 text-white data-[current]:bg-white data-[current]:text-black p-3 text-xl w-full rounded-2xl"
class="flex items-center gap-2 text-white data-current:bg-white data-current:text-black p-3 text-xl w-full rounded-2xl"
>
<Emoji class="w-6 h-6" name={m.emoji} /> {m.name}
</Link>
</a>
);
})}
</nav>
@@ -30,4 +30,4 @@ export default function MyLayout({ Component }: PageProps) {
<KMenuButton />
</div>
);
}
});

View File

@@ -1,27 +1,39 @@
//routes/middleware-error-handler/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
import { DomainError } from "@lib/errors.ts";
import { getCookies } from "@std/http/cookie";
import { verify } from "https://deno.land/x/djwt@v2.2/mod.ts";
import { verify } from "@zaubrik/djwt";
import * as perf from "@lib/performance.ts";
import { JWT_SECRET } from "@lib/env.ts";
import { define } from "../utils.ts";
export async function handler(
req: Request,
ctx: FreshContext,
function importKey(secret: string) {
return crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-512" },
false,
["sign", "verify"],
);
}
const authMiddleware = define.middleware(async function (
ctx,
) {
const req = ctx.req;
try {
performance.mark("a");
const allCookies = getCookies(req.headers);
const sessionCookie = allCookies["session_cookie"];
if (!ctx.state.session && sessionCookie && JWT_SECRET) {
try {
const payload = await verify(sessionCookie, JWT_SECRET, "HS512");
const payload = await verify<typeof ctx.state.session>(
sessionCookie,
await importKey(JWT_SECRET),
);
if (payload) {
ctx.state.session = payload;
}
} catch (_err) {
//
console.log({ _err });
}
}
@@ -44,4 +56,6 @@ export async function handler(
status: 500,
});
}
}
});
export default [authMiddleware];

View File

@@ -1,32 +0,0 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Handlers, PageProps } from "$fresh/server.ts";
import { getCacheInfo } from "@lib/cache.ts";
export const handler: Handlers<
{ cacheInfo: ReturnType<typeof getCacheInfo> }
> = {
GET(_, ctx) {
return ctx.render({ cacheInfo: getCacheInfo() });
},
};
export default function Greet(
props: PageProps<
{ cacheInfo: ReturnType<typeof getCacheInfo> }
>,
) {
const { cacheInfo } = props.data;
return (
<MainLayout
url={props.url}
title="Recipes"
context={{ type: "recipes" }}
>
<code>
<pre class="text-white">
{JSON.stringify(cacheInfo, null, 2)}
</pre>
</code>
</MainLayout>
);
}

View File

@@ -1,78 +0,0 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Handlers, PageProps } from "$fresh/server.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { getLogs, Log } from "@lib/log/index.ts";
import { formatDate } from "@lib/string.ts";
import { renderMarkdown } from "@lib/markdown.ts";
const renderLog = (t: unknown) =>
renderMarkdown(`\`\`\`js
${typeof t === "string" ? t : JSON.stringify(t, null, 2).trim()}
\`\`\``);
export const handler: Handlers = {
async GET(_, ctx) {
const logs = await getLogs();
if (!("session" in ctx.state)) {
throw new AccessDeniedError();
}
return ctx.render({
logs: logs.map((l) => {
return {
...l,
html: l.args.map(renderLog).join("<br/>"),
};
}),
});
},
};
function LogLine(
{ log }: {
log: Log & { html?: string };
},
) {
return (
<div
class="mt-4 flex flex-col gap-2 bg-gray-900 px-4 py-2 rounded-2xl max-w-3xl"
style={{ background: "var(--light)" }}
>
<div class="flex gap-2">
<span class="bg-gray-600 py-1 px-2 text-xs rounded-xl text-white">
{log.date.getHours().toString().padStart(2, "0")}:{log.date
.getMinutes().toString().padStart(2, "0")}:{log.date.getSeconds()
.toString().padStart(2, "0")} {formatDate(log.date)}
</span>
<span class="bg-gray-600 py-1 px-2 text-xs rounded-xl text-white">
{log.scope}
</span>
<span class="bg-gray-600 py-1 px-2 text-xs rounded-xl text-white">
{log.level}
</span>
</div>
<div
class="text-white"
// deno-lint-ignore react-no-danger
dangerouslySetInnerHTML={{ __html: log.html ?? "" }}
/>
</div>
);
}
export default function Greet(
{ data: { logs }, url }: PageProps<{ logs: Log[] }>,
) {
return (
<MainLayout url={url}>
<h1 class="text-white text-4xl">Logs</h1>
{logs.map((r) => {
return (
<LogLine
log={r}
/>
);
})}
</MainLayout>
);
}

View File

@@ -1,89 +0,0 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Handlers, PageProps } from "$fresh/server.ts";
import { getPerformances, PerformanceRes } from "@lib/performance.ts";
import { AccessDeniedError } from "@lib/errors.ts";
export const handler: Handlers = {
async GET(_, ctx) {
const performances = await getPerformances();
if (!("session" in ctx.state)) {
throw new AccessDeniedError();
}
return ctx.render({ performances });
},
};
function PerformanceLine(
{ maximum, data: [amount, min, average, max], url }: {
maximum: number;
url: string;
data: readonly [number, number, number, number];
},
) {
return (
<div
class="mt-10 flex flex-col gap-2 bg-gray-900 px-4 py-2 rounded-2xl"
style={{ background: "var(--light)" }}
>
<div style={{ color: "var(--foreground)" }}>
{url}
</div>
<div class="flex gap-2">
<span class="bg-gray-600 p-1 text-xs rounded-xl text-white">
{Math.floor(average / 1000)}ms
</span>
<span class="bg-gray-600 p-1 text-xs rounded-xl text-white">
{amount}x
</span>
</div>
<div class="" style={{ maxWidth: "75vw" }}>
<div
class="h-2 bg-red-200 rounded"
style={{
width: `${Math.floor((max / maximum) * 100)}%`,
borderRadius: "1rem 1rem 1rem 0px",
minWidth: "5px",
}}
/>
<div
class="h-2 bg-green-200 rounded"
style={{
width: `${Math.floor((average / maximum) * 100)}%`,
borderStartEndRadius: "0px",
borderRadius: "0px 0px 1rem 0px",
minWidth: "5px",
}}
/>
<div
class="h-2 bg-yellow-200 rounded"
style={{
width: `${Math.floor((min / maximum) * 100)}%`,
borderRadius: "0px 0px 1rem 1rem",
minWidth: "5px",
}}
/>
</div>
</div>
);
}
export default function Greet(
{ data: { performances }, url }: PageProps<{ performances: PerformanceRes }>,
) {
return (
<MainLayout url={url}>
<h1 class="text-white text-4xl ">Performance</h1>
{performances.res.map((r) => {
return (
<PerformanceLine
maximum={performances.max}
url={r.url}
data={r.data}
/>
);
})}
</MainLayout>
);
}

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const article = await fetchResource(`articles/${ctx.params.name}`);
return json(article);
},
};
});

View File

@@ -1,5 +1,3 @@
import { Handlers } from "$fresh/server.ts";
import { Defuddle } from "defuddle/node";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts";
@@ -7,6 +5,7 @@ import * as unsplash from "@lib/unsplash.ts";
import { getYoutubeVideoDetails } from "@lib/youtube.ts";
import {
extractYoutubeId,
fileExtension,
formatDate,
isYoutubeLink,
safeFileName,
@@ -16,7 +15,7 @@ import { createLogger } from "@lib/log/index.ts";
import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { define } from "../../../../utils.ts";
const log = createLogger("api/article");
@@ -85,29 +84,31 @@ async function processCreateArticle(
streamResponse.info("downloading article");
const result = await webScrape(fetchUrl, streamResponse);
const scrapeResult = await webScrape(fetchUrl, streamResponse);
log.debug("downloaded and parse parsed", result);
log.debug("downloaded and parse parsed", scrapeResult);
streamResponse.info("parsed article, creating tags with openai");
const aiMeta = await openai.extractArticleMetadata(result.markdown);
const aiMeta = await openai.extractArticleMetadata(scrapeResult.markdown);
streamResponse.info("postprocessing article");
const title = result?.title || aiMeta?.headline || "";
const title = scrapeResult?.title || aiMeta?.headline || "";
let coverImagePath: string | undefined = undefined;
if (result?.image?.length) {
log.debug("using local image for cover image", { image: result.image });
if (scrapeResult?.image?.length) {
log.debug("using local image for cover image", {
image: scrapeResult.image,
});
coverImagePath = await fetchAndStoreCover(
result.image,
scrapeResult.image,
title,
streamResponse,
);
} else {
const urlPath = await getUnsplashCoverImage(
result.markdown,
scrapeResult.markdown,
streamResponse,
);
coverImagePath = await fetchAndStoreCover(urlPath, title, streamResponse);
@@ -119,15 +120,15 @@ async function processCreateArticle(
const newArticle: ArticleResource["content"] = {
_type: "Article",
headline: title,
articleBody: result.markdown,
articleBody: scrapeResult.markdown,
url: fetchUrl,
datePublished: formatDate(
result?.published || aiMeta?.datePublished || undefined,
scrapeResult?.published || aiMeta?.datePublished || undefined,
),
image: coverImagePath,
author: {
_type: "Person",
name: (result.schemaOrgData?.author?.name || aiMeta?.author || "")
name: (scrapeResult.schemaOrgData?.author?.name || aiMeta?.author || "")
.replace(
"@",
"twitter:",
@@ -201,8 +202,9 @@ async function processCreateYoutubeVideo(
streamResponse.send({ type: "finished", url: filename });
}
export const handler: Handlers = {
GET(req, ctx) {
export const handler = define.handlers({
GET(ctx) {
const req = ctx.req;
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -239,4 +241,4 @@ export const handler: Handlers = {
return streamResponse.response;
},
};
});

View File

@@ -1,6 +1,4 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { formatDate, safeFileName } from "@lib/string.ts";
import { fileExtension, formatDate, safeFileName } from "@lib/string.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import {
AccessDeniedError,
@@ -13,6 +11,7 @@ import { webScrape } from "@lib/webScraper.ts";
import * as openai from "@lib/openai.ts";
import * as unsplash from "@lib/unsplash.ts";
import { createLogger } from "@lib/log/index.ts";
import { define } from "../../../../utils.ts";
function ext(str: string) {
try {
@@ -163,29 +162,24 @@ async function processEnhanceArticle(
streamResponse.send({ type: "finished", url: name.replace(/$\.md/, "") });
}
const POST = (
_req: Request,
ctx: FreshContext,
): Response => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
export const handler = define.handlers({
POST: (ctx) => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
const streamResponse = createStreamResponse();
const streamResponse = createStreamResponse();
processEnhanceArticle(ctx.params.name, streamResponse)
.catch((err) => {
log.error(err);
streamResponse.error(err.message);
})
.finally(() => {
streamResponse.cancel();
});
processEnhanceArticle(ctx.params.name, streamResponse)
.catch((err) => {
log.error(err);
streamResponse.error(err.message);
})
.finally(() => {
streamResponse.cancel();
});
return streamResponse.response;
};
export const handler: Handlers = {
POST,
};
return streamResponse.response;
},
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async GET() {
const articles = await fetchResource("articles");
return json(articles?.content);
},
};
});

View File

@@ -1,5 +1,4 @@
import { Handlers } from "$fresh/server.ts";
import { create, getNumericDate } from "https://deno.land/x/djwt@v2.2/mod.ts";
import { create, getNumericDate } from "@zaubrik/djwt";
import { oauth2Client } from "@lib/auth.ts";
import { getCookies, setCookie } from "@std/http/cookie";
import { codeChallengeMap } from "./login.ts";
@@ -9,15 +8,16 @@ import { BadRequestError } from "@lib/errors.ts";
import { db } from "@lib/db/sqlite.ts";
import { userTable } from "@lib/db/schema.ts";
import { eq } from "drizzle-orm";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(request) {
export const handler = define.handlers({
async GET(ctx) {
if (!JWT_SECRET) {
throw new BadRequestError();
}
// Exchange the authorization code for an access token
const cookies = getCookies(request.headers);
const cookies = getCookies(ctx.req.headers);
const stored = codeChallengeMap.get(cookies["code_challenge"]);
if (!stored) {
@@ -26,7 +26,7 @@ export const handler: Handlers = {
const { codeVerifier, redirect } = stored;
const tokens = await oauth2Client.code.getToken(request.url, {
const tokens = await oauth2Client.code.getToken(ctx.req.url, {
codeVerifier,
});
@@ -53,11 +53,23 @@ export const handler: Handlers = {
user = res[0];
}
if (!JWT_SECRET) {
throw new BadRequestError();
}
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(JWT_SECRET),
{ name: "HMAC", hash: "SHA-512" },
false,
["sign", "verify"],
);
const jwt = await create({ alg: "HS512", type: "JWT" }, {
id: user.id,
name: user.name,
exp: getNumericDate(SESSION_DURATION),
}, JWT_SECRET);
}, key);
const headers = new Headers({
location: redirect || "/",
@@ -78,4 +90,4 @@ export const handler: Handlers = {
status: 302,
});
},
};
});

View File

@@ -1,14 +1,15 @@
import { Handlers } from "$fresh/server.ts";
import { oauth2Client } from "@lib/auth.ts";
import { setCookie } from "@std/http/cookie";
import { define } from "../../../utils.ts";
export const codeChallengeMap = new Map<
string,
{ codeVerifier: string; redirect?: string }
>();
export const handler: Handlers = {
async GET(req) {
export const handler = define.handlers({
async GET(ctx) {
const req = ctx.req;
const url = new URL(req.url);
const { codeVerifier, uri } = await oauth2Client.code.getAuthorizationUri();
@@ -33,4 +34,4 @@ export const handler: Handlers = {
status: 302,
});
},
};
});

View File

@@ -1,8 +1,9 @@
import { deleteCookie } from "@std/http/cookie";
import { Handlers } from "$fresh/server.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
GET(req) {
export const handler = define.handlers({
GET(ctx) {
const req = ctx.req;
const url = new URL(req.url);
const redirect = decodeURIComponent(url.searchParams.get("redirect") || "");
@@ -19,4 +20,4 @@ export const handler: Handlers = {
status: 302,
});
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { documentTable } from "@lib/db/schema.ts";
import { db } from "@lib/db/sqlite.ts";
import { json } from "@lib/helpers.ts";
import { caches } from "@lib/cache.ts";
import { define } from "../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async DELETE() {
for (const cache of caches.values()) {
cache.clear();
@@ -12,4 +12,4 @@ export const handler: Handlers = {
await db.delete(documentTable).run();
return json({ status: "ok" });
},
};
});

View File

@@ -1,6 +1,6 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { getImageContent } from "@lib/image.ts";
import { createLogger } from "@lib/log/index.ts";
import { Context } from "fresh";
const log = createLogger("api/image");
@@ -69,9 +69,9 @@ async function generateETag(content: Uint8Array<ArrayBuffer>): Promise<string> {
}"`;
}
async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
async function GET(ctx: Context<unknown>): Promise<Response> {
try {
const url = new URL(req.url);
const url = new URL(ctx.req.url);
const params = parseParams(url);
if (typeof params === "string") {
@@ -106,6 +106,6 @@ async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
}
}
export const handler: Handlers = {
export const handler = {
GET,
};

View File

@@ -1,8 +1,8 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { define } from "../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
GET() {
return json([]);
},
};
});

View File

@@ -1,9 +1,9 @@
import { Handlers } from "$fresh/server.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import { define } from "../../utils.ts";
const activeResponses: ReturnType<typeof createStreamResponse>[] = [];
export const handler: Handlers = {
export const handler = define.handlers({
GET() {
const r = createStreamResponse();
@@ -11,4 +11,4 @@ export const handler: Handlers = {
return r.response;
},
};
});

View File

@@ -1,8 +1,7 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import * as tmdb from "@lib/tmdb.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import {
fileExtension,
formatDate,
isString,
safeFileName,
@@ -11,13 +10,14 @@ import {
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const movie = await fetchResource(`movies/${ctx.params.name}`);
return json(movie?.content);
},
async POST(_, ctx) {
async POST(ctx) {
const session = ctx.state.session;
if (!session) throw new AccessDeniedError();
@@ -70,4 +70,4 @@ export const handler: Handlers = {
return json({ name: fileName });
},
};
});

View File

@@ -1,7 +1,6 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import * as tmdb from "@lib/tmdb.ts";
import {
fileExtension,
formatDate,
isString,
safeFileName,
@@ -16,80 +15,76 @@ import {
import { createRecommendationResource } from "@lib/recommendation.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { define } from "../../../../utils.ts";
const POST = async (
req: Request,
ctx: FreshContext,
): Promise<Response> => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
export const handler = define.handlers({
POST: async function (ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
const movie = await fetchResource<ReviewResource>(
`movies/${ctx.params.name}`,
);
if (!movie) {
throw new NotFoundError();
}
const movie = await fetchResource<ReviewResource>(
`movies/${ctx.params.name}`,
);
if (!movie) {
throw new NotFoundError();
}
const body = await req.json();
const name = ctx.params.name;
const { tmdbId } = body;
if (!name || !tmdbId) {
throw new BadRequestError();
}
const body = await ctx.req.json();
const name = ctx.params.name;
const { tmdbId } = body;
if (!name || !tmdbId) {
throw new BadRequestError();
}
const movieDetails = await tmdb.getMovie(tmdbId);
const movieCredits = !movie.content?.author &&
await tmdb.getMovieCredits(tmdbId);
const movieDetails = await tmdb.getMovie(tmdbId);
const movieCredits = !movie.content?.author &&
await tmdb.getMovieCredits(tmdbId);
const director = movieCredits &&
movieCredits?.crew?.filter?.((person) => person.job === "Director")[0];
const director = movieCredits &&
movieCredits?.crew?.filter?.((person) => person.job === "Director")[0];
movie.content ??= {
_type: "Review",
};
movie.content.datePublished ??= formatDate(movieDetails.release_date);
if (director && !movie.content?.author) {
movie.content.author = {
_type: "Person",
name: director.name,
movie.content ??= {
_type: "Review",
};
}
if (movieDetails.genres) {
movie.content.keywords = [
...new Set([
...(movie.content.keywords?.map((g) => g.toLowerCase()) || []),
...movieDetails.genres.map((g) =>
g.name?.toLowerCase().replaceAll(" ", "-")
),
].filter(isString)),
];
}
movie.content.datePublished ??= formatDate(movieDetails.release_date);
movie.content.tmdbId ??= tmdbId;
if (director && !movie.content?.author) {
movie.content.author = {
_type: "Person",
name: director.name,
};
}
let finalPath = "";
const posterPath = movieDetails.poster_path;
if (posterPath && !movie.content.image) {
const poster = await tmdb.getMoviePoster(posterPath);
const extension = fileExtension(posterPath);
finalPath = `movies/images/${safeFileName(name)}_cover.${extension}`;
await createResource(finalPath, poster);
movie.content.image = finalPath;
}
if (movieDetails.genres) {
movie.content.keywords = [
...new Set([
...(movie.content.keywords?.map((g) => g.toLowerCase()) || []),
...movieDetails.genres.map((g) =>
g.name?.toLowerCase().replaceAll(" ", "-")
),
].filter(isString)),
];
}
await createResource(`movies/${toUrlSafeString(movie.name)}.md`, movie);
movie.content.tmdbId ??= tmdbId;
createRecommendationResource(movie, movieDetails.overview);
let finalPath = "";
const posterPath = movieDetails.poster_path;
if (posterPath && !movie.content.image) {
const poster = await tmdb.getMoviePoster(posterPath);
const extension = fileExtension(posterPath);
finalPath = `movies/images/${safeFileName(name)}_cover.${extension}`;
await createResource(finalPath, poster);
movie.content.image = finalPath;
}
return json(movie);
};
await createResource(`movies/${toUrlSafeString(movie.name)}.md`, movie);
export const handler: Handlers = {
POST,
};
createRecommendationResource(movie, movieDetails.overview);
return json(movie);
},
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async GET() {
const movies = await fetchResource("movies");
return json(movies);
},
};
});

View File

@@ -1,10 +1,11 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(req, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const req = ctx.req;
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -15,9 +16,8 @@ export const handler: Handlers = {
throw new BadRequestError();
}
console.log(s);
const resources = await searchResource(s);
return json(resources);
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const recipe = await fetchResource(`recipes/${ctx.params.name}`);
return json(recipe);
},
};
});

View File

@@ -1,16 +1,15 @@
import { Handlers } from "$fresh/server.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createStreamResponse, isValidUrl } from "@lib/helpers.ts";
import * as openai from "@lib/openai.ts";
import { createLogger } from "@lib/log/index.ts";
import recipeSchema from "@lib/recipeSchema.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { safeFileName, toUrlSafeString } from "@lib/string.ts";
import { fileExtension, safeFileName, toUrlSafeString } from "@lib/string.ts";
import { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts";
import z from "zod";
import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts";
import { RecipeResource } from "@lib/marka/schema.ts";
import { define } from "../../../../utils.ts";
const log = createLogger("api/article");
@@ -93,8 +92,9 @@ async function processCreateRecipeFromUrl(
streamResponse.send({ type: "finished", url: id });
}
export const handler: Handlers = {
GET(req, ctx) {
export const handler = define.handlers({
GET(ctx) {
const req = ctx.req;
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -120,4 +120,4 @@ export const handler: Handlers = {
return streamResponse.response;
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async GET() {
const recipes = await fetchResource("recipes");
return json(recipes);
},
};
});

View File

@@ -1,4 +1,3 @@
import { Handlers } from "$fresh/server.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import * as tmdb from "@lib/tmdb.ts";
import {
@@ -8,6 +7,7 @@ import {
import { AccessDeniedError } from "@lib/errors.ts";
import { listResources } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { define } from "../../../utils.ts";
async function processUpdateRecommendations(
streamResponse: ReturnType<typeof createStreamResponse>,
@@ -53,8 +53,8 @@ async function processUpdateRecommendations(
streamResponse.info("100% Finished");
}
export const handler: Handlers = {
GET(_, ctx) {
export const handler = define.handlers({
GET(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -65,4 +65,4 @@ export const handler: Handlers = {
return streamResponse.response;
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { getAllRecommendations } from "@lib/recommendation.ts";
import { json } from "@lib/helpers.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -34,4 +34,4 @@ export const handler: Handlers = {
keywords,
});
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { getAllRecommendations } from "@lib/recommendation.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async GET() {
const recommendations = await getAllRecommendations();
return json(recommendations);
},
};
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { getSimilarMovies } from "@lib/recommendation.ts";
import { json } from "@lib/helpers.ts";
import { define } from "../../../../utils.ts";
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -14,4 +14,4 @@ export const handler: Handlers = {
return json(recommendations);
},
};
});

View File

@@ -1,12 +1,16 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import * as tmdb from "@lib/tmdb.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { formatDate, isString, safeFileName } from "@lib/string.ts";
import {
fileExtension,
formatDate,
isString,
safeFileName,
} from "@lib/string.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { toUrlSafeString } from "@lib/string.ts";
import { define } from "../../../utils.ts";
function pickDirector(
credits: Awaited<ReturnType<typeof tmdb.getSeriesCredits>>,
@@ -16,12 +20,12 @@ function pickDirector(
return crewDirector?.name ?? createdBy?.[0]?.name;
}
export const handler: Handlers = {
async GET(_, ctx) {
export const handler = define.handlers({
async GET(ctx) {
const series = await fetchResource(`series/${ctx.params.name}`);
return json(series);
},
async POST(_, ctx) {
async POST(ctx) {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
@@ -78,4 +82,4 @@ export const handler: Handlers = {
return json({ name: fileName });
},
};
});

View File

@@ -1,7 +1,5 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import * as tmdb from "@lib/tmdb.ts";
import { safeFileName } from "@lib/string.ts";
import { fileExtension, safeFileName } from "@lib/string.ts";
import { json } from "@lib/helpers.ts";
import {
AccessDeniedError,
@@ -9,74 +7,75 @@ import {
NotFoundError,
} from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../../utils.ts";
const isString = (input: string | undefined): input is string => {
return typeof input === "string";
};
const POST = async (
req: Request,
ctx: FreshContext,
): Promise<Response> => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
export const handler = define.handlers({
POST: async (ctx) => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
const body = await req.json();
const name = ctx.params.name;
const { tmdbId } = body;
if (!name || !tmdbId) {
throw new BadRequestError();
}
const body = await ctx.req.json();
const name = ctx.params.name;
const { tmdbId } = body;
if (!name || !tmdbId) {
throw new BadRequestError();
}
const series = await fetchResource(`series/${ctx.params.name}`);
if (!series) {
throw new NotFoundError();
}
const series = await fetchResource(`series/${ctx.params.name}`);
if (!series) {
throw new NotFoundError();
}
const seriesDetails = await tmdb.getSeries(tmdbId);
const seriesCredits = !series?.content?.author &&
await tmdb.getSeriesCredits(tmdbId);
const seriesDetails = await tmdb.getSeries(tmdbId);
const seriesCredits = !series?.content?.author &&
await tmdb.getSeriesCredits(tmdbId);
const releaseDate = seriesDetails.first_air_date;
if (releaseDate && series.content?.datePublished) {
series.content.datePublished = new Date(releaseDate).toISOString();
}
const posterPath = seriesDetails.poster_path;
const director = seriesCredits &&
seriesCredits.crew?.filter?.((person) => person.job === "Director")[0] ||
seriesDetails?.created_by?.[0];
if (director && director.name && !series.content?.author) {
series.content.author = series.content.author || {
_type: "Person",
name: director.name,
};
}
const releaseDate = seriesDetails.first_air_date;
if (releaseDate && series.content?.datePublished) {
const d = new Date(releaseDate);
if (!isNaN(d.getTime())) {
series.content.datePublished = d.toISOString();
}
}
const posterPath = seriesDetails.poster_path;
const director = seriesCredits &&
seriesCredits.crew?.filter?.((person) =>
person.job === "Director"
)[0] ||
seriesDetails?.created_by?.[0];
if (director && director.name && !series.content?.author) {
series.content.author = series.content.author || {
_type: "Person",
name: director.name,
};
}
if (seriesDetails.genres) {
series.content.keywords = [
...new Set([
...(series.content.keywords?.map((t) => t.toLowerCase()) || []),
...seriesDetails.genres.map((g) => g.name?.toLowerCase()),
].filter(isString)),
];
}
if (seriesDetails.genres) {
series.content.keywords = [
...new Set([
...(series.content.keywords?.map((t) => t.toLowerCase()) || []),
...seriesDetails.genres.map((g) => g.name?.toLowerCase()),
].filter(isString)),
];
}
let finalPath = "";
if (posterPath && !series.content?.image) {
const poster = await tmdb.getMoviePoster(posterPath);
const extension = fileExtension(posterPath);
let finalPath = "";
if (posterPath && !series.content?.image) {
const poster = await tmdb.getMoviePoster(posterPath);
const extension = fileExtension(posterPath);
finalPath = `series/images/${safeFileName(name)}_cover.${extension}`;
await createResource(finalPath, poster);
series.content.image = finalPath;
}
await createResource(`series/${safeFileName(series.name)}.md`, series);
finalPath = `series/images/${safeFileName(name)}_cover.${extension}`;
await createResource(finalPath, poster);
series.content.image = finalPath;
}
await createResource(`series/${safeFileName(series.name)}.md`, series);
return json(series);
};
export const handler: Handlers = {
POST,
};
return json(series);
},
});

View File

@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { define } from "../../../utils.ts";
export const handler: Handlers = {
export const handler = define.handlers({
async GET() {
const series = await fetchResource("series");
return json(series);
},
};
});

View File

@@ -1,7 +1,7 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { getMovie } from "@lib/tmdb.ts";
import { json } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts";
import { define } from "../../../utils.ts";
type CachedMovieCredits = {
lastUpdated: number;
@@ -13,40 +13,36 @@ const cache = createCache<CachedMovieCredits>("movie-credits", {
expires: CACHE_INTERVAL,
});
const GET = async (
_req: Request,
_ctx: FreshContext,
) => {
const id = _ctx.params.id;
export const handler = define.handlers({
GET: async (ctx) => {
const id = ctx.params.id;
if (!id) {
return new Response("Bad Request", {
status: 400,
});
}
if (!id) {
return new Response("Bad Request", {
status: 400,
});
}
const cacheId = `/movie/${id}`;
const cacheId = `/movie/${id}`;
const cachedResponse = cache.get(cacheId);
if (
cachedResponse && Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
) {
return json(cachedResponse.data);
}
const cachedResponse = cache.get(cacheId);
if (
cachedResponse &&
Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
) {
return json(cachedResponse.data);
}
const res = await getMovie(+id);
const res = await getMovie(+id);
cache.set(
cacheId,
JSON.stringify({
lastUpdated: Date.now(),
data: res,
}),
);
cache.set(
cacheId,
JSON.stringify({
lastUpdated: Date.now(),
data: res,
}),
);
return json(res);
};
export const handler: Handlers = {
GET,
};
return json(res);
},
});

View File

@@ -1,8 +1,8 @@
import { FreshContext } from "$fresh/server.ts";
import { getMovieCredits } from "@lib/tmdb.ts";
import { json } from "@lib/helpers.ts";
import { createLogger } from "@lib/log/index.ts";
import { createCache } from "@lib/cache.ts";
import { define } from "../../../../utils.ts";
type CachedMovieCredits = {
lastUpdated: number;
@@ -16,37 +16,37 @@ const cache = createCache<CachedMovieCredits>("movie-credits", {
const log = createLogger("api/tmdb");
export const handler = async (
_req: Request,
_ctx: FreshContext,
) => {
const id = _ctx.params.id;
export const handler = define.handlers({
GET: async (ctx) => {
const id = ctx.params.id;
if (!id) {
return new Response("Bad Request", {
status: 400,
});
}
if (!id) {
return new Response("Bad Request", {
status: 400,
});
}
log.debug("getting movie credits");
log.debug("getting movie credits");
const cacheId = `/movie/credits/${id}`;
const cacheId = `/movie/credits/${id}`;
const cachedResponse = cache.get(cacheId);
if (
cachedResponse && Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
) {
return json(cachedResponse.data);
}
const cachedResponse = cache.get(cacheId);
if (
cachedResponse &&
Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL)
) {
return json(cachedResponse.data);
}
const res = await getMovieCredits(+id);
cache.set(
cacheId,
JSON.stringify({
lastUpdated: Date.now(),
data: res,
}),
);
const res = await getMovieCredits(+id);
cache.set(
cacheId,
JSON.stringify({
lastUpdated: Date.now(),
data: res,
}),
);
return json(res);
};
return json(res);
},
});

View File

@@ -1,33 +1,28 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { searchMovie, searchTVShow } from "@lib/tmdb.ts";
import { AccessDeniedError, BadRequestError } from "@lib/errors.ts";
import { define } from "../../../utils.ts";
const GET = async (
req: Request,
ctx: FreshContext,
) => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
export const handler = define.handlers({
GET: async (ctx) => {
const session = ctx.state.session;
if (!session) {
throw new AccessDeniedError();
}
const u = new URL(req.url);
const u = new URL(ctx.req.url);
const query = u.searchParams.get("q");
const query = u.searchParams.get("q");
if (!query) {
throw new BadRequestError();
}
if (!query) {
throw new BadRequestError();
}
const type = u.searchParams.get("type") || "movies";
const type = u.searchParams.get("type") || "movies";
const res = type === "movies"
? await searchMovie(query)
: await searchTVShow(query);
const res = type === "movies"
? await searchMovie(query)
: await searchTVShow(query);
return new Response(JSON.stringify(res.results));
};
export const handler: Handlers = {
GET,
};
return new Response(JSON.stringify(res.results));
},
});

View File

@@ -1,4 +1,4 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { PageProps } from "fresh";
import { MainLayout } from "@components/layouts/main.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { YoutubePlayer } from "@components/Youtube.tsx";
@@ -12,21 +12,22 @@ import { MetaTags } from "@components/MetaTags.tsx";
import { fetchResource } from "@lib/marka/index.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
import { parseRating } from "@lib/helpers.ts";
import { HttpError } from "fresh";
import { define } from "../../utils.ts";
export const handler: Handlers<{ article: ArticleResource; session: unknown }> =
{
async GET(_, ctx) {
const article = await fetchResource<ArticleResource>(
`articles/${ctx.params.name}.md`,
);
if (!article) {
return ctx.renderNotFound();
}
return ctx.render({ article, session: ctx.state.session });
},
};
export const handler = define.handlers({
async GET(ctx) {
const article = await fetchResource<ArticleResource>(
`articles/${ctx.params.name}.md`,
);
if (!article) {
throw new HttpError(404);
}
return { data: { article, session: ctx.state.session } };
},
});
export default function Greet(
export default define.page(function (
props: PageProps<
{ article: ArticleResource; session: Record<string, string> }
>,
@@ -103,4 +104,4 @@ export default function Greet(
</div>
</MainLayout>
);
}
});

Some files were not shown because too many files have changed in this diff Show More