From c5cf629482db241dbe6cda6af847165cf6cd7044 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Tue, 1 Aug 2023 17:50:00 +0200 Subject: [PATCH] refactor: remove some duplicated code --- components/Card.tsx | 2 - components/MovieCard.tsx | 2 +- components/RecipeCard.tsx | 2 +- components/RecipeHero.tsx | 2 - deno.json | 1 + fresh.gen.ts | 60 ++++++------ islands/KMenu/commands.ts | 5 +- lib/crud.ts | 58 ++++++++++++ lib/documents.ts | 14 +-- lib/env.ts | 4 + lib/errors.ts | 25 +++++ lib/helpers.ts | 7 ++ lib/{ => resource}/movies.ts | 18 ++-- lib/{ => resource}/recipes.ts | 18 +++- lib/string.ts | 13 +++ routes/_middleware.ts | 25 +++++ routes/api/images/index.ts | 11 ++- routes/api/index.ts | 15 ++- routes/api/movies/[name].ts | 142 ++-------------------------- routes/api/movies/enhance/[name].ts | 103 ++++++++++++++++++++ routes/api/movies/index.ts | 40 ++------ routes/api/recipes/[name].ts | 29 ++---- routes/api/recipes/index.ts | 40 ++------ routes/api/tmdb/[id].ts | 16 ++-- routes/api/tmdb/credits/[id].ts | 8 +- routes/api/tmdb/query.ts | 19 ++-- routes/movies/[name].tsx | 3 +- routes/movies/index.tsx | 8 +- routes/recipes/[name].tsx | 3 +- routes/recipes/index.tsx | 5 +- 30 files changed, 377 insertions(+), 321 deletions(-) create mode 100644 lib/crud.ts create mode 100644 lib/env.ts create mode 100644 lib/errors.ts create mode 100644 lib/helpers.ts rename lib/{ => resource}/movies.ts (80%) rename lib/{ => resource}/recipes.ts (89%) create mode 100644 routes/_middleware.ts create mode 100644 routes/api/movies/enhance/[name].ts diff --git a/components/Card.tsx b/components/Card.tsx index 1ae3da3..c99f063 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -1,5 +1,3 @@ -import { Recipe } from "../lib/recipes.ts"; - export function Card( { link, title, image }: { link?: string; title?: string; image?: string }, ) { diff --git a/components/MovieCard.tsx b/components/MovieCard.tsx index 555d53a..559d103 100644 --- a/components/MovieCard.tsx +++ b/components/MovieCard.tsx @@ -1,5 +1,5 @@ import { Card } from "@components/Card.tsx"; -import { Movie } from "@lib/movies.ts"; +import { Movie } from "@lib/resource/movies.ts"; export function MovieCard({ movie }: { movie: Movie }) { const { meta: { image = "/placeholder.svg" } = {} } = movie; diff --git a/components/RecipeCard.tsx b/components/RecipeCard.tsx index 3d9a837..be918fc 100644 --- a/components/RecipeCard.tsx +++ b/components/RecipeCard.tsx @@ -1,5 +1,5 @@ import { Card } from "@components/Card.tsx"; -import { Recipe } from "@lib/recipes.ts"; +import { Recipe } from "@lib/resource/recipes.ts"; export function RecipeCard({ recipe }: { recipe: Recipe }) { const { meta: { image = "/placeholder.svg" } = {} } = recipe; diff --git a/components/RecipeHero.tsx b/components/RecipeHero.tsx index 0129e48..da73fce 100644 --- a/components/RecipeHero.tsx +++ b/components/RecipeHero.tsx @@ -1,5 +1,3 @@ -import { Recipe } from "@lib/recipes.ts"; - import IconExternalLink from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/external-link.tsx"; import { Star } from "@components/Stars.tsx"; export function RecipeHero( diff --git a/deno.json b/deno.json index ea81c4f..3d3c7e9 100755 --- a/deno.json +++ b/deno.json @@ -15,6 +15,7 @@ }, "imports": { "$fresh/": "https://deno.land/x/fresh@1.3.1/", + "yaml": "https://deno.land/std@0.196.0/yaml/parse.ts", "preact": "https://esm.sh/preact@10.15.1", "preact/": "https://esm.sh/preact@10.15.1/", "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0", diff --git a/fresh.gen.ts b/fresh.gen.ts index 6f5edbf..09f4fec 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -4,20 +4,22 @@ import * as $0 from "./routes/_404.tsx"; import * as $1 from "./routes/_app.tsx"; -import * as $2 from "./routes/api/images/index.ts"; -import * as $3 from "./routes/api/index.ts"; -import * as $4 from "./routes/api/movies/[name].ts"; -import * as $5 from "./routes/api/movies/index.ts"; -import * as $6 from "./routes/api/recipes/[name].ts"; -import * as $7 from "./routes/api/recipes/index.ts"; -import * as $8 from "./routes/api/tmdb/[id].ts"; -import * as $9 from "./routes/api/tmdb/credits/[id].ts"; -import * as $10 from "./routes/api/tmdb/query.ts"; -import * as $11 from "./routes/index.tsx"; -import * as $12 from "./routes/movies/[name].tsx"; -import * as $13 from "./routes/movies/index.tsx"; -import * as $14 from "./routes/recipes/[name].tsx"; -import * as $15 from "./routes/recipes/index.tsx"; +import * as $2 from "./routes/_middleware.ts"; +import * as $3 from "./routes/api/images/index.ts"; +import * as $4 from "./routes/api/index.ts"; +import * as $5 from "./routes/api/movies/[name].ts"; +import * as $6 from "./routes/api/movies/enhance/[name].ts"; +import * as $7 from "./routes/api/movies/index.ts"; +import * as $8 from "./routes/api/recipes/[name].ts"; +import * as $9 from "./routes/api/recipes/index.ts"; +import * as $10 from "./routes/api/tmdb/[id].ts"; +import * as $11 from "./routes/api/tmdb/credits/[id].ts"; +import * as $12 from "./routes/api/tmdb/query.ts"; +import * as $13 from "./routes/index.tsx"; +import * as $14 from "./routes/movies/[name].tsx"; +import * as $15 from "./routes/movies/index.tsx"; +import * as $16 from "./routes/recipes/[name].tsx"; +import * as $17 from "./routes/recipes/index.tsx"; import * as $$0 from "./islands/Counter.tsx"; import * as $$1 from "./islands/IngredientsList.tsx"; import * as $$2 from "./islands/KMenu.tsx"; @@ -28,20 +30,22 @@ const manifest = { routes: { "./routes/_404.tsx": $0, "./routes/_app.tsx": $1, - "./routes/api/images/index.ts": $2, - "./routes/api/index.ts": $3, - "./routes/api/movies/[name].ts": $4, - "./routes/api/movies/index.ts": $5, - "./routes/api/recipes/[name].ts": $6, - "./routes/api/recipes/index.ts": $7, - "./routes/api/tmdb/[id].ts": $8, - "./routes/api/tmdb/credits/[id].ts": $9, - "./routes/api/tmdb/query.ts": $10, - "./routes/index.tsx": $11, - "./routes/movies/[name].tsx": $12, - "./routes/movies/index.tsx": $13, - "./routes/recipes/[name].tsx": $14, - "./routes/recipes/index.tsx": $15, + "./routes/_middleware.ts": $2, + "./routes/api/images/index.ts": $3, + "./routes/api/index.ts": $4, + "./routes/api/movies/[name].ts": $5, + "./routes/api/movies/enhance/[name].ts": $6, + "./routes/api/movies/index.ts": $7, + "./routes/api/recipes/[name].ts": $8, + "./routes/api/recipes/index.ts": $9, + "./routes/api/tmdb/[id].ts": $10, + "./routes/api/tmdb/credits/[id].ts": $11, + "./routes/api/tmdb/query.ts": $12, + "./routes/index.tsx": $13, + "./routes/movies/[name].tsx": $14, + "./routes/movies/index.tsx": $15, + "./routes/recipes/[name].tsx": $16, + "./routes/recipes/index.tsx": $17, }, islands: { "./islands/Counter.tsx": $$0, diff --git a/islands/KMenu/commands.ts b/islands/KMenu/commands.ts index 5ceb325..54c6c08 100644 --- a/islands/KMenu/commands.ts +++ b/islands/KMenu/commands.ts @@ -1,5 +1,5 @@ import { Menu } from "@islands/KMenu/types.ts"; -import { Movie } from "@lib/movies.ts"; +import { Movie } from "@lib/resource/movies.ts"; import { TMDBMovie } from "@lib/types.ts"; export const menus: Record = { @@ -41,11 +41,10 @@ export const menus: Record = { title: `${m.title} released ${m.release_date}`, cb: async () => { state.activeState.value = "loading"; - const res = await fetch(`/api/movies/${movie.name}/`, { + await fetch(`/api/movies/${movie.name}/`, { method: "POST", body: JSON.stringify({ tmdbId: m.id }), }); - const j = await res.json(); state.visible.value = false; state.activeState.value = "normal"; window.location.reload(); diff --git a/lib/crud.ts b/lib/crud.ts new file mode 100644 index 0000000..76fa5db --- /dev/null +++ b/lib/crud.ts @@ -0,0 +1,58 @@ +import { + createDocument, + Document, + getDocument, + getDocuments, + transformDocument, +} from "@lib/documents.ts"; +import { Root } from "https://esm.sh/remark-frontmatter@4.0.1"; + +export function createCrud( + { prefix, parse }: { + prefix: string; + parse: (doc: string, id: string) => T; + }, +) { + function pathFromId(id: string) { + return `${prefix}${id}.md`; + } + + async function read(id: string) { + const path = pathFromId(id); + const content = await getDocument(path); + + return parse(content, id); + } + function create(id: string, content: string | ArrayBuffer) { + const path = pathFromId(id); + return createDocument(path, content); + } + + async function update(id: string, updater: (r: Root) => Root) { + const path = pathFromId(id); + const content = await getDocument(path); + const newDoc = transformDocument(content, updater); + await createDocument(path, newDoc); + } + + async function readAll() { + const allDocuments = await getDocuments(); + return Promise.all( + allDocuments.filter((d) => { + return d.name.startsWith(prefix) && + d.contentType === "text/markdown" && + !d.name.endsWith("index.md"); + }).map((doc) => { + const id = doc.name.replace(prefix, "").replace(/\.md$/, ""); + return read(id); + }), + ); + } + + return { + read, + readAll, + create, + update, + }; +} diff --git a/lib/documents.ts b/lib/documents.ts index 9c93906..a859dcf 100644 --- a/lib/documents.ts +++ b/lib/documents.ts @@ -1,5 +1,5 @@ -import { unified } from "npm:unified"; -import remarkParse from "npm:remark-parse"; +import { unified } from "https://esm.sh/unified"; +import remarkParse from "https://esm.sh/remark-parse"; import remarkStringify from "https://esm.sh/remark-stringify@10.0.3"; import remarkFrontmatter, { Root, @@ -7,10 +7,8 @@ import remarkFrontmatter, { import remarkRehype from "https://esm.sh/remark-rehype@10.1.0"; import rehypeSanitize from "https://esm.sh/rehype-sanitize@5.0.1"; import rehypeStringify from "https://esm.sh/rehype-stringify@9.0.3"; -import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts"; import * as cache from "@lib/cache/documents.ts"; - -const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER"); +import { SILVERBULLET_SERVER } from "@lib/env.ts"; export type Document = { name: string; @@ -20,10 +18,6 @@ export type Document = { perm: string; }; -export function parseFrontmatter(yaml: string) { - return parse(yaml); -} - export async function getDocuments(): Promise { const cachedDocuments = await cache.getDocuments(); if (cachedDocuments) return cachedDocuments; @@ -31,7 +25,7 @@ export async function getDocuments(): Promise { const headers = new Headers(); headers.append("Accept", "application/json"); - const response = await fetch(SILVERBULLET_SERVER + "/index.json", { + const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, { headers: headers, }); diff --git a/lib/env.ts b/lib/env.ts new file mode 100644 index 0000000..e01ca15 --- /dev/null +++ b/lib/env.ts @@ -0,0 +1,4 @@ +export const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER"); +export const REDIS_HOST = Deno.env.get("REDIS_HOST"); +export const REDIS_PASS = Deno.env.get("REDIS_PASS"); +export const TMDB_API_KEY = Deno.env.get("TMDB_API_KEY"); diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 0000000..93826ac --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,25 @@ +import { MiddlewareHandlerContext } from "$fresh/server.ts"; + +class DomainError extends Error { + status = 500; + render?: (ctx: MiddlewareHandlerContext) => void; + constructor(public statusText = "Internal Server Error") { + super(); + } +} + +class NotFoundError extends DomainError { + status = 404; + constructor(public statusText = "Not Found") { + super(); + } +} + +class BadRequestError extends DomainError { + status = 400; + constructor(public statusText = "Bad Request") { + super(); + } +} + +export { BadRequestError, DomainError, NotFoundError }; diff --git a/lib/helpers.ts b/lib/helpers.ts new file mode 100644 index 0000000..df709b9 --- /dev/null +++ b/lib/helpers.ts @@ -0,0 +1,7 @@ +export function json(content: unknown) { + const headers = new Headers(); + headers.append("Content-Type", "application/json"); + return new Response(JSON.stringify(content), { + headers, + }); +} diff --git a/lib/movies.ts b/lib/resource/movies.ts similarity index 80% rename from lib/movies.ts rename to lib/resource/movies.ts index 895a26f..e4006b7 100644 --- a/lib/movies.ts +++ b/lib/resource/movies.ts @@ -1,8 +1,6 @@ -import { - parseDocument, - parseFrontmatter, - renderMarkdown, -} from "@lib/documents.ts"; +import { parseDocument, renderMarkdown } from "@lib/documents.ts"; +import { parse } from "yaml"; +import { createCrud } from "@lib/crud.ts"; export type Movie = { id: string; @@ -28,7 +26,7 @@ export function parseMovie(original: string, id: string): Movie { for (const child of doc.children) { if (child.type === "yaml") { - meta = parseFrontmatter(child.value) as Movie["meta"]; + meta = parse(child.value) as Movie["meta"]; if (meta["rating"] && typeof meta["rating"] === "string") { meta.rating = [...meta.rating?.matchAll("⭐")].length; @@ -68,3 +66,11 @@ export function parseMovie(original: string, id: string): Movie { meta, }; } + +const crud = createCrud({ + prefix: "Media/movies/", + parse: parseMovie, +}); + +export const getMovie = crud.read; +export const getAllMovies = crud.readAll; diff --git a/lib/recipes.ts b/lib/resource/recipes.ts similarity index 89% rename from lib/recipes.ts rename to lib/resource/recipes.ts index bbcaa55..6767edf 100644 --- a/lib/recipes.ts +++ b/lib/resource/recipes.ts @@ -3,11 +3,11 @@ import { getTextOfChild, getTextOfRange, parseDocument, - parseFrontmatter, renderMarkdown, } from "@lib/documents.ts"; - -import { parseIngredient } from "npm:parse-ingredient"; +import { parse } from "yaml"; +import { parseIngredient } from "https://esm.sh/parse-ingredient"; +import { createCrud } from "@lib/crud.ts"; export type IngredientGroup = { name: string; @@ -132,7 +132,7 @@ export function parseRecipe(original: string, id: string): Recipe { let group: DocumentChild[] = []; for (const child of doc.children) { if (child.type === "yaml") { - meta = parseFrontmatter(child.value) as Recipe["meta"]; + meta = parse(child.value) as Recipe["meta"]; continue; } if ( @@ -169,3 +169,13 @@ export function parseRecipe(original: string, id: string): Recipe { preparation: preparation ? renderMarkdown(preparation) : "", }; } + +const crud = createCrud({ + prefix: `Recipes/`, + parse: parseRecipe, +}); + +export const getAllRecipes = crud.readAll; +export const getRecipe = crud.read; +export const updateRecipe = crud.update; +export const createRecipe = crud.create; diff --git a/lib/string.ts b/lib/string.ts index 1c94956..78a69cc 100644 --- a/lib/string.ts +++ b/lib/string.ts @@ -2,3 +2,16 @@ export function formatDate(date: Date): string { const options = { year: "numeric", month: "long", day: "numeric" } as const; return new Intl.DateTimeFormat("en-US", options).format(date); } + +export function safeFileName(inputString: string): string { + // Convert the string to lowercase + let fileName = inputString.toLowerCase(); + + // Replace spaces with underscores + fileName = fileName.replace(/ /g, "_"); + + // Remove characters that are not safe for file names + fileName = fileName.replace(/[^\w.-]/g, ""); + + return fileName; +} diff --git a/routes/_middleware.ts b/routes/_middleware.ts new file mode 100644 index 0000000..8733182 --- /dev/null +++ b/routes/_middleware.ts @@ -0,0 +1,25 @@ +//routes/middleware-error-handler/_middleware.ts +import { MiddlewareHandlerContext } from "$fresh/server.ts"; +import { DomainError } from "@lib/errors.ts"; + +export async function handler( + _req: Request, + ctx: MiddlewareHandlerContext, +) { + try { + ctx.state.flag = true; + return await ctx.next(); + } catch (error) { + console.error(error); + + if (error instanceof DomainError) { + return new Response(error.statusText, { + status: error.status, + }); + } + + return new Response("Internal Server Error", { + status: 500, + }); + } +} diff --git a/routes/api/images/index.ts b/routes/api/images/index.ts index 620af89..b6e85d4 100644 --- a/routes/api/images/index.ts +++ b/routes/api/images/index.ts @@ -1,4 +1,4 @@ -import { HandlerContext } from "$fresh/server.ts"; +import { HandlerContext, Handlers } from "$fresh/server.ts"; import { ImageMagick, initializeImageMagick, @@ -6,6 +6,7 @@ import { } from "https://deno.land/x/imagemagick_deno@0.0.14/mod.ts"; import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts"; import * as cache from "@lib/cache/image.ts"; +import { SILVERBULLET_SERVER } from "@lib/env.ts"; await initializeImageMagick(); @@ -81,7 +82,7 @@ function parseParams(reqUrl: URL) { }; } -export const handler = async ( +const GET = async ( _req: Request, _ctx: HandlerContext, ): Promise => { @@ -92,7 +93,7 @@ export const handler = async ( return new Response(params, { status: 400 }); } - const imageUrl = Deno.env.get("SILVERBULLET_SERVER") + "/" + params.image; + const imageUrl = SILVERBULLET_SERVER + "/" + params.image; console.log("[api/image] " + imageUrl); @@ -138,3 +139,7 @@ export const handler = async ( }, }); }; + +export const handler: Handlers = { + GET, +}; diff --git a/routes/api/index.ts b/routes/api/index.ts index 79877f3..e205871 100644 --- a/routes/api/index.ts +++ b/routes/api/index.ts @@ -1,11 +1,10 @@ -import { HandlerContext } from "$fresh/server.ts"; +import { Handlers } from "$fresh/server.ts"; import { getDocuments } from "@lib/documents.ts"; +import { json } from "@lib/helpers.ts"; -export const handler = async ( - _req: Request, - _ctx: HandlerContext, -): Promise => { - const documents = await getDocuments(); - const response = new Response(JSON.stringify(documents)); - return response; +export const handler: Handlers = { + async GET() { + const documents = await getDocuments(); + return json(documents); + }, }; diff --git a/routes/api/movies/[name].ts b/routes/api/movies/[name].ts index 683ae36..00456ad 100644 --- a/routes/api/movies/[name].ts +++ b/routes/api/movies/[name].ts @@ -1,136 +1,10 @@ -import { HandlerContext } from "$fresh/server.ts"; -import { - createDocument, - getDocument, - transformDocument, -} from "@lib/documents.ts"; -import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; -import { type Movie, parseMovie } from "@lib/movies.ts"; -import * as tmdb from "@lib/tmdb.ts"; -import { parse, stringify } from "https://deno.land/std@0.194.0/yaml/mod.ts"; -import { formatDate } from "@lib/string.ts"; +import { Handlers } from "$fresh/server.ts"; +import { getMovie } from "@lib/resource/movies.ts"; +import { json } from "@lib/helpers.ts"; -function safeFileName(inputString: string): string { - // Convert the string to lowercase - let fileName = inputString.toLowerCase(); - - // Replace spaces with underscores - fileName = fileName.replace(/ /g, "_"); - - // Remove characters that are not safe for file names - fileName = fileName.replace(/[^\w.-]/g, ""); - - return fileName; -} - -export async function getMovie(name: string) { - const document = await getDocument(`Media/movies/${name}.md`); - - const movie = parseMovie(document, name); - - return movie; -} - -async function updateMovieMetadata( - name: string, - metadata: Partial, -) { - const docId = `Media/movies/${name}.md`; - - const currentDoc = await getDocument(docId); - if (!currentDoc) return; - - const newDoc = transformDocument(currentDoc, (root) => { - const frontmatterNode = root.children.find((c) => c.type === "yaml"); - - const frontmatter = frontmatterNode?.value as string; - - if (frontmatter) { - const value = parse(frontmatter) as Movie["meta"]; - - if (metadata.author && !value.author) { - value.author = metadata.author; - } - - if (metadata.image && !value.image) { - value.image = metadata.image; - } - - if (metadata.date && !value.date) { - value.date = formatDate(metadata.date); - } - frontmatterNode.value = stringify(value); - } - - return root; - }); - - const response = await createDocument(docId, newDoc); - - return response; -} - -export const handler = async ( - _req: Request, - _ctx: HandlerContext, -): Promise => { - const headers = new Headers(); - headers.append("Content-Type", "application/json"); - - const movie = await getMovie(_ctx.params.name); - - if (_req.method === "GET") { - return new Response(JSON.stringify(movie)); - } - - if (_req.method === "POST") { - const body = await _req.json(); - const name = _ctx.params.name; - const { tmdbId } = body; - if (!name || !tmdbId) { - return new Response("Bad Request", { - status: 400, - }); - } - - const movieDetails = await tmdb.getMovie(tmdbId); - const movieCredits = !movie.meta.author && - await tmdb.getMovieCredits(tmdbId); - - const releaseDate = movieDetails.release_date; - const posterPath = movieDetails.poster_path; - const director = movieCredits?.crew?.filter?.((person) => - person.job === "Director" - )[0]; - - let finalPath = ""; - if (posterPath && !movie.meta.image) { - const poster = await tmdb.getMoviePoster(posterPath); - const extension = fileExtension(posterPath); - - finalPath = `Media/movies/images/${ - safeFileName(name) - }_cover.${extension}`; - await createDocument(finalPath, poster); - } - - const metadata = {} as Movie["meta"]; - if (releaseDate) { - metadata.date = new Date(releaseDate); - } - if (finalPath) { - metadata.image = finalPath; - } - if (director) { - metadata.author = director.name; - } - - await updateMovieMetadata(name, metadata); - - return new Response(JSON.stringify(movieCredits), { - headers, - }); - } - - return new Response(); +export const handler: Handlers = { + async GET(_, ctx) { + const movie = await getMovie(ctx.params.name); + return json(movie); + }, }; diff --git a/routes/api/movies/enhance/[name].ts b/routes/api/movies/enhance/[name].ts new file mode 100644 index 0000000..51225e3 --- /dev/null +++ b/routes/api/movies/enhance/[name].ts @@ -0,0 +1,103 @@ +import { HandlerContext, Handlers } from "$fresh/server.ts"; +import { + createDocument, + getDocument, + transformDocument, +} from "@lib/documents.ts"; +import { fileExtension } from "https://deno.land/x/file_extension@v2.1.0/mod.ts"; +import { getMovie, type Movie } from "@lib/resource/movies.ts"; +import * as tmdb from "@lib/tmdb.ts"; +import { parse, stringify } from "https://deno.land/std@0.194.0/yaml/mod.ts"; +import { formatDate, safeFileName } from "@lib/string.ts"; +import { json } from "@lib/helpers.ts"; +import { BadRequestError } from "@lib/errors.ts"; + +async function updateMovieMetadata( + name: string, + metadata: Partial, +) { + const docId = `Media/movies/${name}.md`; + + const currentDoc = await getDocument(docId); + if (!currentDoc) return; + + const newDoc = transformDocument(currentDoc, (root) => { + const frontmatterNode = root.children.find((c) => c.type === "yaml"); + + const frontmatter = frontmatterNode?.value as string; + + if (frontmatter) { + const value = parse(frontmatter) as Movie["meta"]; + + if (metadata.author && !value.author) { + value.author = metadata.author; + } + + if (metadata.image && !value.image) { + value.image = metadata.image; + } + + if (metadata.date && !value.date) { + value.date = formatDate(metadata.date); + } + frontmatterNode.value = stringify(value); + } + + return root; + }); + + const response = await createDocument(docId, newDoc); + + return response; +} + +const GET = async ( + _req: Request, + _ctx: HandlerContext, +): Promise => { + const movie = await getMovie(_ctx.params.name); + + const body = await _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.meta.author && + await tmdb.getMovieCredits(tmdbId); + + const releaseDate = movieDetails.release_date; + const posterPath = movieDetails.poster_path; + const director = + movieCredits?.crew?.filter?.((person) => person.job === "Director")[0]; + + let finalPath = ""; + if (posterPath && !movie.meta.image) { + const poster = await tmdb.getMoviePoster(posterPath); + const extension = fileExtension(posterPath); + + finalPath = `Media/movies/images/${safeFileName(name)}_cover.${extension}`; + await createDocument(finalPath, poster); + } + + const metadata = {} as Movie["meta"]; + if (releaseDate) { + metadata.date = new Date(releaseDate); + } + if (finalPath) { + metadata.image = finalPath; + } + if (director) { + metadata.author = director.name; + } + + await updateMovieMetadata(name, metadata); + + return json(movieCredits); +}; + +export const handler: Handlers = { + GET, +}; diff --git a/routes/api/movies/index.ts b/routes/api/movies/index.ts index d9adb65..868b2c5 100644 --- a/routes/api/movies/index.ts +++ b/routes/api/movies/index.ts @@ -1,34 +1,10 @@ -import { HandlerContext } from "$fresh/server.ts"; -import { getDocument, getDocuments } from "@lib/documents.ts"; -import { parseMovie } from "@lib/movies.ts"; +import { Handlers } from "$fresh/server.ts"; +import { getAllMovies } from "@lib/resource/movies.ts"; +import { json } from "@lib/helpers.ts"; -export async function getMovies() { - const documents = await getDocuments(); - - return Promise.all( - documents.filter((d) => { - return d.name.startsWith("Media/movies/") && - d.contentType === "text/markdown" && - !d.name.endsWith("index.md"); - }).map(async (doc) => { - const document = await getDocument(doc.name); - const movie = parseMovie(document, doc.name); - return { - ...movie, - id: movie.id.replace(/\.md$/, "").replace(/^Media\/movies\//, ""), - }; - }), - ); -} - -export const handler = async ( - _req: Request, - _ctx: HandlerContext, -): Promise => { - const headers = new Headers(); - headers.append("Content-Type", "application/json"); - - const movies = await getMovies(); - - return new Response(JSON.stringify(movies), { headers }); +export const handler: Handlers = { + async GET() { + const movies = await getAllMovies(); + return json(movies); + }, }; diff --git a/routes/api/recipes/[name].ts b/routes/api/recipes/[name].ts index 74f8b1c..2c92669 100644 --- a/routes/api/recipes/[name].ts +++ b/routes/api/recipes/[name].ts @@ -1,23 +1,10 @@ -import { HandlerContext } from "$fresh/server.ts"; -import { getDocument } from "@lib/documents.ts"; -import { parseRecipe } from "@lib/recipes.ts"; +import { Handlers } from "$fresh/server.ts"; +import { getRecipe } from "@lib/resource/recipes.ts"; +import { json } from "@lib/helpers.ts"; -export async function getRecipe(name: string) { - const document = await getDocument(`Recipes/${name}.md`); - - const recipe = parseRecipe(document, name); - - return recipe; -} - -export const handler = async ( - _req: Request, - _ctx: HandlerContext, -): Promise => { - const recipe = await getRecipe(_ctx.params.name); - - const headers = new Headers(); - headers.append("Content-Type", "application/json"); - - return new Response(JSON.stringify(recipe)); +export const handler: Handlers = { + async GET(_, ctx) { + const recipe = await getRecipe(ctx.params.name); + return json(recipe); + }, }; diff --git a/routes/api/recipes/index.ts b/routes/api/recipes/index.ts index 548c092..c93b8a1 100644 --- a/routes/api/recipes/index.ts +++ b/routes/api/recipes/index.ts @@ -1,34 +1,10 @@ -import { HandlerContext } from "$fresh/server.ts"; -import { getDocument, getDocuments } from "@lib/documents.ts"; -import { parseRecipe } from "@lib/recipes.ts"; +import { Handlers } from "$fresh/server.ts"; +import { getAllRecipes } from "@lib/resource/recipes.ts"; +import { json } from "@lib/helpers.ts"; -export async function getRecipes() { - const documents = await getDocuments(); - - return Promise.all( - documents.filter((d) => { - return d.name.startsWith("Recipes/") && - d.contentType === "text/markdown" && - !d.name.endsWith("index.md"); - }).map(async (doc) => { - const document = await getDocument(doc.name); - const recipe = parseRecipe(document, doc.name); - return { - ...recipe, - id: recipe.id.replace(/^Recipes\//, "").replace(/\.md$/, ""), - }; - }), - ); -} - -export const handler = async ( - _req: Request, - _ctx: HandlerContext, -): Promise => { - const headers = new Headers(); - headers.append("Content-Type", "application/json"); - - const recipes = await getRecipes(); - - return new Response(JSON.stringify(recipes), { headers }); +export const handler: Handlers = { + async GET() { + const recipes = await getAllRecipes(); + return json(recipes); + }, }; diff --git a/routes/api/tmdb/[id].ts b/routes/api/tmdb/[id].ts index 1b9473e..b32a1ed 100644 --- a/routes/api/tmdb/[id].ts +++ b/routes/api/tmdb/[id].ts @@ -1,6 +1,7 @@ -import { HandlerContext } from "$fresh/server.ts"; +import { HandlerContext, Handlers } from "$fresh/server.ts"; import { getMovie } from "@lib/tmdb.ts"; import * as cache from "@lib/cache/cache.ts"; +import { json } from "@lib/helpers.ts"; type CachedMovieCredits = { lastUpdated: number; @@ -9,7 +10,7 @@ type CachedMovieCredits = { const CACHE_INTERVAL = 1000 * 60 * 24 * 30; -export const handler = async ( +const GET = async ( _req: Request, _ctx: HandlerContext, ) => { @@ -21,16 +22,13 @@ export const handler = async ( }); } - const headers = new Headers(); - headers.append("Content-Type", "application/json"); - const cacheId = `/movie/${id}`; const cachedResponse = await cache.get(cacheId); if ( cachedResponse && Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL) ) { - return new Response(JSON.stringify(cachedResponse.data), { headers }); + return json(cachedResponse.data); } const res = await getMovie(+id); @@ -43,5 +41,9 @@ export const handler = async ( }), ); - return new Response(JSON.stringify(res)); + return json(res); +}; + +export const handler: Handlers = { + GET, }; diff --git a/routes/api/tmdb/credits/[id].ts b/routes/api/tmdb/credits/[id].ts index d1920e5..b1cd795 100644 --- a/routes/api/tmdb/credits/[id].ts +++ b/routes/api/tmdb/credits/[id].ts @@ -1,6 +1,7 @@ import { HandlerContext } from "$fresh/server.ts"; import { getMovieCredits } from "@lib/tmdb.ts"; import * as cache from "@lib/cache/cache.ts"; +import { json } from "@lib/helpers.ts"; type CachedMovieCredits = { lastUpdated: number; @@ -21,9 +22,6 @@ export const handler = async ( }); } - const headers = new Headers(); - headers.append("Content-Type", "application/json"); - console.log("[api] getting movie credits"); const cacheId = `/movie/credits/${id}`; @@ -32,7 +30,7 @@ export const handler = async ( if ( cachedResponse && Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL) ) { - return new Response(JSON.stringify(cachedResponse.data), { headers }); + return json(cachedResponse.data); } const res = await getMovieCredits(+id); @@ -44,5 +42,5 @@ export const handler = async ( }), ); - return new Response(JSON.stringify(res)); + return json(res); }; diff --git a/routes/api/tmdb/query.ts b/routes/api/tmdb/query.ts index 7027405..aa80b9c 100644 --- a/routes/api/tmdb/query.ts +++ b/routes/api/tmdb/query.ts @@ -1,6 +1,8 @@ -import { HandlerContext } from "$fresh/server.ts"; +import { HandlerContext, Handlers } from "$fresh/server.ts"; import { searchMovie } from "@lib/tmdb.ts"; import * as cache from "@lib/cache/cache.ts"; +import { BadRequestError } from "@lib/errors.ts"; +import { json } from "@lib/helpers.ts"; type CachedMovieQuery = { lastUpdated: number; @@ -9,7 +11,7 @@ type CachedMovieQuery = { const CACHE_INTERVAL = 1000 * 60 * 24 * 30; -export const handler = async ( +const GET = async ( _req: Request, _ctx: HandlerContext, ) => { @@ -18,21 +20,16 @@ export const handler = async ( const query = u.searchParams.get("q"); if (!query) { - return new Response("Bad Request", { - status: 400, - }); + throw new BadRequestError(); } - const headers = new Headers(); - headers.append("Content-Type", "application/json"); - const cacheId = `/movie/query/${query}`; const cachedResponse = await cache.get(cacheId); if ( cachedResponse && Date.now() < (cachedResponse.lastUpdated + CACHE_INTERVAL) ) { - return new Response(JSON.stringify(cachedResponse.data), { headers }); + return json(cachedResponse.data); } const res = await searchMovie(query); @@ -47,3 +44,7 @@ export const handler = async ( return new Response(JSON.stringify(res.results)); }; + +export const handler: Handlers = { + GET, +}; diff --git a/routes/movies/[name].tsx b/routes/movies/[name].tsx index be82885..0e144f6 100644 --- a/routes/movies/[name].tsx +++ b/routes/movies/[name].tsx @@ -1,7 +1,6 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; -import { Movie } from "@lib/movies.ts"; -import { getMovie } from "../api/movies/[name].ts"; +import { getMovie, Movie } from "@lib/resource/movies.ts"; import { RecipeHero } from "@components/RecipeHero.tsx"; import { KMenu } from "@islands/KMenu.tsx"; diff --git a/routes/movies/index.tsx b/routes/movies/index.tsx index 1ff466f..67d914b 100644 --- a/routes/movies/index.tsx +++ b/routes/movies/index.tsx @@ -1,16 +1,12 @@ import { Handlers, PageProps } from "$fresh/server.ts"; -import { RecipeCard } from "@components/RecipeCard.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; -import { Recipe } from "@lib/recipes.ts"; -import { getRecipes } from "../api/recipes/index.ts"; import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx"; -import { getMovies } from "../api/movies/index.ts"; -import { Movie } from "@lib/movies.ts"; +import { getAllMovies, Movie } from "@lib/resource/movies.ts"; import { MovieCard } from "@components/MovieCard.tsx"; export const handler: Handlers = { async GET(_, ctx) { - const movies = await getMovies(); + const movies = await getAllMovies(); return ctx.render(movies); }, }; diff --git a/routes/recipes/[name].tsx b/routes/recipes/[name].tsx index 265a581..ffc3d78 100644 --- a/routes/recipes/[name].tsx +++ b/routes/recipes/[name].tsx @@ -2,10 +2,9 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { IngredientsList } from "@islands/IngredientsList.tsx"; import { RecipeHero } from "@components/RecipeHero.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; -import { Recipe } from "@lib/recipes.ts"; -import { getRecipe } from "../api/recipes/[name].ts"; import Counter from "@islands/Counter.tsx"; import { useSignal } from "@preact/signals"; +import { getRecipe, Recipe } from "@lib/resource/recipes.ts"; export const handler: Handlers = { async GET(_, ctx) { diff --git a/routes/recipes/index.tsx b/routes/recipes/index.tsx index c648dcd..f5651e4 100644 --- a/routes/recipes/index.tsx +++ b/routes/recipes/index.tsx @@ -1,13 +1,12 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { RecipeCard } from "@components/RecipeCard.tsx"; import { MainLayout } from "@components/layouts/main.tsx"; -import { Recipe } from "@lib/recipes.ts"; -import { getRecipes } from "../api/recipes/index.ts"; +import { getAllRecipes, Recipe } from "@lib/resource/recipes.ts"; import IconArrowLeft from "https://deno.land/x/tabler_icons_tsx@0.0.3/tsx/arrow-left.tsx"; export const handler: Handlers = { async GET(_, ctx) { - const recipes = await getRecipes(); + const recipes = await getAllRecipes(); return ctx.render(recipes); }, };