diff --git a/components/Card.tsx b/components/Card.tsx
index 1cea3e5..6eff7b4 100644
--- a/components/Card.tsx
+++ b/components/Card.tsx
@@ -11,13 +11,13 @@ export function Card(
rating,
title,
image,
- thumbnail,
+ thumbhash,
backgroundColor,
backgroundSize = 100,
}: {
backgroundSize?: number;
backgroundColor?: string;
- thumbnail?: string;
+ thumbhash?: string;
link?: string;
title?: string;
image?: string;
@@ -39,22 +39,20 @@ export function Card(
- {true && (
-
-
-
- )}
+
+
+
diff --git a/components/Image.tsx b/components/Image.tsx
index 7b5d435..7dd7c7b 100644
--- a/components/Image.tsx
+++ b/components/Image.tsx
@@ -34,7 +34,7 @@ const Image = (
class: string;
src: string;
alt?: string;
- thumbnail?: string;
+ thumbhash?: string;
fill?: boolean;
width?: number | string;
height?: number | string;
@@ -55,19 +55,17 @@ const Image = (
height: props.fill ? "100%" : "",
zIndex: props.fill ? -1 : "",
}}
- data-thumb={props.thumbnail}
+ data-thumb={props.thumbhash}
>
({
+const HeroContext = createContext<{ image?: string; thumbhash?: string }>({
image: undefined,
- thumbnail: undefined,
+ thumbhash: undefined,
});
function Wrapper(
- { children, image, thumbnail }: {
+ { children, image, thumbhash }: {
children: ComponentChildren;
image?: string;
- thumbnail?: string;
+ thumbhash?: string;
},
) {
return (
{image &&
@@ -30,7 +29,7 @@ function Wrapper(
diff --git a/drizzle/0010_youthful_tyrannus.sql b/drizzle/0010_youthful_tyrannus.sql
new file mode 100644
index 0000000..01f1491
--- /dev/null
+++ b/drizzle/0010_youthful_tyrannus.sql
@@ -0,0 +1 @@
+ALTER TABLE `image` RENAME COLUMN "blurhash" TO "thumbhash";
\ No newline at end of file
diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json
new file mode 100644
index 0000000..3d6d9fe
--- /dev/null
+++ b/drizzle/meta/0010_snapshot.json
@@ -0,0 +1,311 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "685b57ca-45e0-4373-baee-fc3abb4f2d74",
+ "prevId": "5694a345-e55c-4aa3-9f29-1045b28f5203",
+ "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": "(current_timestamp)"
+ },
+ "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": {
+ "\"image\".\"blurhash\"": "\"image\".\"thumbhash\""
+ }
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 4a32ec0..c4b474b 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -71,6 +71,13 @@
"when": 1736172911816,
"tag": "0009_free_robin_chapel",
"breakpoints": true
+ },
+ {
+ "idx": 10,
+ "version": "6",
+ "when": 1762099260474,
+ "tag": "0010_youthful_tyrannus",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/islands/IngredientsList.tsx b/islands/IngredientsList.tsx
index 33ed81c..e37d49d 100644
--- a/islands/IngredientsList.tsx
+++ b/islands/IngredientsList.tsx
@@ -2,7 +2,7 @@ import { Signal } from "@preact/signals";
import type { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
import { FunctionalComponent } from "preact";
import { unitsOfMeasure } from "@lib/parseIngredient.ts";
-import { renderMarkdown } from "@lib/documents.ts";
+import { renderMarkdown } from "@lib/markdown.ts";
function formatAmount(num: number) {
if (num === 0) return "";
diff --git a/islands/KMenu/commands/add_movie_infos.ts b/islands/KMenu/commands/add_movie_infos.ts
index deb809f..11d02f2 100644
--- a/islands/KMenu/commands/add_movie_infos.ts
+++ b/islands/KMenu/commands/add_movie_infos.ts
@@ -1,7 +1,7 @@
-
-import { Movie } from "@lib/resource/movies.ts";
import { TMDBMovie } from "@lib/types.ts";
import { getCookie } from "@lib/string.ts";
+import { MenuEntry } from "../types.ts";
+import { ReviewResource } from "@lib/marka/schema.ts";
export const addMovieInfos: MenuEntry = {
title: "Add Movie infos",
@@ -9,7 +9,7 @@ export const addMovieInfos: MenuEntry = {
icon: "IconReportSearch",
cb: async (state, context) => {
state.activeState.value = "loading";
- const movie = context as Movie;
+ const movie = context as ReviewResource;
const query = movie.name;
@@ -33,7 +33,7 @@ export const addMovieInfos: MenuEntry = {
});
state.visible.value = false;
state.activeState.value = "normal";
- window.location.reload();
+ globalThis.location.reload();
},
})),
};
diff --git a/islands/KMenu/commands/add_series_infos.ts b/islands/KMenu/commands/add_series_infos.ts
index c168a22..9461547 100644
--- a/islands/KMenu/commands/add_series_infos.ts
+++ b/islands/KMenu/commands/add_series_infos.ts
@@ -1,7 +1,7 @@
import { MenuEntry } from "@islands/KMenu/types.ts";
import { TMDBSeries } from "@lib/types.ts";
import { getCookie } from "@lib/string.ts";
-import { Series } from "@lib/resource/series.ts";
+import { ReviewResource } from "@lib/marka/schema.ts";
export const addSeriesInfo: MenuEntry = {
title: "Add Series infos",
@@ -9,7 +9,7 @@ export const addSeriesInfo: MenuEntry = {
icon: "IconReportSearch",
cb: async (state, context) => {
state.activeState.value = "loading";
- const series = context as Series;
+ const series = context as ReviewResource;
const query = series.name;
diff --git a/islands/KMenu/commands/create_movie.ts b/islands/KMenu/commands/create_movie.ts
index 7bbf597..d2e2b8b 100644
--- a/islands/KMenu/commands/create_movie.ts
+++ b/islands/KMenu/commands/create_movie.ts
@@ -1,8 +1,8 @@
import { MenuEntry } from "@islands/KMenu/types.ts";
import { TMDBMovie } from "@lib/types.ts";
import { debounce } from "@lib/helpers.ts";
-import { Movie } from "@lib/resource/movies.ts";
import { getCookie } from "@lib/string.ts";
+import { ReviewResource } from "@lib/marka/schema.ts";
export const createNewMovie: MenuEntry = {
title: "Create new movie",
@@ -52,9 +52,9 @@ export const createNewMovie: MenuEntry = {
const response = await fetch("/api/movies/" + r.id, {
method: "POST",
});
- const movie = await response.json() as Movie;
+ const movie = await response.json() as ReviewResource;
unsub();
- window.location.href = "/movies/" + movie.name;
+ globalThis.location.href = "/movies/" + movie.name;
},
};
}),
diff --git a/islands/KMenu/commands/create_series.ts b/islands/KMenu/commands/create_series.ts
index 51f5725..9987e79 100644
--- a/islands/KMenu/commands/create_series.ts
+++ b/islands/KMenu/commands/create_series.ts
@@ -1,8 +1,8 @@
import { MenuEntry } from "@islands/KMenu/types.ts";
import { TMDBSeries } from "@lib/types.ts";
import { debounce } from "@lib/helpers.ts";
-import { Series } from "@lib/resource/series.ts";
import { getCookie } from "@lib/string.ts";
+import { ReviewResource } from "@lib/marka/schema.ts";
export const createNewSeries: MenuEntry = {
title: "Create new series",
@@ -54,9 +54,9 @@ export const createNewSeries: MenuEntry = {
const response = await fetch("/api/series/" + r.id, {
method: "POST",
});
- const series = await response.json() as Series;
+ const series = await response.json() as ReviewResource;
unsub();
- window.location.href = "/series/" + series.name;
+ globalThis.location.href = "/series/" + series.name;
},
};
}),
diff --git a/islands/Link.tsx b/islands/Link.tsx
index ea8e4ec..014e184 100644
--- a/islands/Link.tsx
+++ b/islands/Link.tsx
@@ -6,13 +6,16 @@ declare global {
}
export function Link(
- { href, children, class: _class, style }: {
+ props: {
href?: string;
class?: string;
style?: preact.JSX.CSSProperties;
children: preact.ComponentChildren;
+ "data-thumb"?: string;
},
) {
+ const { href, children, class: _class, style } = props;
+ const thumbhash = props["data-thumb"];
function handleClick() {
if (globalThis.loadingTimeout) {
return;
@@ -41,6 +44,7 @@ export function Link(
href={href}
style={style}
onClick={handleClick}
+ data-thumb={thumbhash}
class={_class}
>
{children}
diff --git a/islands/Search.tsx b/islands/Search.tsx
index b1aa4fe..d822eac 100644
--- a/islands/Search.tsx
+++ b/islands/Search.tsx
@@ -2,7 +2,6 @@ import { useEffect, useRef } from "preact/hooks";
import useDebouncedCallback from "@lib/hooks/useDebouncedCallback.ts";
import { IconLoader2, IconSearch } from "@components/icons.tsx";
import { useEventListener } from "@lib/hooks/useEventListener.ts";
-import { GenericResource } from "@lib/types.ts";
import { resources } from "@lib/resources.ts";
import { getCookie } from "@lib/string.ts";
import { IS_BROWSER } from "$fresh/runtime.ts";
@@ -11,6 +10,7 @@ import { Rating } from "@components/Rating.tsx";
import { useSignal } from "@preact/signals";
import Image from "@components/Image.tsx";
import { Emoji } from "@components/Emoji.tsx";
+import { GenericResource } from "@lib/marka/schema.ts";
export async function fetchQueryResource(url: URL, type = "") {
const query = url.searchParams.get("q");
@@ -46,7 +46,7 @@ export const RedirectSearchHandler = () => {
}, IS_BROWSER ? document?.body : undefined);
}
- return <>>;
+ return;
};
const SearchResultImage = ({ src }: { src: string }) => {
@@ -67,8 +67,9 @@ export const SearchResultItem = (
showEmoji?: boolean;
},
) => {
- const resourceType = resources[item.type];
- const href = resourceType ? `${resourceType.link}/${item.id}` : "";
+ const resourceType = resources[item?.content._type];
+ const href = item?.path.replace("/resources", "").replace(/\.md$/, "");
+ console.log({ item, href });
return (
: ""}
- {item.meta?.image && }
- {item?.name}
+ {item.image && }
+ {item.content?.headline || item.content?.name ||
+ item.content?.itemReviewed.name || item?.name}
);
};
@@ -190,16 +192,16 @@ const Search = (
{data.value?.length && !isLoading.value
?
: isLoading.value
- ?
- : (
-
-
- No Results
-
- )}
+ ?
+ : (
+
+
+ No Results
+
+ )}
);
};
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
index 034a58f..90ca58c 100644
--- a/lib/db/schema.ts
+++ b/lib/db/schema.ts
@@ -47,7 +47,7 @@ export const imageTable = sqliteTable("image", {
),
url: text().notNull(),
average: text().notNull(),
- blurhash: text().notNull(),
+ thumbhash: text().notNull(),
mime: text().notNull(),
});
diff --git a/lib/helpers.ts b/lib/helpers.ts
index b57c695..f0c8b23 100644
--- a/lib/helpers.ts
+++ b/lib/helpers.ts
@@ -104,12 +104,13 @@ export function debounce) => void>(
export function parseRating(rating: string | number) {
if (typeof rating === "string") {
try {
- return parseInt(rating);
+ const res = parseInt(rating);
+ if (!Number.isNaN(res)) return res;
} catch (_e) {
// This is okay
}
- return [...rating.matchAll(/⭐/g)].length;
+ return rating.length / 2;
}
return rating;
}
diff --git a/lib/image.ts b/lib/image.ts
index 6a1e2b4..5fc0046 100644
--- a/lib/image.ts
+++ b/lib/image.ts
@@ -3,7 +3,7 @@ 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 path from "node:path";
-import { ensureDir } from "fs";
+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";
@@ -13,7 +13,7 @@ import sharp from "npm:sharp@next";
const log = createLogger("cache/image");
const imageDir = path.join(DATA_DIR, "images");
-await ensureDir(imageDir);
+await mkdir(imageDir, { recursive: true });
async function getRemoteImage(imageUrl: string) {
try {
@@ -100,7 +100,7 @@ async function getLocalImagePath(
hostname,
pathname.split("/").filter((s) => s.length).join("-"),
);
- await ensureDir(imagePath);
+ await mkdir(imagePath, { recursive: true });
if (width || height) {
imagePath = path.join(imagePath, `${width ?? "-"}x${height ?? "-"}`);
@@ -313,7 +313,7 @@ export async function getImage(url: string) {
// Store in database
const [newImage] = await db.insert(imageTable).values({
url: url,
- blurhash: thumbhash.hash,
+ thumbhash: thumbhash.hash,
average: thumbhash.average,
mime: imageContent.mediaType,
}).returning();
diff --git a/lib/log/constants.ts b/lib/log/constants.ts
index b50ea66..24e0082 100644
--- a/lib/log/constants.ts
+++ b/lib/log/constants.ts
@@ -1,12 +1,12 @@
import * as env from "@lib/env.ts";
-import { ensureDir } from "fs";
import { join } from "node:path";
import { getLogLevel, LOG_LEVEL } from "@lib/log/types.ts";
+import { mkdir } from "node:fs/promises";
export const LOG_DIR = join(env.DATA_DIR, "logs");
// Ensure the log directory exists
-await ensureDir(LOG_DIR);
+await mkdir(LOG_DIR, { recursive: true });
export let logLevel = getLogLevel(env.LOG_LEVEL);
export function setLogLevel(level: LOG_LEVEL) {
diff --git a/lib/marka.ts b/lib/marka.ts
deleted file mode 100644
index 7f587d8..0000000
--- a/lib/marka.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { MARKA_API_KEY } from "./env.ts";
-const url = `https://marka.max-richter.dev/resources`;
-//const url = "http://localhost:8080/resources";
-
-export async function fetchResource(resource: string) {
- try {
- const response = await fetch(
- `${url}/${resource}`,
- );
- return response.json();
- } catch (_e) {
- return [];
- }
-}
-
-export async function createResource(
- path: string,
- content: string | object | ArrayBuffer,
-) {
- const isJson = typeof content === "object" &&
- !(content instanceof ArrayBuffer);
- const fetchUrl = `${url}/${path}`;
- const response = await fetch(fetchUrl, {
- method: "POST",
- headers: {
- "Content-Type": isJson ? "application/json" : "",
- "Authentication": MARKA_API_KEY,
- },
- body: isJson ? JSON.stringify(content) : content,
- });
- if (!response.ok) {
- throw new Error(`Failed to create resource: ${response.status}`);
- }
- return response.json();
-}
diff --git a/lib/marka/index.ts b/lib/marka/index.ts
new file mode 100644
index 0000000..a16b031
--- /dev/null
+++ b/lib/marka/index.ts
@@ -0,0 +1,80 @@
+import { MARKA_API_KEY } from "../env.ts";
+import { getImage } from "../image.ts";
+import { GenericResource } from "./schema.ts";
+
+//const url = `https://marka.max-richter.dev`;
+const url = "http://localhost:8080";
+
+async function addImageToResource(
+ resource: GenericResource,
+): Promise {
+ const imageUrl = resource?.content?.image;
+ if (imageUrl) {
+ try {
+ const absoluteImageUrl = (imageUrl.startsWith("https://") ||
+ imageUrl.startsWith("http://"))
+ ? imageUrl
+ : `${url}/${imageUrl}`;
+ const image = await getImage(absoluteImageUrl);
+ return { ...resource, image } as T;
+ } catch (e) {
+ console.log(`Failed to fetch image: ${imageUrl}`, e);
+ }
+ }
+ return resource as T;
+}
+
+export async function fetchResource(
+ resource: string,
+): Promise {
+ try {
+ const response = await fetch(
+ `${url}/resources/${resource}`,
+ );
+ const res = await response.json();
+ return addImageToResource(res);
+ } catch (_e) {
+ return;
+ }
+}
+
+export async function listResources(
+ resource: string,
+): Promise {
+ try {
+ const response = await fetch(
+ `${url}/resources/${resource}`,
+ );
+ const list = await response.json();
+ return Promise.all(
+ list?.content
+ .filter((a: GenericResource) => a?.content?._type)
+ .map((res: GenericResource) => addImageToResource(res)),
+ );
+ } catch (_e) {
+ return [];
+ }
+}
+
+export async function createResource(
+ path: string,
+ content: string | object | ArrayBuffer,
+) {
+ const isJson = typeof content === "object" &&
+ !(content instanceof ArrayBuffer);
+ const fetchUrl = `${url}/resources/${path}`;
+ const headers = new Headers();
+ headers.append("Content-Type", isJson ? "application/json" : "");
+ if (MARKA_API_KEY) {
+ headers.append("Authentication", MARKA_API_KEY);
+ }
+ const response = await fetch(fetchUrl, {
+ method: "POST",
+ headers,
+ body: isJson ? JSON.stringify(content) : content,
+ });
+ if (!response.ok) {
+ throw new Error(`Failed to create resource: ${response.status}`);
+ }
+ return response.json();
+}
diff --git a/lib/marka/schema.ts b/lib/marka/schema.ts
new file mode 100644
index 0000000..a704b56
--- /dev/null
+++ b/lib/marka/schema.ts
@@ -0,0 +1,126 @@
+import { z } from "zod";
+import { imageTable } from "../db/schema.ts";
+
+export const PersonSchema = z.object({
+ _type: z.literal("Person"),
+ name: z.string().optional(),
+});
+
+export const ReviewRatingSchema = z.object({
+ bestRating: z.number().optional(),
+ worstRating: z.number().optional(),
+ // Accept number or string (e.g., "⭐️⭐️⭐️⭐️⭐️")
+ ratingValue: z.union([z.number(), z.string()]).optional(),
+});
+
+const WithAuthor = z.object({ author: PersonSchema.optional() });
+const WithKeywords = z.object({ keywords: z.array(z.string()).optional() });
+const WithImage = z.object({ image: z.string().optional() });
+const WithDatePublished = z.object({ datePublished: z.string().optional() });
+
+const BaseContent = WithAuthor.merge(WithKeywords)
+ .merge(WithImage)
+ .merge(WithDatePublished);
+
+export const BaseFileSchema = z.object({
+ type: z.literal("file"),
+ name: z.string(),
+ path: z.string(),
+ modTime: z.string(), // ISO timestamp string
+ mime: z.string(),
+ size: z.number().int().nonnegative(),
+});
+
+const makeContentSchema = <
+ TName extends "Article" | "Review" | "Recipe",
+ TShape extends z.ZodRawShape,
+>(
+ name: TName,
+ shape: TShape,
+) =>
+ z
+ .object({
+ _type: z.literal(name),
+ keywords: z.array(z.string()).optional(),
+ })
+ .merge(BaseContent)
+ .extend(shape);
+
+export const ArticleContentSchema = makeContentSchema("Article", {
+ headline: z.string().optional(),
+ articleBody: z.string().optional(),
+ url: z.string().optional(),
+ reviewRating: ReviewRatingSchema.optional(),
+});
+
+export const ReviewContentSchema = makeContentSchema("Review", {
+ tmdbId: z.number().optional(),
+ link: z.string().optional(),
+ reviewRating: ReviewRatingSchema.optional(),
+ reviewBody: z.string().optional(),
+ itemReviewed: z
+ .object({
+ name: z.string().optional(),
+ })
+ .optional(),
+});
+
+export const RecipeContentSchema = makeContentSchema("Recipe", {
+ description: z.string().optional(),
+ name: z.string().optional(),
+ recipeIngredient: z.array(z.string()).optional(),
+ recipeInstructions: z.array(z.string()).optional(),
+ totalTime: z.string().optional(),
+ recipeYield: z.number().optional(),
+ url: z.string().optional(),
+});
+
+export const articleMetadataSchema = z.object({
+ headline: z.union([z.null(), z.string()]).describe("Headline of the article"),
+ author: z.union([z.null(), z.string()]).describe("Author of the article"),
+ datePublished: z.union([z.null(), z.string()]).describe(
+ "Date the article was published",
+ ),
+ keywords: z.union([z.null(), z.array(z.string())]).describe(
+ "Keywords for the article",
+ ),
+});
+
+export const ArticleSchema = BaseFileSchema.extend({
+ content: ArticleContentSchema,
+});
+
+export const ReviewSchema = BaseFileSchema.extend({
+ content: ReviewContentSchema,
+});
+
+export const RecipeSchema = BaseFileSchema.extend({
+ content: RecipeContentSchema,
+});
+
+export const GenericResourceSchema = z.union([
+ ArticleSchema,
+ ReviewSchema,
+ RecipeSchema,
+]);
+
+export type Person = z.infer;
+export type ReviewRating = z.infer;
+
+export type BaseFile = z.infer;
+
+export type ArticleResource = z.infer & {
+ image?: typeof imageTable.$inferSelect;
+};
+
+export type ReviewResource = z.infer & {
+ image?: typeof imageTable.$inferSelect;
+};
+
+export type RecipeResource = z.infer & {
+ image?: typeof imageTable.$inferSelect;
+};
+
+export type GenericResource = z.infer & {
+ image?: typeof imageTable.$inferSelect;
+};
diff --git a/lib/documents.ts b/lib/markdown.ts
similarity index 89%
rename from lib/documents.ts
rename to lib/markdown.ts
index 71fe185..42c0521 100644
--- a/lib/documents.ts
+++ b/lib/markdown.ts
@@ -44,11 +44,3 @@ export function renderMarkdown(doc: string) {
allowMath: true,
});
}
-
-export function createDocument(
- path: string,
- entry: string,
- mimetype = "image/jpeg",
-) {
- console.log("creating", { path, entry, mimetype });
-}
diff --git a/lib/openai.ts b/lib/openai.ts
index d065e48..2145ad8 100644
--- a/lib/openai.ts
+++ b/lib/openai.ts
@@ -4,7 +4,7 @@ import { OPENAI_API_KEY } from "@lib/env.ts";
import { hashString } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts";
import { recipeResponseSchema } from "@lib/recipeSchema.ts";
-import { articleMetadataSchema } from "./resource/articles.ts";
+import { articleMetadataSchema } from "./marka/schema.ts";
const openAI = OPENAI_API_KEY && new OpenAI({ apiKey: OPENAI_API_KEY });
diff --git a/lib/recipeSchema.ts b/lib/recipeSchema.ts
index 9edcd94..d5dcbfa 100644
--- a/lib/recipeSchema.ts
+++ b/lib/recipeSchema.ts
@@ -1,4 +1,5 @@
import { z } from "npm:zod";
+import { RecipeResource } from "./marka/schema.ts";
export const IngredientSchema = z.object({
quantity: z.string().describe(
@@ -17,7 +18,6 @@ export const IngredientGroupSchema = z.object({
export type IngredientGroup = z.infer;
const recipeSchema = z.object({
- _type: z.literal("Recipe"),
name: z.string().describe(
"Title of the Recipe, without the name of the website or author",
),
@@ -29,6 +29,9 @@ const recipeSchema = z.object({
_type: z.literal("Person"),
name: z.string().describe("author of the Recipe (optional)"),
}),
+ keywords: z.array(z.string()).describe(
+ "List of keywords that match the recipe",
+ ),
recipeIngredient: z.array(z.string())
.describe("List of ingredients"),
recipeInstructions: z.array(z.string()).describe("List of instructions"),
@@ -48,7 +51,7 @@ export const recipeResponseSchema = z.union([recipeSchema, noRecipeSchema]);
export function isValidRecipe(
recipe:
- | Recipe
+ | RecipeResource
| null
| undefined,
) {
diff --git a/lib/recommendation.ts b/lib/recommendation.ts
index 8dbca87..38cde98 100644
--- a/lib/recommendation.ts
+++ b/lib/recommendation.ts
@@ -1,8 +1,8 @@
import * as openai from "@lib/openai.ts";
import * as tmdb from "@lib/tmdb.ts";
-import { GenericResource } from "@lib/types.ts";
import { parseRating } from "@lib/helpers.ts";
import { createCache } from "@lib/cache.ts";
+import { GenericResource, ReviewResource } from "./marka/schema.ts";
type RecommendationResource = {
id: string;
@@ -18,42 +18,48 @@ type RecommendationResource = {
const cache = createCache("recommendations");
export async function createRecommendationResource(
- res: GenericResource,
+ res: ReviewResource,
description?: string,
) {
- const cacheId = `${res.type}:${res.id.replaceAll(":", "")}`;
+ const cacheId = `${res.type}:${res.name.replaceAll(":", "")}`;
const resource = cache.get(cacheId) || {
- id: res.id,
+ id: res.name,
type: res.type,
rating: -1,
};
if (description && !resource.keywords) {
- const keywords = await openai.createKeywords(res.type, description, res.id);
+ const keywords = await openai.createKeywords(
+ res.type,
+ description,
+ res.name,
+ );
if (keywords?.length) {
resource.keywords = keywords;
}
}
- const { author, date, rating } = res.meta || {};
+ const { author, datePublished, reviewRating } = res.content;
- if (res?.tags) {
- resource.tags = res.tags;
+ if (res?.content?.keywords) {
+ resource.keywords = res.content.keywords;
}
- if (typeof rating !== "undefined") {
- resource.rating = parseRating(rating);
+ if (typeof reviewRating?.ratingValue !== "undefined") {
+ resource.rating = parseRating(reviewRating?.ratingValue);
}
- if (author) {
- resource.author = author;
+ if (author?.name) {
+ resource.author = author.name;
}
if (description) {
resource.description = description;
}
- if (date) {
- const d = typeof date === "string" ? new Date(date) : date;
+ if (datePublished) {
+ const d = typeof datePublished === "string"
+ ? new Date(datePublished)
+ : datePublished;
resource.year = d.getFullYear();
}
diff --git a/lib/resource/articles.ts b/lib/resource/articles.ts
deleted file mode 100644
index a55d47c..0000000
--- a/lib/resource/articles.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { z } from "zod";
-export type Article = {
- _type: "Article";
- headline?: string;
- datePublished?: string;
- articleBody?: string;
- keywords?: string[];
- image?: string;
- url?: string;
- reviewRating?: {
- bestRating?: number;
- worstRating?: number;
- ratingValue?: number;
- };
- author?: {
- _type: "Person";
- name?: string;
- };
-};
-
-export const articleMetadataSchema = z.object({
- headline: z.union([z.null(), z.string()]).describe("Headline of the article"),
- author: z.union([z.null(), z.string()]).describe("Author of the article"),
- datePublished: z.union([z.null(), z.string()]).describe(
- "Date the article was published",
- ),
- keywords: z.union([z.null(), z.array(z.string())]).describe(
- "Keywords for the article",
- ),
-});
diff --git a/lib/resource/movies.ts b/lib/resource/movies.ts
deleted file mode 100644
index 18b33e5..0000000
--- a/lib/resource/movies.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export type Movie = {
- _type: "Review";
- tmdbId?: number;
- link?: string;
- author?: {
- _type: "Person";
- name?: string;
- };
- datePublished?: string;
- reviewRating?: {
- bestRating?: number;
- worstRating?: number;
- ratingValue?: number;
- };
- reviewBody?: string;
- itemReviewed?: {
- name?: string;
- };
- keywords?: string[];
- image?: string;
-};
diff --git a/lib/resource/recipes.ts b/lib/resource/recipes.ts
deleted file mode 100644
index 2697270..0000000
--- a/lib/resource/recipes.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export type Recipe = {
- _type: "Recipe";
- author?: {
- _type: "Person";
- name?: string;
- };
- description?: string;
- image?: string;
- name?: string;
- recipeIngredient?: string[];
- recipeInstructions?: string[];
- datePublished?: string;
- totalTime?: string;
- recipeYield?: number;
- url?: string;
- keywords?: string[];
-};
diff --git a/lib/resource/series.ts b/lib/resource/series.ts
deleted file mode 100644
index b6fc136..0000000
--- a/lib/resource/series.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { Movie } from "./movies.ts";
-
-export type Series = Movie;
diff --git a/lib/search.ts b/lib/search.ts
index 669d98e..084448f 100644
--- a/lib/search.ts
+++ b/lib/search.ts
@@ -1,12 +1,8 @@
import { resources } from "@lib/resources.ts";
import fuzzysort from "npm:fuzzysort";
-import { GenericResource } from "@lib/types.ts";
import { extractHashTags } from "@lib/string.ts";
-import { Movie } from "@lib/resource/movies.ts";
-import { Article } from "@lib/resource/articles.ts";
-import { Recipe } from "@lib/resource/recipes.ts";
-import { Series } from "@lib/resource/series.ts";
-import { fetchResource } from "./marka.ts";
+import { listResources } from "./marka/index.ts";
+import { GenericResource } from "./marka/schema.ts";
type ResourceType = keyof typeof resources;
@@ -48,8 +44,8 @@ export function parseResourceUrl(_url: string | URL): SearchParams | undefined {
}
const isResource = (
- item: Movie | Series | Article | Recipe | boolean,
-): item is Movie | Series | Article | Recipe => {
+ item: GenericResource | boolean | undefined,
+): item is GenericResource => {
return !!item;
};
@@ -57,10 +53,10 @@ export async function searchResource(
{ q, tags = [], types, rating }: SearchParams,
): Promise {
const resources = (await Promise.all([
- (!types || types.includes("movie")) && fetchResource("movies"),
- (!types || types.includes("series")) && fetchResource("series"),
- (!types || types.includes("article")) && fetchResource("articles"),
- (!types || types.includes("recipe")) && fetchResource("recipes"),
+ (!types || types.includes("movie")) && listResources("movies"),
+ (!types || types.includes("series")) && listResources("series"),
+ (!types || types.includes("article")) && listResources("articles"),
+ (!types || types.includes("recipe")) && listResources("recipes"),
])).flat().filter(isResource);
const results: Record = {};
@@ -68,27 +64,35 @@ export async function searchResource(
for (const resource of resources) {
if (
!(resource.name in results) &&
- tags?.length && resource.tags.length &&
- tags.every((t) => resource.tags.includes(t))
+ tags?.length && resource.content.keywords?.length &&
+ tags.every((t) => resource.content.keywords?.includes(t))
) {
- results[resource.id] = resource;
+ results[resource.name] = resource;
}
if (
- !(resource.id in results) &&
- rating && resource?.meta?.rating && resource.meta.rating >= rating
+ !(resource.name in results) &&
+ rating && resource?.content?.reviewRating &&
+ resource.content?.reviewRating?.ratingValue >= rating
) {
- results[resource.id] = resource;
+ results[resource.name] = resource;
}
}
if (q.length && q !== "*") {
const fuzzyResult = fuzzysort.go(q, resources, {
- keys: ["content", "name", "description", "meta.author"],
+ keys: [
+ "name",
+ "content.articleBody",
+ "content.reviewBody",
+ "content.name",
+ "content.description",
+ "content.author.name",
+ ],
threshold: 0.3,
});
for (const result of fuzzyResult) {
- results[result.obj.id] = result.obj;
+ results[result.obj.name] = result.obj;
}
}
diff --git a/lib/taskManager.ts b/lib/taskManager.ts
index 3182da4..c58878f 100644
--- a/lib/taskManager.ts
+++ b/lib/taskManager.ts
@@ -1,5 +1,5 @@
import { transcribe } from "@lib/openai.ts";
-import { createDocument } from "@lib/documents.ts";
+import { createResource } from "@lib/marka/index.ts";
import { createLogger } from "./log/index.ts";
import { convertOggToMp3 } from "./helpers.ts";
@@ -48,9 +48,8 @@ export async function endTask(chatId: string): Promise {
finalNote += "**[Voice message could not be transcribed]**\n\n";
}
} else if (entry.type === "photo") {
- const photoUrl = `${
- task.noteName.replace(/\.md$/, "")
- }/photo-${photoIndex++}.jpg`;
+ const photoUrl = `${task.noteName.replace(/\.md$/, "")
+ }/photo-${photoIndex++}.jpg`;
finalNote += `**Photo**:\n ${photoUrl}\n\n`;
photoTasks.push({
@@ -62,13 +61,13 @@ export async function endTask(chatId: string): Promise {
try {
for (const entry of photoTasks) {
- await createDocument(entry.path, entry.content, "image/jpeg");
+ await createResource(entry.path, entry.content);
}
} catch (err) {
log.error("Error creating photo document:", err);
}
try {
- await createDocument(task.noteName, finalNote, "text/markdown");
+ await createResource(task.noteName, finalNote);
} catch (error) {
log.error("Error creating document:", error);
return error instanceof Error
diff --git a/lib/types.ts b/lib/types.ts
index 97ac3dd..4597993 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,4 +1,4 @@
-import { resources } from "@lib/resources.ts";
+import { GenericResource, GenericResourceSchema } from "./marka/schema.ts";
export interface TMDBMovie {
adult: boolean;
@@ -33,22 +33,6 @@ export interface TMDBSeries {
vote_count: number;
}
-export type GenericResource = {
- name: string;
- id: string;
- tags?: string[];
- type: keyof typeof resources;
- content?: string;
- meta?: {
- image?: string;
- author?: string;
- rating?: number;
- average?: string;
- date?: Date | string;
- thumbnail?: string;
- };
-};
-
export interface GiteaOauthUser {
sub: string;
name: string;
@@ -61,7 +45,7 @@ export interface GiteaOauthUser {
export type SearchResult = {
id: string;
name: string;
- type: keyof typeof resources;
+ type: GenericResource["content"]["_type"];
date?: string;
rating: number;
tags: string[];
diff --git a/routes/_app.tsx b/routes/_app.tsx
index 4c24222..af7eac4 100644
--- a/routes/_app.tsx
+++ b/routes/_app.tsx
@@ -29,7 +29,7 @@ export default function App({ Component }: PageProps) {