feat: refactor whole bunch of stuff

This commit is contained in:
Max Richter
2025-11-02 19:03:11 +01:00
parent 81ebc8f5e0
commit e6b90cb785
56 changed files with 753 additions and 360 deletions

View File

@@ -29,7 +29,7 @@ export default function App({ Component }: PageProps) {
<Component />
</Partial>
</body>
<script src="/thumbnails.js" type="module" async defer />
<script src="/thumbhash.js" type="module" async defer />
</html>
);
}

View File

@@ -3,7 +3,7 @@ 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/documents.ts";
import { renderMarkdown } from "@lib/markdown.ts";
const renderLog = (t: unknown) =>
renderMarkdown(`\`\`\`js
@@ -42,7 +42,7 @@ function LogLine(
<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)}
.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}

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts";
import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = {
async GET(_, ctx) {

View File

@@ -3,8 +3,6 @@ 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";
import { Article } from "@lib/resource/articles.ts";
import { getYoutubeVideoDetails } from "@lib/youtube.ts";
import {
extractYoutubeId,
@@ -12,8 +10,9 @@ import {
toUrlSafeString,
} from "@lib/string.ts";
import { createLogger } from "@lib/log/index.ts";
import { createResource } from "@lib/marka.ts";
import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
const log = createLogger("api/article");
@@ -93,7 +92,7 @@ async function processCreateYoutubeVideo(
const id = newId || youtubeId;
const newArticle: Article = {
const newArticle: ArticleResource["content"] = {
_type: "Article",
headline: video.snippet.title,
articleBody: video.snippet.description,

View File

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

View File

@@ -63,8 +63,8 @@ function parseParams(reqUrl: URL): ImageParams | string {
async function generateETag(content: ArrayBuffer): Promise<string> {
const hashBuffer = await crypto.subtle.digest("SHA-256", content);
return `"${Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}"`;
}
@@ -80,13 +80,9 @@ async function GET(req: Request, _ctx: FreshContext): Promise<Response> {
});
}
const imageUrl = params.image.startsWith("resources")
? `https://marka.max-richter.dev/${params.image.replace(/^\//, "")}`
: params.image;
log.debug("Processing image request:", { params });
log.debug("Processing image request:", { imageUrl, params });
const image = await getImageContent(imageUrl, params);
const image = await getImageContent(params.image, params);
// Generate ETag based on image content
const eTag = await generateETag(image.content);

View File

@@ -1,11 +1,11 @@
import { Handlers } from "$fresh/server.ts";
import { Movie } from "@lib/resource/movies.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 { isString, safeFileName } from "@lib/string.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers = {
async GET(_, ctx) {
@@ -50,7 +50,7 @@ export const handler: Handlers = {
);
}
const movie: Movie = {
const movie: ReviewResource["content"] = {
_type: "Review",
image: `resources/${finalPath}`,
datePublished: releaseDate,

View File

@@ -9,7 +9,7 @@ import {
NotFoundError,
} from "@lib/errors.ts";
import { createRecommendationResource } from "@lib/recommendation.ts";
import { createResource, fetchResource } from "@lib/marka.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
const POST = async (
req: Request,

View File

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

View File

@@ -1,6 +1,6 @@
import { Handlers } from "$fresh/server.ts";
import { json } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts";
import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers = {
async GET(_, ctx) {

View File

@@ -3,15 +3,15 @@ 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 { Recipe } from "@lib/resource/recipes.ts";
import recipeSchema from "@lib/recipeSchema.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { safeFileName } from "@lib/string.ts";
import { parseJsonLdToRecipeSchema } from "./parseJsonLd.ts";
import z from "zod";
import { createResource } from "@lib/marka.ts";
import { createResource } from "@lib/marka/index.ts";
import { webScrape } from "@lib/webScraper.ts";
import { Defuddle } from "defuddle/node";
import { RecipeResource } from "@lib/marka/schema.ts";
const log = createLogger("api/article");
@@ -51,7 +51,7 @@ async function processCreateRecipeFromUrl(
recipe = await openai.extractRecipe(result.content);
}
const id = safeFileName(recipe?.title || "");
const id = safeFileName(recipe?.name || "");
if (!recipe) {
streamResponse.enqueue("failed to parse recipe");
@@ -59,23 +59,13 @@ async function processCreateRecipeFromUrl(
return;
}
const newRecipe: Recipe = {
const newRecipe: RecipeResource["content"] = {
...recipe,
_type: "Recipe",
name: recipe?.title,
description: recipe?.description,
recipeIngredient: recipe?.ingredients || [],
recipeInstructions: recipe?.instructions || [],
keywords: recipe.tags || [],
image: recipe?.image,
totalTime: recipe?.totalTime
? `${recipe?.totalTime?.toString()} minutes`
: undefined,
url: fetchUrl,
author: {
_type: "Person",
name: recipe?.author,
},
recipeYield: recipe?.servings,
};
if (newRecipe?.image && newRecipe.image.length > 5) {

View File

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

View File

@@ -1,25 +1,25 @@
import { Handlers } from "$fresh/server.ts";
import { createStreamResponse } from "@lib/helpers.ts";
import { Movie } from "@lib/resource/movies.ts";
import * as tmdb from "@lib/tmdb.ts";
import {
createRecommendationResource,
getRecommendation,
} from "@lib/recommendation.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { fetchResource } from "@lib/marka.ts";
import { listResources } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
async function processUpdateRecommendations(
streamResponse: ReturnType<typeof createStreamResponse>,
) {
const allMovies = await fetchResource("movies");
const allMovies = await listResources<ReviewResource>("movies");
const movies = allMovies?.content.filter((m) => {
const movies = allMovies?.filter((m: ReviewResource) => {
if (!m?.content) return false;
if (!m.content.reviewRating) return false;
if (!m.content.tmdbId) return false;
return true;
}) as Movie[];
}) as ReviewResource[];
streamResponse.enqueue("Fetched all movies");
@@ -27,22 +27,23 @@ async function processUpdateRecommendations(
const total = movies.length;
await Promise.all(movies.map(async (movie) => {
if (!movie.meta.tmdbId) return;
if (!movie.meta.rating) return;
const recommendation = getRecommendation(movie.id, movie.type);
if (!movie.content.tmdbId) return;
if (!movie.content.reviewRating) return;
const recommendation = getRecommendation(movie.name, movie.type);
if (recommendation) {
done++;
return;
}
try {
const movieDetails = await tmdb.getMovie(movie.meta.tmdbId);
const movieDetails = await tmdb.getMovie(movie.content.tmdbId);
await createRecommendationResource(movie, movieDetails.overview);
} catch (err) {
console.log(err);
}
done++;
streamResponse.enqueue(
`${Math.floor((done / total) * 100)}% [${done + 1}/${total}] ${movie.id}`,
`${Math.floor((done / total) * 100)}% [${done + 1
}/${total}] ${movie.name}`,
);
})).catch((err) => {
console.log(err);

View File

@@ -4,8 +4,8 @@ import * as tmdb from "@lib/tmdb.ts";
import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts";
import { isString, safeFileName } from "@lib/string.ts";
import { AccessDeniedError } from "@lib/errors.ts";
import { Series } from "@lib/resource/series.ts";
import { createResource, fetchResource } from "@lib/marka.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers = {
async GET(_, ctx) {
@@ -49,7 +49,7 @@ export const handler: Handlers = {
);
}
const series: Series = {
const series: ReviewResource["content"] = {
_type: "Review",
image: `resources/${finalPath}`,
datePublished: releaseDate,

View File

@@ -9,7 +9,7 @@ import {
BadRequestError,
NotFoundError,
} from "@lib/errors.ts";
import { createResource, fetchResource } from "@lib/marka.ts";
import { createResource, fetchResource } from "@lib/marka/index.ts";
const isString = (input: string | undefined): input is string => {
return typeof input === "string";

View File

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

View File

@@ -1,18 +1,18 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { Article } from "@lib/resource/articles.ts";
import { KMenu } from "@islands/KMenu.tsx";
import { YoutubePlayer } from "@components/Youtube.tsx";
import { HashTags } from "@components/HashTags.tsx";
import { isYoutubeLink } from "@lib/string.ts";
import { removeImage, renderMarkdown } from "@lib/documents.ts";
import { removeImage, renderMarkdown } from "@lib/markdown.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { fetchResource } from "@lib/marka.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { ArticleResource } from "@lib/marka/schema.ts";
export const handler: Handlers<{ article: Article; session: unknown }> = {
export const handler: Handlers<{ article: ArticleResource; session: unknown }> = {
async GET(_, ctx) {
const article = await fetchResource(`articles/${ctx.params.name}.md`);
if (!article) {
@@ -30,11 +30,9 @@ export default function Greet(
const { author = "", date = "", articleBody = "" } = article?.content || {};
const content = renderMarkdown(
removeImage(articleBody, article.content.image),
removeImage(articleBody, article.image?.url),
);
console.log({ article });
return (
<MainLayout
url={props.url}
@@ -46,8 +44,8 @@ export default function Greet(
<MetaTags resource={article} />
<PageHero
image={article.content.image}
thumbnail={article.content.thumbnail}
image={article.image?.url}
thumbhash={article.image?.thumbhash}
>
<PageHero.Header>
<PageHero.BackLink href="/articles" />

View File

@@ -1,6 +1,6 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { Article } from "@lib/resource/articles.ts";
import { type ArticleResource } from "@lib/marka/schema.ts";
import { KMenu } from "@islands/KMenu.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
@@ -9,13 +9,13 @@ import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource } from "@lib/types.ts";
import { ResourceCard } from "@components/Card.tsx";
import { Link } from "@islands/Link.tsx";
import { fetchResource } from "@lib/marka.ts";
import { listResources } from "@lib/marka/index.ts";
export const handler: Handlers<
{ articles: Article[] | null; searchResults?: GenericResource[] }
{ articles: ArticleResource[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
const { content: articles } = await fetchResource("articles");
const articles = await listResources("articles");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["article"] });
@@ -25,7 +25,7 @@ export const handler: Handlers<
export default function Greet(
props: PageProps<
{ articles: Article[] | null; searchResults: GenericResource[] }
{ articles: ArticleResource[] | null; searchResults: GenericResource[] }
>,
) {
const { articles, searchResults } = props.data;

View File

@@ -1,7 +1,7 @@
import { PageProps, RouteContext } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { Movie } from "@lib/resource/movies.ts";
import { removeImage, renderMarkdown } from "@lib/documents.ts";
import { ReviewResource, ReviewSchema } from "@lib/marka/schema.ts";
import { removeImage, renderMarkdown } from "@lib/markdown.ts";
import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { Recommendations } from "@islands/Recommendations.tsx";
@@ -9,20 +9,22 @@ import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts";
import { fetchResource } from "@lib/marka/index.ts";
export default async function Greet(
props: PageProps<{ movie: Movie; session: Record<string, string> }>,
props: PageProps<{ movie: ReviewResource; session: Record<string, string> }>,
ctx: RouteContext,
) {
const movie = await fetchResource(`movies/${ctx.params.name}.md`);
const movie = await fetchResource<ReviewResource>(
`movies/${ctx.params.name}.md`,
);
const session = ctx.state.session;
if (!movie) {
return ctx.renderNotFound();
}
const { author = "", date = "" } = movie.content;
const { author = "", datePublished = "" } = movie.content;
const content = renderMarkdown(
removeImage(movie.content.reviewBody || "", movie.content.image),
@@ -34,8 +36,8 @@ export default async function Greet(
<KMenu type="main" context={movie} />
<MetaTags resource={movie} />
<PageHero
image={movie.content.image}
thumbnail={movie.content.thumbnail}
image={movie.image?.url}
thumbhash={movie.image?.thumbhash}
>
<PageHero.Header>
<PageHero.BackLink href="/movies" />
@@ -60,7 +62,7 @@ export default async function Greet(
>
{movie.content.reviewRating && (
<Star
rating={parseRating(movie.content.reviewRating?.ratingValue)}
rating={parseRating(movie.content?.reviewRating?.ratingValue)}
/>
)}
</PageHero.Subline>

View File

@@ -1,5 +1,5 @@
import { MainLayout } from "@components/layouts/main.tsx";
import { Movie } from "@lib/resource/movies.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
import { ResourceCard } from "@components/Card.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
@@ -7,15 +7,15 @@ import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { GenericResource } from "@lib/types.ts";
import { PageProps } from "$fresh/server.ts";
import { fetchResource } from "@lib/marka.ts";
import { listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
export default async function Greet(
export default async function MovieIndex(
props: PageProps<
{ movies: Movie[] | null; searchResults: GenericResource[] }
{ movies: ReviewResource[] | null; searchResults: GenericResource[] }
>,
) {
const { content: allMovies } = await fetchResource("movies");
const allMovies = await listResources("movies");
const searchParams = parseResourceUrl(props.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["movie"] });
@@ -47,8 +47,8 @@ export default async function Greet(
<h3 class="text-2xl text-white font-light">🍿 Movies</h3>
</header>
<Grid>
{movies?.map((doc) => {
return <ResourceCard res={doc} />;
{movies?.map((doc, i) => {
return <ResourceCard key={i} res={doc} />;
})}
</Grid>
</MainLayout>

View File

@@ -8,10 +8,10 @@ import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
import { renderMarkdown } from "@lib/documents.ts";
import { renderMarkdown } from "@lib/markdown.ts";
import { isValidRecipe } from "@lib/recipeSchema.ts";
import { MetaTags } from "@components/MetaTags.tsx";
import { fetchResource } from "@lib/marka.ts";
import { fetchResource } from "@lib/marka/index.ts";
export const handler: Handlers<{ recipe: Recipe; session: unknown } | null> = {
async GET(_, ctx) {
@@ -86,8 +86,8 @@ export default function Page(
<MetaTags resource={recipe} />
<PageHero
image={recipe.content?.image}
thumbnail={recipe.content?.thumbnail}
image={recipe.image?.url}
thumbhash={recipe.image?.thumbhash}
>
<PageHero.Header>
<PageHero.BackLink href="/recipes" />

View File

@@ -7,14 +7,14 @@ import { KMenu } from "@islands/KMenu.tsx";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { GenericResource } from "@lib/types.ts";
import { ResourceCard } from "@components/Card.tsx";
import { fetchResource } from "@lib/marka.ts";
import { fetchResource, listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
export const handler: Handlers<
{ recipes: Recipe[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
const { content: recipes } = await fetchResource("recipes");
const recipes = await listResources("recipes");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["recipe"] });

View File

@@ -1,17 +1,17 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { HashTags } from "@components/HashTags.tsx";
import { removeImage, renderMarkdown } from "@lib/documents.ts";
import { Series } from "@lib/resource/series.ts";
import { removeImage, renderMarkdown } from "@lib/markdown.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import PageHero from "@components/PageHero.tsx";
import { Star } from "@components/Stars.tsx";
import { MetaTags } from "@components/MetaTags.tsx";
import { parseRating } from "@lib/helpers.ts";
import { fetchResource } from "@lib/marka.ts";
import { fetchResource } from "@lib/marka/index.ts";
import { ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers<{ serie: Series; session: unknown }> = {
export const handler: Handlers<{ serie: ReviewResource; session: unknown }> = {
async GET(_, ctx) {
const serie = await fetchResource(`series/${ctx.params.name}.md`);
@@ -44,8 +44,8 @@ export default function Greet(
<MetaTags resource={serie} />
<PageHero
image={serie.content?.image}
thumbnail={serie.content?.thumbnail}
image={serie.image?.url}
thumbhash={serie.image?.thumbhash}
>
<PageHero.Header>
<PageHero.BackLink href="/series" />

View File

@@ -2,19 +2,18 @@ import { Handlers, PageProps } from "$fresh/server.ts";
import { MainLayout } from "@components/layouts/main.tsx";
import { Grid } from "@components/Grid.tsx";
import { IconArrowLeft } from "@components/icons.tsx";
import { Series } from "@lib/resource/series.ts";
import { RedirectSearchHandler } from "@islands/Search.tsx";
import { KMenu } from "@islands/KMenu.tsx";
import { ResourceCard } from "@components/Card.tsx";
import { GenericResource } from "@lib/types.ts";
import { fetchResource } from "@lib/marka.ts";
import { listResources } from "@lib/marka/index.ts";
import { parseResourceUrl, searchResource } from "@lib/search.ts";
import { GenericResource, ReviewResource } from "@lib/marka/schema.ts";
export const handler: Handlers<
{ series: Series[] | null; searchResults?: GenericResource[] }
{ series: ReviewResource[] | null; searchResults?: GenericResource[] }
> = {
async GET(req, ctx) {
const { content: series } = await fetchResource("series");
const series = await listResources("series");
const searchParams = parseResourceUrl(req.url);
const searchResults = searchParams &&
await searchResource({ ...searchParams, types: ["series"] });
@@ -24,7 +23,7 @@ export const handler: Handlers<
export default function Greet(
props: PageProps<
{ series: Series[] | null; searchResults: GenericResource[] }
{ series: ReviewResource[] | null; searchResults: GenericResource[] }
>,
) {
const { series, searchResults } = props.data;
@@ -50,8 +49,8 @@ export default function Greet(
<h3 class="text-2xl text-white font-light">🎥 Series</h3>
</header>
<Grid>
{series?.map((doc) => {
return <ResourceCard sublink="series" res={doc} />;
{series?.map((doc, i) => {
return <ResourceCard key={i} sublink="series" res={doc} />;
})}
</Grid>
</MainLayout>