feat: add initial recommendation data

This commit is contained in:
2023-09-08 13:33:29 +02:00
parent 517b1ba23d
commit cc112b7554
19 changed files with 289 additions and 170 deletions

View File

@ -1,4 +1,4 @@
import { OpenAI } from "https://deno.land/x/openai/mod.ts";
import { OpenAI } from "https://deno.land/x/openai@1.4.2/mod.ts";
import { OPENAI_API_KEY } from "@lib/env.ts";
const openAI = OPENAI_API_KEY && new OpenAI(OPENAI_API_KEY);
@ -63,6 +63,28 @@ export async function extractAuthorName(content: string) {
return author;
}
export async function createKeywords(type: string, description: string) {
if (!openAI) return;
const chatCompletion = await openAI.createChatCompletion({
model: "gpt-3.5-turbo",
messages: [
{
"role": "system",
"content":
`you create some general vibey keywords to use in a recommendation system based on a ${type} description`,
},
{ "role": "user", "content": description.slice(0, 2000) },
{
"role": "user",
"content": "return a list of keywords seperated by commas",
},
],
});
return chatCompletion.choices[0].message.content?.toLowerCase().split(", ")
.map((v) => v.replaceAll(" ", "-"));
}
export async function createTags(content: string) {
if (!openAI) return;
const chatCompletion = await openAI.createChatCompletion({

53
lib/recommendation.ts Normal file
View File

@ -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));
}

View File

@ -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<Movie>({
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;

View File

@ -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}
`);
}

View File

@ -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;

View File

@ -37,6 +37,7 @@ export interface TMDBSeries {
export type GenericResource = {
name: string;
id: string;
tags?: string[];
type: keyof typeof resources;
meta?: {
image?: string;