From cc112b755487c22dae9b57dcaea278f224bceae7 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Fri, 8 Sep 2023 13:33:29 +0200 Subject: [PATCH] feat: add initial recommendation data --- components/layouts/main.tsx | 56 +++-------- deno.json | 9 +- dev.ts | 3 +- fresh.config.ts | 6 ++ fresh.gen.ts | 146 ++++++++++++++-------------- islands/Search.tsx | 2 +- lib/openai.ts | 24 ++++- lib/recommendation.ts | 53 ++++++++++ lib/resource/movies.ts | 19 +++- lib/resource/series.ts | 22 +++-- lib/search.ts | 5 +- lib/types.ts | 1 + main.ts | 5 +- routes/_layout.tsx | 41 ++++++++ routes/api/movies/[name].ts | 11 ++- routes/api/movies/enhance/[name].ts | 12 ++- routes/api/series/[name].ts | 2 +- routes/movies/[name].tsx | 15 +-- routes/movies/index.tsx | 27 ++--- 19 files changed, 289 insertions(+), 170 deletions(-) create mode 100644 fresh.config.ts create mode 100644 lib/recommendation.ts create mode 100644 routes/_layout.tsx diff --git a/components/layouts/main.tsx b/components/layouts/main.tsx index 75cf5e0..7e843f5 100644 --- a/components/layouts/main.tsx +++ b/components/layouts/main.tsx @@ -20,48 +20,18 @@ export type Props = { export const MainLayout = ( { children, url, title, context, searchResults }: Props, ) => { - const hasSearch = url.search.includes("q="); + const _url = typeof url === "string" ? new URL(url) : url; + const hasSearch = _url.search.includes("q="); - return ( -
- - - - {title && - {title}} - - -
- {hasSearch && ( - - )} - {!hasSearch && children} -
-
- ); + if (hasSearch) { + return ( + + ); + } + + return <>{children}; }; diff --git a/deno.json b/deno.json index d5907e8..159e720 100644 --- a/deno.json +++ b/deno.json @@ -1,10 +1,11 @@ { "lock": false, "tasks": { + "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", "start": "deno run -A --watch=static/,routes/ dev.ts", - "update": "deno run -A -r https://fresh.deno.dev/update .", "build": "deno run -A dev.ts build", - "preview": "deno run -A main.ts" + "preview": "deno run -A main.ts", + "update": "deno run -A -r https://fresh.deno.dev/update ." }, "lint": { "rules": { @@ -27,7 +28,7 @@ "@islands": "./islands", "@islands/": "./islands/", "zod": "https://deno.land/x/zod@v3.21.4/mod.ts", - "$fresh/": "https://deno.land/x/fresh@1.4.2/", + "$fresh/": "https://deno.land/x/fresh@1.4.3/", "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.1", @@ -46,4 +47,4 @@ "_fresh" ] } -} \ No newline at end of file +} diff --git a/dev.ts b/dev.ts index 2d85d6c..1fe3e34 100755 --- a/dev.ts +++ b/dev.ts @@ -1,5 +1,6 @@ #!/usr/bin/env -S deno run -A --watch=static/,routes/ import dev from "$fresh/dev.ts"; +import config from "./fresh.config.ts"; -await dev(import.meta.url, "./main.ts"); +await dev(import.meta.url, "./main.ts", config); diff --git a/fresh.config.ts b/fresh.config.ts new file mode 100644 index 0000000..fd48d1b --- /dev/null +++ b/fresh.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "$fresh/server.ts"; +import twindPlugin from "$fresh/plugins/twind.ts"; +import twindConfig from "./twind.config.ts"; +export default defineConfig({ + plugins: [twindPlugin(twindConfig)], +}); diff --git a/fresh.gen.ts b/fresh.gen.ts index 8c9cf6c..7652e7b 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -4,42 +4,43 @@ import * as $0 from "./routes/_404.tsx"; import * as $1 from "./routes/_app.tsx"; -import * as $2 from "./routes/_middleware.ts"; -import * as $3 from "./routes/admin/log/index.tsx"; -import * as $4 from "./routes/admin/performance/index.tsx"; -import * as $5 from "./routes/api/articles/[name].ts"; -import * as $6 from "./routes/api/articles/create/index.ts"; -import * as $7 from "./routes/api/articles/index.ts"; -import * as $8 from "./routes/api/auth/callback.ts"; -import * as $9 from "./routes/api/auth/login.ts"; -import * as $10 from "./routes/api/auth/logout.ts"; -import * as $11 from "./routes/api/cache/index.ts"; -import * as $12 from "./routes/api/images/index.ts"; -import * as $13 from "./routes/api/index.ts"; -import * as $14 from "./routes/api/logs.ts"; -import * as $15 from "./routes/api/movies/[name].ts"; -import * as $16 from "./routes/api/movies/enhance/[name].ts"; -import * as $17 from "./routes/api/movies/index.ts"; -import * as $18 from "./routes/api/query/index.ts"; -import * as $19 from "./routes/api/query/sync.ts"; -import * as $20 from "./routes/api/recipes/[name].ts"; -import * as $21 from "./routes/api/recipes/index.ts"; -import * as $22 from "./routes/api/resources.ts"; -import * as $23 from "./routes/api/series/[name].ts"; -import * as $24 from "./routes/api/series/enhance/[name].ts"; -import * as $25 from "./routes/api/series/index.ts"; -import * as $26 from "./routes/api/tmdb/[id].ts"; -import * as $27 from "./routes/api/tmdb/credits/[id].ts"; -import * as $28 from "./routes/api/tmdb/query.ts"; -import * as $29 from "./routes/articles/[name].tsx"; -import * as $30 from "./routes/articles/index.tsx"; -import * as $31 from "./routes/index.tsx"; -import * as $32 from "./routes/movies/[name].tsx"; -import * as $33 from "./routes/movies/index.tsx"; -import * as $34 from "./routes/recipes/[name].tsx"; -import * as $35 from "./routes/recipes/index.tsx"; -import * as $36 from "./routes/series/[name].tsx"; -import * as $37 from "./routes/series/index.tsx"; +import * as $2 from "./routes/_layout.tsx"; +import * as $3 from "./routes/_middleware.ts"; +import * as $4 from "./routes/admin/log/index.tsx"; +import * as $5 from "./routes/admin/performance/index.tsx"; +import * as $6 from "./routes/api/articles/[name].ts"; +import * as $7 from "./routes/api/articles/create/index.ts"; +import * as $8 from "./routes/api/articles/index.ts"; +import * as $9 from "./routes/api/auth/callback.ts"; +import * as $10 from "./routes/api/auth/login.ts"; +import * as $11 from "./routes/api/auth/logout.ts"; +import * as $12 from "./routes/api/cache/index.ts"; +import * as $13 from "./routes/api/images/index.ts"; +import * as $14 from "./routes/api/index.ts"; +import * as $15 from "./routes/api/logs.ts"; +import * as $16 from "./routes/api/movies/[name].ts"; +import * as $17 from "./routes/api/movies/enhance/[name].ts"; +import * as $18 from "./routes/api/movies/index.ts"; +import * as $19 from "./routes/api/query/index.ts"; +import * as $20 from "./routes/api/query/sync.ts"; +import * as $21 from "./routes/api/recipes/[name].ts"; +import * as $22 from "./routes/api/recipes/index.ts"; +import * as $23 from "./routes/api/resources.ts"; +import * as $24 from "./routes/api/series/[name].ts"; +import * as $25 from "./routes/api/series/enhance/[name].ts"; +import * as $26 from "./routes/api/series/index.ts"; +import * as $27 from "./routes/api/tmdb/[id].ts"; +import * as $28 from "./routes/api/tmdb/credits/[id].ts"; +import * as $29 from "./routes/api/tmdb/query.ts"; +import * as $30 from "./routes/articles/[name].tsx"; +import * as $31 from "./routes/articles/index.tsx"; +import * as $32 from "./routes/index.tsx"; +import * as $33 from "./routes/movies/[name].tsx"; +import * as $34 from "./routes/movies/index.tsx"; +import * as $35 from "./routes/recipes/[name].tsx"; +import * as $36 from "./routes/recipes/index.tsx"; +import * as $37 from "./routes/series/[name].tsx"; +import * as $38 from "./routes/series/index.tsx"; import * as $$0 from "./islands/Counter.tsx"; import * as $$1 from "./islands/IngredientsList.tsx"; import * as $$2 from "./islands/KMenu.tsx"; @@ -56,42 +57,43 @@ const manifest = { routes: { "./routes/_404.tsx": $0, "./routes/_app.tsx": $1, - "./routes/_middleware.ts": $2, - "./routes/admin/log/index.tsx": $3, - "./routes/admin/performance/index.tsx": $4, - "./routes/api/articles/[name].ts": $5, - "./routes/api/articles/create/index.ts": $6, - "./routes/api/articles/index.ts": $7, - "./routes/api/auth/callback.ts": $8, - "./routes/api/auth/login.ts": $9, - "./routes/api/auth/logout.ts": $10, - "./routes/api/cache/index.ts": $11, - "./routes/api/images/index.ts": $12, - "./routes/api/index.ts": $13, - "./routes/api/logs.ts": $14, - "./routes/api/movies/[name].ts": $15, - "./routes/api/movies/enhance/[name].ts": $16, - "./routes/api/movies/index.ts": $17, - "./routes/api/query/index.ts": $18, - "./routes/api/query/sync.ts": $19, - "./routes/api/recipes/[name].ts": $20, - "./routes/api/recipes/index.ts": $21, - "./routes/api/resources.ts": $22, - "./routes/api/series/[name].ts": $23, - "./routes/api/series/enhance/[name].ts": $24, - "./routes/api/series/index.ts": $25, - "./routes/api/tmdb/[id].ts": $26, - "./routes/api/tmdb/credits/[id].ts": $27, - "./routes/api/tmdb/query.ts": $28, - "./routes/articles/[name].tsx": $29, - "./routes/articles/index.tsx": $30, - "./routes/index.tsx": $31, - "./routes/movies/[name].tsx": $32, - "./routes/movies/index.tsx": $33, - "./routes/recipes/[name].tsx": $34, - "./routes/recipes/index.tsx": $35, - "./routes/series/[name].tsx": $36, - "./routes/series/index.tsx": $37, + "./routes/_layout.tsx": $2, + "./routes/_middleware.ts": $3, + "./routes/admin/log/index.tsx": $4, + "./routes/admin/performance/index.tsx": $5, + "./routes/api/articles/[name].ts": $6, + "./routes/api/articles/create/index.ts": $7, + "./routes/api/articles/index.ts": $8, + "./routes/api/auth/callback.ts": $9, + "./routes/api/auth/login.ts": $10, + "./routes/api/auth/logout.ts": $11, + "./routes/api/cache/index.ts": $12, + "./routes/api/images/index.ts": $13, + "./routes/api/index.ts": $14, + "./routes/api/logs.ts": $15, + "./routes/api/movies/[name].ts": $16, + "./routes/api/movies/enhance/[name].ts": $17, + "./routes/api/movies/index.ts": $18, + "./routes/api/query/index.ts": $19, + "./routes/api/query/sync.ts": $20, + "./routes/api/recipes/[name].ts": $21, + "./routes/api/recipes/index.ts": $22, + "./routes/api/resources.ts": $23, + "./routes/api/series/[name].ts": $24, + "./routes/api/series/enhance/[name].ts": $25, + "./routes/api/series/index.ts": $26, + "./routes/api/tmdb/[id].ts": $27, + "./routes/api/tmdb/credits/[id].ts": $28, + "./routes/api/tmdb/query.ts": $29, + "./routes/articles/[name].tsx": $30, + "./routes/articles/index.tsx": $31, + "./routes/index.tsx": $32, + "./routes/movies/[name].tsx": $33, + "./routes/movies/index.tsx": $34, + "./routes/recipes/[name].tsx": $35, + "./routes/recipes/index.tsx": $36, + "./routes/series/[name].tsx": $37, + "./routes/series/index.tsx": $38, }, islands: { "./islands/Counter.tsx": $$0, diff --git a/islands/Search.tsx b/islands/Search.tsx index 6db3605..778452b 100644 --- a/islands/Search.tsx +++ b/islands/Search.tsx @@ -71,7 +71,7 @@ export const SearchResultItem = ( ) => { const doc = item.document; const resourceType = resources[doc.type]; - const href = (resourceType) ? `${resourceType.link}/${doc.id}` : ""; + const href = resourceType ? `${resourceType.link}/${doc.id}` : ""; return ( v.replaceAll(" ", "-")); +} + export async function createTags(content: string) { if (!openAI) return; const chatCompletion = await openAI.createChatCompletion({ diff --git a/lib/recommendation.ts b/lib/recommendation.ts new file mode 100644 index 0000000..3df2753 --- /dev/null +++ b/lib/recommendation.ts @@ -0,0 +1,53 @@ +import * as cache from "@lib/cache/cache.ts"; +import * as openai from "@lib/openai.ts"; +import { GenericResource } from "@lib/types.ts"; +import { parseRating } from "@lib/helpers.ts"; + +type RecommendationResource = { + id: string; + type: string; + rating: number; + tags?: string[]; + keywords?: string[]; + author?: string; + year?: number; +}; + +export async function createRecommendationResource( + res: GenericResource, + description?: string, +) { + const cacheId = `recommendations:${res.type}:${res.id}`; + const resource: RecommendationResource = await cache.get(cacheId) || { + id: res.id, + type: res.type, + rating: -1, + }; + if (description && !resource.keywords) { + const keywords = await openai.createKeywords(res.type, description); + if (keywords?.length) { + resource.keywords = keywords; + } + } + + const { author, date, rating } = res.meta || {}; + + if (res?.tags) { + resource.tags = res.tags; + } + + if (typeof rating !== "undefined") { + resource.rating = parseRating(rating); + } + + if (author) { + resource.author = author; + } + + if (date) { + const d = typeof date === "string" ? new Date(date) : date; + resource.year = d.getFullYear(); + } + + cache.set(cacheId, JSON.stringify(resource)); +} diff --git a/lib/resource/movies.ts b/lib/resource/movies.ts index 3ed7566..2b86b62 100644 --- a/lib/resource/movies.ts +++ b/lib/resource/movies.ts @@ -12,6 +12,8 @@ export type Movie = { tags: string[]; meta: { date: Date; + tmdbId?: number; + keywords?: string[]; image: string; thumbnail?: string; average?: string; @@ -26,6 +28,11 @@ export function renderMovie(movie: Movie) { meta.date = formatDate(meta.date) as unknown as Date; } + delete meta.thumbnail; + delete meta.average; + + const movieImage = `![](${movie.meta.image})`; + return fixRenderedMarkdown(`${ meta ? `--- @@ -35,7 +42,11 @@ ${stringify(meta)} ---` } # ${movie.name} -${movie.meta.image ? `![](${movie.meta.image})` : ""} +${ + // So we do not add a new image to the description everytime we render + (movie.meta.image && !movie.description.includes(movieImage)) + ? movieImage + : ""} ${movie.tags.map((t) => `#${t}`).join(" ")} ${movie.description} `); @@ -103,6 +114,10 @@ const crud = createCrud({ hasThumbnails: true, }); -export const getMovie = crud.read; +export const getMovie = async (id: string) => { + const movie = await crud.read(id); + return movie; +}; + export const getAllMovies = crud.readAll; export const createMovie = crud.create; diff --git a/lib/resource/series.ts b/lib/resource/series.ts index 069f59a..3b4bda2 100644 --- a/lib/resource/series.ts +++ b/lib/resource/series.ts @@ -15,6 +15,7 @@ export type Series = { date: Date; image: string; author: string; + tmdbId?: number; rating: number; average?: string; thumbnail?: string; @@ -22,12 +23,17 @@ export type Series = { }; }; -function renderSeries(movie: Series) { - const meta = movie.meta; +function renderSeries(series: Series) { + const meta = series.meta; if ("date" in meta) { meta.date = formatDate(meta.date); } + delete meta.thumbnail; + delete meta.average; + + const movieImage = `![](${series.meta.image})`; + return fixRenderedMarkdown(`${ meta ? `--- @@ -36,10 +42,14 @@ ${stringify(meta)} : `--- ---` } -# ${movie.name} -${movie.meta.image ? `![](${movie.meta.image})` : ""} -${movie.tags.map((t) => `#${t}`).join(" ")} -${movie.description} +# ${series.name} +${ + // So we do not add a new image to the description everytime we render + (series.meta.image && !series.description.includes(movieImage)) + ? movieImage + : ""} +${series.tags.map((t) => `#${t}`).join(" ")} +${series.description} `); } diff --git a/lib/search.ts b/lib/search.ts index 99d1803..1a2cabc 100644 --- a/lib/search.ts +++ b/lib/search.ts @@ -1,4 +1,3 @@ -import { BadRequestError } from "@lib/errors.ts"; import { resources } from "@lib/resources.ts"; import { SearchResult } from "@lib/types.ts"; import { getTypeSenseClient } from "@lib/typesense.ts"; @@ -15,9 +14,9 @@ type SearchParams = { query_by?: string; }; -export function parseResourceUrl(_url: string): SearchParams | undefined { +export function parseResourceUrl(_url: string | URL): SearchParams | undefined { try { - const url = new URL(_url); + const url = typeof _url === "string" ? new URL(_url) : _url; let query = url.searchParams.get("q") || "*"; if (!query) { return undefined; diff --git a/lib/types.ts b/lib/types.ts index 850609d..f5024c1 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -37,6 +37,7 @@ export interface TMDBSeries { export type GenericResource = { name: string; id: string; + tags?: string[]; type: keyof typeof resources; meta?: { image?: string; diff --git a/main.ts b/main.ts index 984b0ae..1990aa8 100644 --- a/main.ts +++ b/main.ts @@ -8,8 +8,7 @@ import "$std/dotenv/load.ts"; import { start } from "$fresh/server.ts"; import manifest from "./fresh.gen.ts"; +import config from "./fresh.config.ts"; -import twindPlugin from "$fresh/plugins/twind.ts"; -import twindConfig from "./twind.config.ts"; +await start(manifest, config); -await start(manifest, { plugins: [twindPlugin(twindConfig)] }); diff --git a/routes/_layout.tsx b/routes/_layout.tsx new file mode 100644 index 0000000..8fc8acd --- /dev/null +++ b/routes/_layout.tsx @@ -0,0 +1,41 @@ +import { LayoutProps } from "$fresh/server.ts"; +import { resources } from "@lib/resources.ts"; +import { CSS, KATEX_CSS } from "https://deno.land/x/gfm@0.2.5/mod.ts"; +import { Head } from "$fresh/runtime.ts"; +import { Emoji } from "@components/Emoji.tsx"; + +export default function MyLayout({ Component, url }: LayoutProps) { + return ( + + ); +} diff --git a/routes/api/movies/[name].ts b/routes/api/movies/[name].ts index 28321a2..80e45fa 100644 --- a/routes/api/movies/[name].ts +++ b/routes/api/movies/[name].ts @@ -25,8 +25,11 @@ export const handler: Handlers = { const releaseDate = movieDetails.release_date; const posterPath = movieDetails.poster_path; - const director = - movieCredits?.crew?.filter?.((person) => person.job === "Director")[0]; + const director = movieCredits?.crew?.filter?.((person) => + person.job === "Director" + )[0]; + + movieDetails.overview; let finalPath = ""; const name = movieDetails.title || movieDetails.original_title || @@ -41,7 +44,9 @@ export const handler: Handlers = { await createDocument(finalPath, poster); } - const metadata = {} as Movie["meta"]; + const metadata = { + tmdbId, + } as Movie["meta"]; if (releaseDate) { metadata.date = new Date(releaseDate); } diff --git a/routes/api/movies/enhance/[name].ts b/routes/api/movies/enhance/[name].ts index 5f38ede..d4c3c81 100644 --- a/routes/api/movies/enhance/[name].ts +++ b/routes/api/movies/enhance/[name].ts @@ -11,6 +11,7 @@ import { NotFoundError, } from "@lib/errors.ts"; import * as cache from "@lib/cache/cache.ts"; +import { createRecommendationResource } from "@lib/recommendation.ts"; const POST = async ( req: Request, @@ -42,8 +43,9 @@ const POST = async ( movie.meta.date = new Date(releaseDate); } - const director = - movieCredits?.crew?.filter?.((person) => person.job === "Director")[0]; + const director = movieCredits?.crew?.filter?.((person) => + person.job === "Director" + )[0]; if (director && !movie.meta.author) { movie.meta.author = director.name; } @@ -57,6 +59,10 @@ const POST = async ( ]; } + if (!movie.meta.tmdbId) { + movie.meta.tmdbId = tmdbId; + } + let finalPath = ""; const posterPath = movieDetails.poster_path; if (posterPath && !movie.meta.image) { @@ -72,6 +78,8 @@ const POST = async ( cache.del(`documents:Media:movies:${name}.md`); + createRecommendationResource(movie, movieDetails.overview); + return json(movie); }; diff --git a/routes/api/series/[name].ts b/routes/api/series/[name].ts index cdf0078..16ca4ad 100644 --- a/routes/api/series/[name].ts +++ b/routes/api/series/[name].ts @@ -42,7 +42,7 @@ export const handler: Handlers = { await createDocument(finalPath, poster); } - const metadata = {} as Series["meta"]; + const metadata = { tmdbId } as Series["meta"]; if (releaseDate) { metadata.date = new Date(releaseDate); } diff --git a/routes/movies/[name].tsx b/routes/movies/[name].tsx index e22fa46..fd75097 100644 --- a/routes/movies/[name].tsx +++ b/routes/movies/[name].tsx @@ -1,4 +1,4 @@ -import { Handlers, PageProps } from "$fresh/server.ts"; +import { Handlers, PageProps, RouteContext } from "$fresh/server.ts"; import { MainLayout } from "@components/layouts/main.tsx"; import { getMovie, Movie } from "@lib/resource/movies.ts"; import { RecipeHero } from "@components/RecipeHero.tsx"; @@ -7,17 +7,12 @@ import { renderMarkdown } from "@lib/documents.ts"; import { KMenu } from "@islands/KMenu.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; -export const handler: Handlers = { - async GET(_, ctx) { - const movie = await getMovie(ctx.params.name); - return ctx.render({ movie, session: ctx.state.session }); - }, -}; - -export default function Greet( +export default async function Greet( props: PageProps<{ movie: Movie; session: Record }>, + ctx: RouteContext, ) { - const { movie, session } = props.data; + const movie = await getMovie(ctx.params.name); + const session = ctx.state.session; const { author = "", date = "" } = movie.meta; diff --git a/routes/movies/index.tsx b/routes/movies/index.tsx index 33e11d3..28038fc 100644 --- a/routes/movies/index.tsx +++ b/routes/movies/index.tsx @@ -1,4 +1,3 @@ - import { MainLayout } from "@components/layouts/main.tsx"; import { getAllMovies, Movie } from "@lib/resource/movies.ts"; import { ResourceCard } from "@components/Card.tsx"; @@ -8,26 +7,18 @@ import { KMenu } from "@islands/KMenu.tsx"; import { RedirectSearchHandler } from "@islands/Search.tsx"; import { parseResourceUrl, searchResource } from "@lib/search.ts"; import { SearchResult } from "@lib/types.ts"; +import { PageProps } from "$fresh/server.ts"; -export const handler: Handlers< - { movies: Movie[] | null; searchResults?: SearchResult } -> = { - async GET(req, ctx) { - const movies = await getAllMovies(); - const searchParams = parseResourceUrl(req.url); - const searchResults = searchParams && - await searchResource({ ...searchParams, type: "movie" }); - return ctx.render({ - movies: movies.sort((a, b) => a?.meta?.rating > b?.meta?.rating ? -1 : 1), - searchResults, - }); - }, -}; - -export default function Greet( +export default async function Greet( props: PageProps<{ movies: Movie[] | null; searchResults: SearchResult }>, ) { - const { movies, searchResults } = props.data; + const allMovies = await getAllMovies(); + const searchParams = parseResourceUrl(props.url); + const searchResults = searchParams && + await searchResource({ ...searchParams, type: "movie" }); + const movies = allMovies.sort((a, b) => + a?.meta?.rating > b?.meta?.rating ? -1 : 1 + ); return (