Fix nextEntry & lastPlayedAt calculations on history/watchlist

This commit is contained in:
Zoe Roux 2025-04-08 16:31:09 +02:00
parent 67d7643261
commit 09dd78b272
No known key found for this signature in database
3 changed files with 74 additions and 39 deletions

View File

@ -181,6 +181,12 @@ export const historyH = new Elysia({ tags: ["profiles"] })
const vals = values( const vals = values(
body.map((x) => ({ ...x, entryUseId: isUuid(x.entry) })), body.map((x) => ({ ...x, entryUseId: isUuid(x.entry) })),
).as("hist"); ).as("hist");
const valEqEntries = sql`
case
when hist.entryUseId::boolean then ${entries.id} = hist.entry::uuid
else ${entries.slug} = hist.entry
end
`;
const rows = await db const rows = await db
.insert(history) .insert(history)
@ -195,19 +201,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
playedDate: sql`hist.playedDate::timestamptz`, playedDate: sql`hist.playedDate::timestamptz`,
}) })
.from(vals) .from(vals)
.innerJoin( .innerJoin(entries, valEqEntries)
entries,
or(
and(
sql`hist.entryUseId::boolean`,
eq(entries.id, sql`hist.entry::uuid`),
),
and(
not(sql`hist.entryUseId::boolean`),
eq(entries.slug, sql`hist.entry`),
),
),
)
.leftJoin(videos, eq(videos.id, sql`hist.videoId::uuid`)), .leftJoin(videos, eq(videos.id, sql`hist.videoId::uuid`)),
) )
.returning({ pk: history.pk }); .returning({ pk: history.pk });
@ -273,9 +267,15 @@ export const historyH = new Elysia({ tags: ["profiles"] })
else 0 else 0
end end
`, `,
nextEntry: nextEntryQ.pk, nextEntry: sql`
case
when hist.percent::integer >= 95 then ${nextEntryQ.pk}
else ${entries.pk}
end
`,
score: sql`null`, score: sql`null`,
startedAt: sql`hist.playedDate::timestamptz`, startedAt: sql`hist.playedDate::timestamptz`,
lastPlayedAt: sql`hist.playedDate::timestamptz`,
completedAt: sql` completedAt: sql`
case case
when ${nextEntryQ.pk} is null then hist.playedDate::timestamptz when ${nextEntryQ.pk} is null then hist.playedDate::timestamptz
@ -286,19 +286,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
updatedAt: sql`now()`, updatedAt: sql`now()`,
}) })
.from(vals) .from(vals)
.leftJoin( .leftJoin(entries, valEqEntries)
entries,
or(
and(
sql`hist.entryUseId::boolean`,
eq(entries.id, sql`hist.entry::uuid`),
),
and(
not(sql`hist.entryUseId::boolean`),
eq(entries.slug, sql`hist.entry`),
),
),
)
.leftJoinLateral(nextEntryQ, sql`true`), .leftJoinLateral(nextEntryQ, sql`true`),
) )
.onConflictDoUpdate({ .onConflictDoUpdate({
@ -321,6 +309,7 @@ export const historyH = new Elysia({ tags: ["profiles"] })
else excluded.next_entry else excluded.next_entry
end end
`, `,
lastPlayedAt: sql`excluded.last_played_at`,
completedAt: coalesce( completedAt: coalesce(
watchlist.completedAt, watchlist.completedAt,
sql`excluded.completed_at`, sql`excluded.completed_at`,

View File

@ -8,7 +8,7 @@ import {
watchStatusQ, watchStatusQ,
} from "~/controllers/shows/logic"; } from "~/controllers/shows/logic";
import { db } from "~/db"; import { db } from "~/db";
import { shows } from "~/db/schema"; import { entries, shows } from "~/db/schema";
import { watchlist } from "~/db/schema/watchlist"; import { watchlist } from "~/db/schema/watchlist";
import { conflictUpdateAllExcept, getColumns } from "~/db/utils"; import { conflictUpdateAllExcept, getColumns } from "~/db/utils";
import { KError } from "~/models/error"; import { KError } from "~/models/error";
@ -32,18 +32,39 @@ async function setWatchStatus({
status, status,
userId, userId,
}: { }: {
show: { pk: number; kind: "movie" | "serie" }; show:
status: SerieWatchStatus; | { pk: number; kind: "movie" }
| { pk: number; kind: "serie"; entriesCount: number };
status: Omit<SerieWatchStatus, "seenCount">;
userId: string; userId: string;
}) { }) {
const profilePk = await getOrCreateProfile(userId); const profilePk = await getOrCreateProfile(userId);
const firstEntryQ = db
.select({ pk: entries.pk })
.from(entries)
.where(eq(entries.showPk, show.pk))
.orderBy(entries.order)
.limit(1);
const [ret] = await db const [ret] = await db
.insert(watchlist) .insert(watchlist)
.values({ .values({
...status, ...status,
profilePk: profilePk, profilePk: profilePk,
seenCount:
status.status === "completed"
? show.kind === "movie"
? 100
: show.entriesCount
: 0,
showPk: show.pk, showPk: show.pk,
nextEntry:
show.kind === "movie" &&
(status.status === "watching" || status.status === "rewatching")
? sql`${firstEntryQ}`
: sql`null`,
lastPlayedAt: status.startedAt,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: [watchlist.profilePk, watchlist.showPk], target: [watchlist.profilePk, watchlist.showPk],
@ -53,10 +74,32 @@ async function setWatchStatus({
"showPk", "showPk",
"createdAt", "createdAt",
"seenCount", "seenCount",
"nextEntry",
"lastPlayedAt",
]), ]),
// do not reset movie's progress during drop ...(status.status === "completed"
...(show.kind === "movie" && status.status !== "dropped" ? {
? { seenCount: sql`excluded.seen_count` } seenCount: sql`excluded.seen_count`,
nextEntry: sql`null`,
}
: {}),
// only set seenCount & nextEntry when marking as "rewatching"
// if it's already rewatching, the history updates are more up-dated.
...(status.status === "rewatching"
? {
seenCount: sql`
case when ${watchlist.status} != 'rewatching'
then excluded.seen_count
else
${watchlist.seenCount}
end`,
nextEntry: sql`
case when ${watchlist.status} != 'rewatching'
then excluded.next_entry
else
${watchlist.nextEntry}
end`,
}
: {}), : {}),
}, },
}) })
@ -115,6 +158,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
), ),
languages: langs, languages: langs,
preferOriginal: preferOriginal ?? settings.preferOriginal, preferOriginal: preferOriginal ?? settings.preferOriginal,
relations: ["nextEntry"],
userId: sub, userId: sub,
}); });
return createPage(items, { url, sort, limit }); return createPage(items, { url, sort, limit });
@ -159,6 +203,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
), ),
languages: langs, languages: langs,
preferOriginal: preferOriginal ?? settings.preferOriginal, preferOriginal: preferOriginal ?? settings.preferOriginal,
relations: ["nextEntry"],
userId: uInfo.id, userId: uInfo.id,
}); });
return createPage(items, { url, sort, limit }); return createPage(items, { url, sort, limit });
@ -195,7 +240,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
"/series/:id/watchstatus", "/series/:id/watchstatus",
async ({ params: { id }, body, jwt: { sub }, error }) => { async ({ params: { id }, body, jwt: { sub }, error }) => {
const [show] = await db const [show] = await db
.select({ pk: shows.pk }) .select({ pk: shows.pk, entriesCount: shows.entriesCount })
.from(shows) .from(shows)
.where( .where(
and( and(
@ -211,7 +256,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
}); });
} }
return await setWatchStatus({ return await setWatchStatus({
show: { pk: show.pk, kind: "serie" }, show: { pk: show.pk, kind: "serie", entriesCount: show.entriesCount },
userId: sub, userId: sub,
status: body, status: body,
}); });
@ -224,7 +269,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
example: madeInAbyss.slug, example: madeInAbyss.slug,
}), }),
}), }),
body: SerieWatchStatus, body: t.Omit(SerieWatchStatus, ["seenCount"]),
response: { response: {
200: t.Intersect([SerieWatchStatus, DbMetadata]), 200: t.Intersect([SerieWatchStatus, DbMetadata]),
404: KError, 404: KError,
@ -258,8 +303,6 @@ export const watchlistH = new Elysia({ tags: ["profiles"] })
status: { status: {
...body, ...body,
startedAt: body.completedAt, startedAt: body.completedAt,
// for movies, watch-percent is stored in `seenCount`.
seenCount: body.status === "completed" ? 100 : 0,
}, },
}); });
}, },

View File

@ -184,7 +184,10 @@ export const getNews = async ({
return [resp, body] as const; return [resp, body] as const;
}; };
export const setSerieStatus = async (id: string, status: SerieWatchStatus) => { export const setSerieStatus = async (
id: string,
status: Omit<SerieWatchStatus, "seenCount">,
) => {
const resp = await app.handle( const resp = await app.handle(
new Request(buildUrl(`series/${id}/watchstatus`), { new Request(buildUrl(`series/${id}/watchstatus`), {
method: "POST", method: "POST",