Add watchstatus create/edit apis

This commit is contained in:
Zoe Roux 2025-04-07 00:40:03 +02:00
parent 3f5af4b7fa
commit 83d8462003
No known key found for this signature in database

View File

@ -1,11 +1,16 @@
import { and, isNotNull, isNull } from "drizzle-orm"; import { type SQL, and, eq, isNotNull, isNull, sql } from "drizzle-orm";
import Elysia, { t } from "elysia"; import Elysia, { t } from "elysia";
import { auth, getUserInfo } from "~/auth"; import { auth, getUserInfo } from "~/auth";
import { shows } from "~/db/schema"; import { db } from "~/db";
import { profiles, shows } from "~/db/schema";
import { watchlist } from "~/db/schema/watchlist";
import { conflictUpdateAllExcept } from "~/db/utils";
import { KError } from "~/models/error"; import { KError } from "~/models/error";
import { bubble, madeInAbyss } from "~/models/examples";
import { Show } from "~/models/show"; import { Show } from "~/models/show";
import { import {
AcceptLanguage, AcceptLanguage,
DbMetadata,
Filter, Filter,
Page, Page,
createPage, createPage,
@ -13,111 +18,216 @@ import {
processLanguages, processLanguages,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; import { desc } from "~/models/utils/descriptions";
import { WatchStatus } from "~/models/watchlist";
import { getShows, showFilters, showSort, watchStatusQ } from "./shows/logic"; import { getShows, showFilters, showSort, watchStatusQ } from "./shows/logic";
async function setWatchStatus({
showFilter,
status,
userId,
}: {
showFilter?: SQL;
status: Omit<WatchStatus, "percent">;
userId: string;
}) {
const profileQ = db
.select({ pk: profiles.pk })
.from(profiles)
.where(eq(profiles.id, userId))
.as("profileQ");
const showQ = db
.select({ pk: shows.pk })
.from(shows)
.where(showFilter)
.as("showQ");
return await db
.insert(watchlist)
.values({
...status,
profilePk: sql`${profileQ}`,
showPk: sql`${showQ}`,
})
.onConflictDoUpdate({
target: [watchlist.profilePk, watchlist.showPk],
set: {
...conflictUpdateAllExcept(watchlist, [
"profilePk",
"showPk",
"createdAt",
]),
},
})
.returning();
}
export const watchlistH = new Elysia({ tags: ["profiles"] }) export const watchlistH = new Elysia({ tags: ["profiles"] })
.use(auth) .use(auth)
.guard({ .guard(
query: t.Object({ {
sort: showSort, query: t.Object({
filter: t.Optional(Filter({ def: showFilters })), sort: showSort,
query: t.Optional(t.String({ description: desc.query })), filter: t.Optional(Filter({ def: showFilters })),
limit: t.Integer({ query: t.Optional(t.String({ description: desc.query })),
minimum: 1, limit: t.Integer({
maximum: 250, minimum: 1,
default: 50, maximum: 250,
description: "Max page size.", default: 50,
}), description: "Max page size.",
after: t.Optional(t.String({ description: desc.after })),
preferOriginal: t.Optional(
t.Boolean({
description: desc.preferOriginal,
}), }),
), after: t.Optional(t.String({ description: desc.after })),
}), preferOriginal: t.Optional(
response: { t.Boolean({
200: Page(Show), description: desc.preferOriginal,
422: KError, }),
},
})
.get(
"/profiles/me/watchlist",
async ({
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages },
request: { url },
jwt: { sub },
}) => {
const langs = processLanguages(languages);
const items = await getShows({
limit,
after,
query,
sort,
filter: and(
isNotNull(watchStatusQ.status),
isNull(shows.collectionPk),
filter,
), ),
languages: langs, }),
preferOriginal, response: {
userId: sub, 200: Page(Show),
}); 422: KError,
return createPage(items, { url, sort, limit }); },
}, },
{ (app) =>
detail: { description: "Get all movies/series in your watchlist" }, app
headers: t.Object( .get(
{ "/profiles/me/watchlist",
"accept-language": AcceptLanguage({ autoFallback: true }), async ({
}, query: { limit, after, query, sort, filter, preferOriginal },
{ additionalProperties: true }, headers: { "accept-language": languages },
), request: { url },
}, jwt: { sub },
) }) => {
.get( const langs = processLanguages(languages);
"/profiles/:id/watchlist", const items = await getShows({
async ({ limit,
params: { id }, after,
query: { limit, after, query, sort, filter, preferOriginal }, query,
headers: { "accept-language": languages, authorization }, sort,
request: { url }, filter: and(
}) => { isNotNull(watchStatusQ.status),
if (!isUuid(id)) { isNull(shows.collectionPk),
const uInfo = await getUserInfo(id, { authorization }); filter,
id = uInfo.id; ),
} languages: langs,
preferOriginal,
userId: sub,
});
return createPage(items, { url, sort, limit });
},
{
detail: { description: "Get all movies/series in your watchlist" },
headers: t.Object(
{
"accept-language": AcceptLanguage({ autoFallback: true }),
},
{ additionalProperties: true },
),
},
)
.get(
"/profiles/:id/watchlist",
async ({
params: { id },
query: { limit, after, query, sort, filter, preferOriginal },
headers: { "accept-language": languages, authorization },
request: { url },
}) => {
if (!isUuid(id)) {
const uInfo = await getUserInfo(id, { authorization });
id = uInfo.id;
}
const langs = processLanguages(languages); const langs = processLanguages(languages);
const items = await getShows({ const items = await getShows({
limit, limit,
after, after,
query, query,
sort, sort,
filter: and( filter: and(
isNotNull(watchStatusQ.status), isNotNull(watchStatusQ.status),
isNull(shows.collectionPk), isNull(shows.collectionPk),
filter, filter,
),
languages: langs,
preferOriginal,
userId: id,
});
return createPage(items, { url, sort, limit });
},
{
detail: {
description: "Get all movies/series in someone's watchlist",
},
params: t.Object({
id: t.String({
description:
"The id or username of the user to read the watchlist of",
example: "zoriya",
}),
}),
headers: t.Object({
authorization: t.TemplateLiteral("Bearer ${string}"),
"accept-language": AcceptLanguage({ autoFallback: true }),
}),
permissions: ["users.read"],
},
), ),
languages: langs, )
preferOriginal, .post(
userId: id, "/series/:id/watchstatus",
async ({ params: { id }, body, jwt: { sub } }) => {
return await setWatchStatus({
showFilter: and(
eq(shows.kind, "serie"),
isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id),
),
userId: sub,
status: body,
}); });
return createPage(items, { url, sort, limit });
}, },
{ {
detail: { description: "Get all movies/series in someone's watchlist" }, detail: { description: "Set watchstatus of a series." },
params: t.Object({ params: t.Object({
id: t.String({ id: t.String({
description: description: "The id or slug of the serie.",
"The id or username of the user to read the watchlist of", example: madeInAbyss.slug,
example: "zoriya",
}), }),
}), }),
headers: t.Object({ body: t.Omit(WatchStatus, ["percent"]),
authorization: t.TemplateLiteral("Bearer ${string}"), response: {
"accept-language": AcceptLanguage({ autoFallback: true }), 201: t.Union([t.Omit(WatchStatus, ["percent"]), DbMetadata]),
},
permissions: ["core.read"],
},
)
.post(
"/movies/:id/watchstatus",
async ({ params: { id }, body, jwt: { sub } }) => {
return await setWatchStatus({
showFilter: and(
eq(shows.kind, "movie"),
isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id),
),
userId: sub,
// for movies, watch-percent is stored in `seenCount`.
status: { ...body, seenCount: body.status === "completed" ? 100 : 0 },
});
},
{
detail: { description: "Set watchstatus of a movie." },
params: t.Object({
id: t.String({
description: "The id or slug of the movie.",
example: bubble.slug,
}),
}), }),
permissions: ["users.read"], body: t.Omit(WatchStatus, ["seenCount", "percent"]),
response: {
201: t.Union([
t.Omit(WatchStatus, ["seenCount", "percent"]),
DbMetadata,
]),
},
permissions: ["core.read"],
}, },
); );