feat: some shit
This commit is contained in:
parent
6123956f08
commit
c7d0e97ac0
95
components/Checkbox.tsx
Normal file
95
components/Checkbox.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { Signal, useSignal } from "@preact/signals";
|
||||
import { useId, useState } from "preact/hooks";
|
||||
|
||||
interface CheckboxProps {
|
||||
label: string;
|
||||
isChecked?: boolean;
|
||||
onChange: (isChecked: boolean) => void;
|
||||
}
|
||||
|
||||
const Checkbox2: preact.FunctionalComponent<CheckboxProps> = (
|
||||
{ label, isChecked = false, onChange },
|
||||
) => {
|
||||
const [checked, setChecked] = useState(isChecked);
|
||||
|
||||
const toggleCheckbox = () => {
|
||||
const newChecked = !checked;
|
||||
setChecked(newChecked);
|
||||
onChange(newChecked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
class="flex items-center rounded-xl p-1 pl-4"
|
||||
style={{ background: "var(--background)", color: "var(--foreground)" }}
|
||||
>
|
||||
<span>
|
||||
{label}
|
||||
</span>
|
||||
<label
|
||||
class="relative flex cursor-pointer items-center rounded-full p-3"
|
||||
for="checkbox"
|
||||
data-ripple-dark="true"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="before:content[''] peer relative h-5 w-5 cursor-pointer appearance-none rounded-md border border-blue-gray-200 transition-all before:absolute before:top-2/4 before:left-2/4 before:block before:h-12 before:w-12 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full before:bg-blue-gray-500 before:opacity-0 before:transition-opacity hover:before:opacity-10"
|
||||
id="checkbox"
|
||||
checked
|
||||
/>
|
||||
<div
|
||||
class={`pointer-events-none absolute top-2/4 left-2/4 -translate-y-2/4 -translate-x-2/4 text-white opacity-${
|
||||
checked ? 100 : 0
|
||||
} transition-opacity`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3.5 w-3.5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="white"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Checkbox = (
|
||||
{ label, checked = useSignal(false) }: {
|
||||
label: string;
|
||||
checked?: Signal<boolean>;
|
||||
},
|
||||
) => {
|
||||
const _id = useId();
|
||||
const id = `checkbox-${_id}`;
|
||||
return (
|
||||
<label
|
||||
className="flex items-center py-3 px-4 rounded-xl"
|
||||
style={{ color: "var(--foreground)", background: "var(--background)" }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked.value}
|
||||
name="checkbox-one"
|
||||
id={id}
|
||||
onChange={(ev) => {
|
||||
checked.value = ev.currentTarget.checked;
|
||||
}}
|
||||
class="bg-gray-200 hover:bg-gray-300 cursor-pointer
|
||||
w-5 h-5 border-3 border-amber-500 focus:outline-none rounded-lg"
|
||||
/>
|
||||
<span class="ml-3">{label}</span>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
14
components/Image.tsx
Normal file
14
components/Image.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { isLocalImage } from "@lib/string.ts";
|
||||
|
||||
export function Image(
|
||||
props: { class: string; src: string; width?: number; height?: number },
|
||||
) {
|
||||
if (isLocalImage(props.src)) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img src={props.src} width={props.width} height={props.height} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { Card } from "@components/Card.tsx";
|
||||
import { Movie } from "@lib/resource/movies.ts";
|
||||
import { Series } from "@lib/resource/series.ts";
|
||||
|
||||
export function MovieCard({ movie }: { movie: Movie }) {
|
||||
const { meta: { image = "/placeholder.svg" } = {} } = movie;
|
||||
@ -16,3 +17,19 @@ export function MovieCard({ movie }: { movie: Movie }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SeriesCard({ series }: { series: Series }) {
|
||||
const { meta: { image = "/placeholder.svg" } = {} } = series;
|
||||
|
||||
const imageUrl = image?.startsWith("Media/series/")
|
||||
? `/api/images?image=${image}&width=200&height=200`
|
||||
: image;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={series.name}
|
||||
image={imageUrl}
|
||||
link={`/series/${series.id}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
34
components/Rating.tsx
Normal file
34
components/Rating.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { IconStar, IconStarFilled } from "@components/icons.tsx";
|
||||
import { useSignal } from "@preact/signals";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
export const Rating = (
|
||||
props: { max?: number; rating: number },
|
||||
) => {
|
||||
const [rating, setRating] = useState(props.rating);
|
||||
const [hover, setHover] = useState(0);
|
||||
const max = useSignal(props.max || 5);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="flex gap-2 px-5 rounded-2xl bg-gray-200 z-10"
|
||||
style={{ color: "var(--foreground)", background: "var(--background)" }}
|
||||
>
|
||||
{Array.from({ length: max.value }).map((_, i) => {
|
||||
return (
|
||||
<span
|
||||
class={`my-5 cursor-pointer opacity-${
|
||||
(i + 1) <= rating ? 100 : (i + 1) <= hover ? 20 : 100
|
||||
}`}
|
||||
onMouseOver={() => setHover(i + 1)}
|
||||
onClick={() => setRating(i + 1)}
|
||||
>
|
||||
{(i + 1) <= rating || (i + 1) <= hover
|
||||
? <IconStarFilled class="w-4 h-4" />
|
||||
: <IconStar class="w-4 h-4" />}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
56
fresh.gen.ts
56
fresh.gen.ts
@ -21,19 +21,21 @@ import * as $15 from "./routes/api/query/index.ts";
|
||||
import * as $16 from "./routes/api/recipes/[name].ts";
|
||||
import * as $17 from "./routes/api/recipes/index.ts";
|
||||
import * as $18 from "./routes/api/resources.ts";
|
||||
import * as $19 from "./routes/api/series/index.ts";
|
||||
import * as $20 from "./routes/api/tmdb/[id].ts";
|
||||
import * as $21 from "./routes/api/tmdb/credits/[id].ts";
|
||||
import * as $22 from "./routes/api/tmdb/query.ts";
|
||||
import * as $23 from "./routes/articles/[name].tsx";
|
||||
import * as $24 from "./routes/articles/index.tsx";
|
||||
import * as $25 from "./routes/index.tsx";
|
||||
import * as $26 from "./routes/movies/[name].tsx";
|
||||
import * as $27 from "./routes/movies/index.tsx";
|
||||
import * as $28 from "./routes/recipes/[name].tsx";
|
||||
import * as $29 from "./routes/recipes/index.tsx";
|
||||
import * as $30 from "./routes/series/[name].tsx";
|
||||
import * as $31 from "./routes/series/index.tsx";
|
||||
import * as $19 from "./routes/api/series/[name].ts";
|
||||
import * as $20 from "./routes/api/series/enhance/[name].ts";
|
||||
import * as $21 from "./routes/api/series/index.ts";
|
||||
import * as $22 from "./routes/api/tmdb/[id].ts";
|
||||
import * as $23 from "./routes/api/tmdb/credits/[id].ts";
|
||||
import * as $24 from "./routes/api/tmdb/query.ts";
|
||||
import * as $25 from "./routes/articles/[name].tsx";
|
||||
import * as $26 from "./routes/articles/index.tsx";
|
||||
import * as $27 from "./routes/index.tsx";
|
||||
import * as $28 from "./routes/movies/[name].tsx";
|
||||
import * as $29 from "./routes/movies/index.tsx";
|
||||
import * as $30 from "./routes/recipes/[name].tsx";
|
||||
import * as $31 from "./routes/recipes/index.tsx";
|
||||
import * as $32 from "./routes/series/[name].tsx";
|
||||
import * as $33 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";
|
||||
@ -66,19 +68,21 @@ const manifest = {
|
||||
"./routes/api/recipes/[name].ts": $16,
|
||||
"./routes/api/recipes/index.ts": $17,
|
||||
"./routes/api/resources.ts": $18,
|
||||
"./routes/api/series/index.ts": $19,
|
||||
"./routes/api/tmdb/[id].ts": $20,
|
||||
"./routes/api/tmdb/credits/[id].ts": $21,
|
||||
"./routes/api/tmdb/query.ts": $22,
|
||||
"./routes/articles/[name].tsx": $23,
|
||||
"./routes/articles/index.tsx": $24,
|
||||
"./routes/index.tsx": $25,
|
||||
"./routes/movies/[name].tsx": $26,
|
||||
"./routes/movies/index.tsx": $27,
|
||||
"./routes/recipes/[name].tsx": $28,
|
||||
"./routes/recipes/index.tsx": $29,
|
||||
"./routes/series/[name].tsx": $30,
|
||||
"./routes/series/index.tsx": $31,
|
||||
"./routes/api/series/[name].ts": $19,
|
||||
"./routes/api/series/enhance/[name].ts": $20,
|
||||
"./routes/api/series/index.ts": $21,
|
||||
"./routes/api/tmdb/[id].ts": $22,
|
||||
"./routes/api/tmdb/credits/[id].ts": $23,
|
||||
"./routes/api/tmdb/query.ts": $24,
|
||||
"./routes/articles/[name].tsx": $25,
|
||||
"./routes/articles/index.tsx": $26,
|
||||
"./routes/index.tsx": $27,
|
||||
"./routes/movies/[name].tsx": $28,
|
||||
"./routes/movies/index.tsx": $29,
|
||||
"./routes/recipes/[name].tsx": $30,
|
||||
"./routes/recipes/index.tsx": $31,
|
||||
"./routes/series/[name].tsx": $32,
|
||||
"./routes/series/index.tsx": $33,
|
||||
},
|
||||
islands: {
|
||||
"./islands/Counter.tsx": $$0,
|
||||
|
@ -3,6 +3,7 @@ import { addMovieInfos } from "@islands/KMenu/commands/add_movie_infos.ts";
|
||||
import { createNewMovie } from "@islands/KMenu/commands/create_movie.ts";
|
||||
import { createNewArticle } from "@islands/KMenu/commands/create_article.ts";
|
||||
import { getCookie } from "@lib/string.ts";
|
||||
import { addSeriesInfo } from "@islands/KMenu/commands/add_series_infos.ts";
|
||||
|
||||
export const menus: Record<string, Menu> = {
|
||||
main: {
|
||||
@ -54,6 +55,7 @@ export const menus: Record<string, Menu> = {
|
||||
return !!getCookie("session_cookie");
|
||||
},
|
||||
},
|
||||
addSeriesInfo,
|
||||
createNewArticle,
|
||||
createNewMovie,
|
||||
addMovieInfos,
|
||||
|
@ -46,7 +46,6 @@ export const addMovieInfos: MenuEntry = {
|
||||
const loc = globalThis["location"];
|
||||
if (!getCookie("session_cookie")) return false;
|
||||
return (loc?.pathname?.includes("movie") &&
|
||||
!loc.pathname.endsWith("movies")) ||
|
||||
(loc?.pathname?.includes("series") && !loc.pathname.endsWith("series"));
|
||||
!loc.pathname.endsWith("movies"));
|
||||
},
|
||||
};
|
||||
|
@ -1,39 +1,43 @@
|
||||
import { MenuEntry } from "@islands/KMenu/types.ts";
|
||||
import { TMDBMovie } from "@lib/types.ts";
|
||||
import { TMDBSeries } from "@lib/types.ts";
|
||||
import { getCookie } from "@lib/string.ts";
|
||||
import { Series } from "@lib/resource/series.ts";
|
||||
|
||||
export const addMovieInfos: MenuEntry = {
|
||||
title: "Add Movie infos",
|
||||
export const addSeriesInfo: MenuEntry = {
|
||||
title: "Add Series infos",
|
||||
meta: "",
|
||||
icon: "IconReportSearch",
|
||||
cb: async (state, context) => {
|
||||
console.log({ state, context });
|
||||
state.activeState.value = "loading";
|
||||
const movie = context as Series;
|
||||
const series = context as Series;
|
||||
|
||||
const query = movie.name;
|
||||
const query = series.name;
|
||||
|
||||
const response = await fetch(
|
||||
`/api/tmdb/query?q=${encodeURIComponent(query)}&type=serie`,
|
||||
);
|
||||
|
||||
const json = await response.json() as TMDBMovie[];
|
||||
const json = await response.json() as TMDBSeries[];
|
||||
|
||||
const menuID = `result/${movie.name}`;
|
||||
console.log({ json });
|
||||
|
||||
const menuID = `result/${series.name}`;
|
||||
|
||||
state.menus[menuID] = {
|
||||
title: "Select",
|
||||
entries: json.map((m) => ({
|
||||
title: `${m.title} released ${m.release_date}`,
|
||||
title: `${m.name || m.original_name} released ${m.first_air_date}`,
|
||||
cb: async () => {
|
||||
state.activeState.value = "loading";
|
||||
await fetch(`/api/movies/enhance/${movie.name}/`, {
|
||||
console.log({ m });
|
||||
await fetch(`/api/series/enhance/${series.name}/`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ tmdbId: m.id }),
|
||||
});
|
||||
state.visible.value = false;
|
||||
state.activeState.value = "normal";
|
||||
window.location.reload();
|
||||
//window.location.reload();
|
||||
},
|
||||
})),
|
||||
};
|
||||
@ -45,8 +49,7 @@ export const addMovieInfos: MenuEntry = {
|
||||
visible: () => {
|
||||
const loc = globalThis["location"];
|
||||
if (!getCookie("session_cookie")) return false;
|
||||
return (loc?.pathname?.includes("movie") &&
|
||||
!loc.pathname.endsWith("movies")) ||
|
||||
(loc?.pathname?.includes("series") && !loc.pathname.endsWith("series"));
|
||||
return (loc?.pathname?.includes("series") &&
|
||||
!loc.pathname.endsWith("series"));
|
||||
},
|
||||
};
|
||||
|
@ -6,6 +6,9 @@ import { SearchResult } from "@lib/types.ts";
|
||||
import { resources } from "@lib/resources.ts";
|
||||
import { isLocalImage } from "@lib/string.ts";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
import Checkbox from "@components/Checkbox.tsx";
|
||||
import { Rating } from "@components/Rating.tsx";
|
||||
import { useSignal } from "@preact/signals";
|
||||
|
||||
export const RedirectSearchHandler = () => {
|
||||
useEventListener("keydown", (e: KeyboardEvent) => {
|
||||
@ -79,15 +82,24 @@ const SearchComponent = (
|
||||
const [searchQuery, setSearchQuery] = useState(q);
|
||||
const [data, setData] = useState<SearchResult>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const showSeenStatus = useSignal(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
if ("history" in globalThis) {
|
||||
const u = new URL(window.location.href);
|
||||
if (u.searchParams.get("q") !== searchQuery) {
|
||||
u.searchParams.set("q", searchQuery);
|
||||
window.history.replaceState({}, "", u);
|
||||
}
|
||||
if (showSeenStatus.value) {
|
||||
u.searchParams.set("status", "not-seen");
|
||||
} else {
|
||||
u.searchParams.delete("status");
|
||||
}
|
||||
|
||||
window.history.replaceState({}, "", u);
|
||||
}
|
||||
|
||||
console.log({ showSeen: showSeenStatus.value });
|
||||
|
||||
const fetchData = async (query: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@ -98,6 +110,9 @@ const SearchComponent = (
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (showSeenStatus.value) {
|
||||
fetchUrl.searchParams.set("status", "not-seen");
|
||||
}
|
||||
if (type) {
|
||||
fetchUrl.searchParams.set("type", type);
|
||||
}
|
||||
@ -131,25 +146,28 @@ const SearchComponent = (
|
||||
debouncedFetchData(q);
|
||||
}, []);
|
||||
|
||||
console.log({ data, isLoading });
|
||||
return (
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-xl w-full shadow-2xl"
|
||||
style={{ background: "#2B2930", color: "#818181" }}
|
||||
>
|
||||
{isLoading && searchQuery
|
||||
? <IconLoader2 class="w-4 h-4 ml-4 mr-2 animate-spin" />
|
||||
: <IconSearch class="w-4 h-4 ml-4 mr-2" />}
|
||||
<input
|
||||
type="text"
|
||||
style={{ fontSize: "1.2em" }}
|
||||
class="bg-transparent py-3 w-full"
|
||||
ref={inputRef}
|
||||
value={searchQuery}
|
||||
onInput={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<header class="flex items-center gap-4">
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-xl w-full shadow-2xl"
|
||||
style={{ background: "#2B2930", color: "#818181" }}
|
||||
>
|
||||
{isLoading && searchQuery
|
||||
? <IconLoader2 class="w-4 h-4 ml-4 mr-2 animate-spin" />
|
||||
: <IconSearch class="w-4 h-4 ml-4 mr-2" />}
|
||||
<input
|
||||
type="text"
|
||||
style={{ fontSize: "1.2em" }}
|
||||
class="bg-transparent py-3 w-full"
|
||||
ref={inputRef}
|
||||
value={searchQuery}
|
||||
onInput={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox label="seen" checked={showSeenStatus} />
|
||||
<Rating rating={4} />
|
||||
</header>
|
||||
{data?.hits?.length && !isLoading
|
||||
? <SearchResultList showEmoji={!type} result={data} />
|
||||
: isLoading
|
||||
|
@ -5,7 +5,6 @@ export function useEventListener<T extends Event>(
|
||||
handler: (event: T) => void,
|
||||
element: Window | HTMLElement = window,
|
||||
) {
|
||||
console.log("Add Eventlistener", { eventName, element, handler });
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef<(event: Event) => void>();
|
||||
|
||||
|
17
lib/tmdb.ts
17
lib/tmdb.ts
@ -14,10 +14,27 @@ export function getMovie(id: number) {
|
||||
return moviedb.movieInfo({ id });
|
||||
}
|
||||
|
||||
export function getSeries(id: number) {
|
||||
return moviedb.tvInfo({ id });
|
||||
}
|
||||
|
||||
export function getMovieCredits(id: number) {
|
||||
return moviedb.movieCredits(id);
|
||||
}
|
||||
|
||||
export function getSeriesCredits(id: number) {
|
||||
return moviedb.tvCredits(id);
|
||||
}
|
||||
|
||||
export async function getMovieGenre(id: number) {
|
||||
const genres = await cache.get("/genres/movies");
|
||||
return moviedb.genreTvList();
|
||||
}
|
||||
|
||||
export async function getSeriesGenre(id: number) {
|
||||
const genres = await cache.get("/genres/series");
|
||||
}
|
||||
|
||||
export async function getMoviePoster(id: string): Promise<ArrayBuffer> {
|
||||
const cachedPoster = await cache.get("posters:" + id);
|
||||
|
||||
|
16
lib/types.ts
16
lib/types.ts
@ -17,6 +17,22 @@ export interface TMDBMovie {
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
}
|
||||
export interface TMDBSeries {
|
||||
adult: boolean;
|
||||
backdrop_path: string;
|
||||
genre_ids: number[];
|
||||
id: number;
|
||||
origin_country: string[];
|
||||
original_language: string;
|
||||
original_name: string;
|
||||
overview: string;
|
||||
popularity: number;
|
||||
poster_path: string;
|
||||
first_air_date: string;
|
||||
name: string;
|
||||
vote_average: number;
|
||||
vote_count: number;
|
||||
}
|
||||
|
||||
export interface GiteaOauthUser {
|
||||
sub: string;
|
||||
|
@ -76,7 +76,7 @@ async function initializeTypesense() {
|
||||
{ name: "name", type: "string" },
|
||||
{ name: "type", type: "string", facet: true },
|
||||
{ name: "date", type: "string", optional: true },
|
||||
{ name: "author", type: "string", facet: true },
|
||||
{ name: "author", type: "string", facet: true, optional: true },
|
||||
{ name: "rating", type: "int32", facet: true },
|
||||
{ name: "tags", type: "string[]", facet: true },
|
||||
{ name: "description", type: "string", optional: true },
|
||||
|
@ -9,6 +9,10 @@ export default function App({ Component }: AppProps) {
|
||||
<link href="/prism-material-dark.css" rel="stylesheet" />
|
||||
<style>
|
||||
{`
|
||||
:root {
|
||||
--background: rgb(43, 41, 48);
|
||||
--foreground: rgb(129, 129, 129);
|
||||
}
|
||||
/* work-sans-regular - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
|
@ -7,23 +7,27 @@ import { extractHashTags } from "@lib/string.ts";
|
||||
export const handler: Handlers = {
|
||||
async GET(req, _ctx) {
|
||||
const url = new URL(req.url);
|
||||
const query = url.searchParams.get("q");
|
||||
let query = url.searchParams.get("q");
|
||||
if (!query) {
|
||||
throw new BadRequestError('Query parameter "q" is required.');
|
||||
}
|
||||
query = decodeURIComponent(query);
|
||||
|
||||
const query_by = url.searchParams.get("query_by") ||
|
||||
"name,description,author,tags";
|
||||
|
||||
let filter_by = "";
|
||||
const filter_by: string[] = [];
|
||||
const type = url.searchParams.get("type");
|
||||
if (type) {
|
||||
filter_by = `type:=${type}`;
|
||||
filter_by.push(`type:=${type}`);
|
||||
}
|
||||
|
||||
const hashTags = extractHashTags(query);
|
||||
if (hashTags?.length) {
|
||||
//filter_by += `tags:=${}`
|
||||
filter_by.push(`tags:[${hashTags.map((t) => `\`${t}\``).join(",")}]`);
|
||||
for (const tag of hashTags) {
|
||||
query = query.replaceAll(`#${tag}`, "");
|
||||
}
|
||||
}
|
||||
|
||||
const typesenseClient = await getTypeSenseClient();
|
||||
@ -31,12 +35,16 @@ export const handler: Handlers = {
|
||||
throw new Error("Query not available");
|
||||
}
|
||||
|
||||
console.log({ query, query_by, filter_by: filter_by.join(" && ") });
|
||||
|
||||
// Perform the Typesense search
|
||||
const searchResults = await typesenseClient.collections("resources")
|
||||
.documents().search({
|
||||
q: query,
|
||||
query_by,
|
||||
filter_by,
|
||||
facet_by: "rating,author,tags",
|
||||
max_facet_values: 10,
|
||||
filter_by: filter_by.join(" && "),
|
||||
per_page: 50,
|
||||
});
|
||||
|
||||
|
68
routes/api/series/[name].ts
Normal file
68
routes/api/series/[name].ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Handlers } from "$fresh/server.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 { safeFileName } from "@lib/string.ts";
|
||||
import { createDocument } from "@lib/documents.ts";
|
||||
import { AccessDeniedError } from "@lib/errors.ts";
|
||||
import { createSeries, getSeries, Series } from "@lib/resource/series.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_, ctx) {
|
||||
const series = await getSeries(ctx.params.name);
|
||||
return json(series);
|
||||
},
|
||||
async POST(_, ctx) {
|
||||
const session = ctx.state.session;
|
||||
if (!session) {
|
||||
throw new AccessDeniedError();
|
||||
}
|
||||
|
||||
const tmdbId = parseInt(ctx.params.name);
|
||||
|
||||
const seriesDetails = await tmdb.getSeries(tmdbId);
|
||||
const seriesCredits = await tmdb.getSeriesCredits(tmdbId);
|
||||
|
||||
const releaseDate = seriesDetails.first_air_date;
|
||||
const posterPath = seriesDetails.poster_path;
|
||||
const director =
|
||||
seriesCredits?.crew?.filter?.((person) => person.job === "Director")[0];
|
||||
|
||||
let finalPath = "";
|
||||
const name = seriesDetails.name || seriesDetails.original_name ||
|
||||
ctx.params.name;
|
||||
if (posterPath) {
|
||||
const poster = await tmdb.getMoviePoster(posterPath);
|
||||
const extension = fileExtension(posterPath);
|
||||
|
||||
finalPath = `Media/series/images/${
|
||||
safeFileName(name)
|
||||
}_cover.${extension}`;
|
||||
await createDocument(finalPath, poster);
|
||||
}
|
||||
|
||||
const metadata = {} as Series["meta"];
|
||||
if (releaseDate) {
|
||||
metadata.date = new Date(releaseDate);
|
||||
}
|
||||
if (finalPath) {
|
||||
metadata.image = finalPath;
|
||||
}
|
||||
if (director) {
|
||||
metadata.author = director.name;
|
||||
}
|
||||
|
||||
const series: Series = {
|
||||
id: name,
|
||||
name: name,
|
||||
type: "series",
|
||||
description: "",
|
||||
tags: [],
|
||||
meta: metadata,
|
||||
};
|
||||
|
||||
await createSeries(series);
|
||||
|
||||
return json(series);
|
||||
},
|
||||
};
|
116
routes/api/series/enhance/[name].ts
Normal file
116
routes/api/series/enhance/[name].ts
Normal file
@ -0,0 +1,116 @@
|
||||
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 * 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 {
|
||||
AccessDeniedError,
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
} from "@lib/errors.ts";
|
||||
import { getSeries, Series } from "@lib/resource/series.ts";
|
||||
|
||||
async function updateSeriesMetadata(
|
||||
name: string,
|
||||
metadata: Partial<Series["meta"]>,
|
||||
) {
|
||||
const docId = `Media/series/${name}.md`;
|
||||
|
||||
console.log({ docId, metadata });
|
||||
|
||||
let currentDoc = await getDocument(docId);
|
||||
if (!currentDoc) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
if (!currentDoc.startsWith("---\n---\n")) {
|
||||
currentDoc = `---\n---\n\n${currentDoc}`;
|
||||
}
|
||||
|
||||
const newDoc = transformDocument(currentDoc, (root) => {
|
||||
const frontmatterNode = root.children.find((c) => c.type === "yaml");
|
||||
|
||||
const frontmatter = frontmatterNode?.value as string;
|
||||
|
||||
const value = parse(frontmatter) as Series["meta"];
|
||||
|
||||
const newValue = {
|
||||
...metadata,
|
||||
date: formatDate(metadata.date),
|
||||
...value,
|
||||
};
|
||||
|
||||
frontmatterNode.value = stringify(newValue);
|
||||
|
||||
return root;
|
||||
});
|
||||
|
||||
console.log(newDoc);
|
||||
|
||||
return createDocument(docId, newDoc);
|
||||
}
|
||||
|
||||
const POST = async (
|
||||
req: Request,
|
||||
ctx: HandlerContext,
|
||||
): Promise<Response> => {
|
||||
const session = ctx.state.session;
|
||||
if (!session) {
|
||||
throw new AccessDeniedError();
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const name = ctx.params.name;
|
||||
const { tmdbId } = body;
|
||||
if (!name || !tmdbId) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
|
||||
const series = await getSeries(ctx.params.name);
|
||||
if (!series) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const seriesDetails = await tmdb.getSeries(tmdbId);
|
||||
const seriesCredits = !series.meta.author &&
|
||||
await tmdb.getSeriesCredits(tmdbId);
|
||||
|
||||
const releaseDate = seriesDetails.first_air_date;
|
||||
const posterPath = seriesDetails.poster_path;
|
||||
const director = seriesCredits &&
|
||||
seriesCredits.crew?.filter?.((person) => person.job === "Director")[0];
|
||||
|
||||
let finalPath = "";
|
||||
if (posterPath && !series.meta.image) {
|
||||
const poster = await tmdb.getMoviePoster(posterPath);
|
||||
const extension = fileExtension(posterPath);
|
||||
|
||||
finalPath = `Media/series/images/${safeFileName(name)}_cover.${extension}`;
|
||||
await createDocument(finalPath, poster);
|
||||
}
|
||||
|
||||
const metadata = {} as Series["meta"];
|
||||
if (releaseDate) {
|
||||
metadata.date = new Date(releaseDate);
|
||||
}
|
||||
if (finalPath) {
|
||||
metadata.image = finalPath;
|
||||
}
|
||||
if (director && director.name) {
|
||||
metadata.author = director.name;
|
||||
}
|
||||
|
||||
await updateSeriesMetadata(name, metadata);
|
||||
|
||||
return json(seriesCredits);
|
||||
};
|
||||
|
||||
export const handler: Handlers = {
|
||||
POST,
|
||||
};
|
@ -6,6 +6,7 @@ import { getAllSeries, Series } from "@lib/resource/series.ts";
|
||||
import { Card } from "@components/Card.tsx";
|
||||
import { RedirectSearchHandler } from "@islands/Search.tsx";
|
||||
import { KMenu } from "@islands/KMenu.tsx";
|
||||
import { SeriesCard } from "@components/MovieCard.tsx";
|
||||
|
||||
export const handler: Handlers<Series[] | null> = {
|
||||
async GET(_, ctx) {
|
||||
@ -32,13 +33,7 @@ export default function Greet(props: PageProps<Series[] | null>) {
|
||||
</header>
|
||||
<Grid>
|
||||
{props.data?.map((doc) => {
|
||||
return (
|
||||
<Card
|
||||
image={doc?.meta?.image || "/placeholder.svg"}
|
||||
link={`/series/${doc.id}`}
|
||||
title={doc.name}
|
||||
/>
|
||||
);
|
||||
return <SeriesCard series={doc} />;
|
||||
})}
|
||||
</Grid>
|
||||
</MainLayout>
|
||||
|
Loading…
x
Reference in New Issue
Block a user