mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 23:55:43 -04:00
fix(tracking): skip watched episodes in bulk marks; read ratings without status
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+73
-15
No files matched your search
@@ -29,6 +29,7 @@ import {
|
||||
unwatchMovie,
|
||||
unwatchSeason,
|
||||
unwatchSeries,
|
||||
watchSeason,
|
||||
} from "../src/tracking";
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -297,6 +298,20 @@ describe("logEpisodeWatchBatch", () => {
|
||||
expect(row?.status).toBe("in_progress");
|
||||
});
|
||||
|
||||
test("skips episodes that are already watched", () => {
|
||||
insertUser();
|
||||
const { episodeIds } = insertTvShow("tv-1", 99999, 1, 3);
|
||||
logEpisodeWatch("user-1", episodeIds[0]);
|
||||
logEpisodeWatchBatch("user-1", episodeIds);
|
||||
|
||||
const watches = testDb
|
||||
.select()
|
||||
.from(userEpisodeWatches)
|
||||
.where(eq(userEpisodeWatches.userId, "user-1"))
|
||||
.all();
|
||||
expect(watches).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("no-op for empty array", () => {
|
||||
insertUser();
|
||||
logEpisodeWatchBatch("user-1", []);
|
||||
@@ -310,6 +325,26 @@ describe("logEpisodeWatchBatch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── watchSeason ─────────────────────────────────────────────────────
|
||||
|
||||
describe("watchSeason", () => {
|
||||
test("does not duplicate already-watched episodes", () => {
|
||||
insertUser();
|
||||
const { episodeIds } = insertTvShow("tv-1", 99999, 1, 3);
|
||||
logEpisodeWatch("user-1", episodeIds[0]);
|
||||
logEpisodeWatch("user-1", episodeIds[1]);
|
||||
watchSeason("user-1", "tv-1-s1");
|
||||
|
||||
const watches = testDb
|
||||
.select()
|
||||
.from(userEpisodeWatches)
|
||||
.where(eq(userEpisodeWatches.userId, "user-1"))
|
||||
.all();
|
||||
expect(watches).toHaveLength(3);
|
||||
expect(new Set(watches.map((w) => w.episodeId)).size).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ── markAllEpisodesWatched ──────────────────────────────────────────
|
||||
|
||||
describe("markAllEpisodesWatched", () => {
|
||||
@@ -647,6 +682,16 @@ describe("getUserTitleInfo", () => {
|
||||
expect(info.episodeWatches).toContain(episodeIds[1]);
|
||||
});
|
||||
|
||||
test("returns a rating for a title not in the library", () => {
|
||||
insertUser();
|
||||
insertTitle();
|
||||
rateTitleStars("user-1", "title-1", 4);
|
||||
|
||||
const info = getUserTitleInfo("user-1", "title-1");
|
||||
expect(info.rating).toBe(4);
|
||||
expect(info.status).toBeNull();
|
||||
});
|
||||
|
||||
test("returns nulls when no data exists", () => {
|
||||
insertUser();
|
||||
insertTitle();
|
||||
|
||||
@@ -95,7 +95,23 @@ export function batchInsertEpisodeWatchesTransaction(
|
||||
db.transaction((tx) => {
|
||||
const now = watchedAt ?? new Date();
|
||||
|
||||
for (const episodeId of episodeIds) {
|
||||
// Bulk marking records state, not rewatches (single-episode logEpisodeWatch is the rewatch path).
|
||||
const alreadyWatched = new Set(
|
||||
tx
|
||||
.select({ episodeId: userEpisodeWatches.episodeId })
|
||||
.from(userEpisodeWatches)
|
||||
.where(
|
||||
and(
|
||||
eq(userEpisodeWatches.userId, userId),
|
||||
inArray(userEpisodeWatches.episodeId, episodeIds),
|
||||
),
|
||||
)
|
||||
.all()
|
||||
.map((w) => w.episodeId),
|
||||
);
|
||||
|
||||
for (const episodeId of new Set(episodeIds)) {
|
||||
if (alreadyWatched.has(episodeId)) continue;
|
||||
tx.insert(userEpisodeWatches).values({ userId, episodeId, watchedAt: now, source }).run();
|
||||
}
|
||||
|
||||
@@ -254,22 +270,19 @@ export function getEpisodeProgressByTitleIds(userId: string, titleIds: string[])
|
||||
}
|
||||
|
||||
export function getUserTitleInfo(userId: string, titleId: string) {
|
||||
const info = db
|
||||
.select({
|
||||
status: userTitleStatus.status,
|
||||
ratingStars: userRatings.ratingStars,
|
||||
})
|
||||
const statusRow = db
|
||||
.select({ status: userTitleStatus.status })
|
||||
.from(userTitleStatus)
|
||||
.leftJoin(
|
||||
userRatings,
|
||||
and(
|
||||
eq(userRatings.userId, userTitleStatus.userId),
|
||||
eq(userRatings.titleId, userTitleStatus.titleId),
|
||||
),
|
||||
)
|
||||
.where(and(eq(userTitleStatus.userId, userId), eq(userTitleStatus.titleId, titleId)))
|
||||
.get();
|
||||
|
||||
// Ratings exist independently of library membership (rateTitleStars doesn't add a status row).
|
||||
const ratingRow = db
|
||||
.select({ ratingStars: userRatings.ratingStars })
|
||||
.from(userRatings)
|
||||
.where(and(eq(userRatings.userId, userId), eq(userRatings.titleId, titleId)))
|
||||
.get();
|
||||
|
||||
const watchedEpisodeIds = db
|
||||
.selectDistinct({ episodeId: userEpisodeWatches.episodeId })
|
||||
.from(userEpisodeWatches)
|
||||
@@ -280,8 +293,8 @@ export function getUserTitleInfo(userId: string, titleId: string) {
|
||||
.map((w) => w.episodeId);
|
||||
|
||||
return {
|
||||
status: info?.status ?? null,
|
||||
rating: info?.ratingStars ?? null,
|
||||
status: statusRow?.status ?? null,
|
||||
rating: ratingRow?.ratingStars ?? null,
|
||||
episodeWatches: watchedEpisodeIds,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user