mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-16 01:05:42 -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,
|
unwatchMovie,
|
||||||
unwatchSeason,
|
unwatchSeason,
|
||||||
unwatchSeries,
|
unwatchSeries,
|
||||||
|
watchSeason,
|
||||||
} from "../src/tracking";
|
} from "../src/tracking";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -297,6 +298,20 @@ describe("logEpisodeWatchBatch", () => {
|
|||||||
expect(row?.status).toBe("in_progress");
|
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", () => {
|
test("no-op for empty array", () => {
|
||||||
insertUser();
|
insertUser();
|
||||||
logEpisodeWatchBatch("user-1", []);
|
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 ──────────────────────────────────────────
|
// ── markAllEpisodesWatched ──────────────────────────────────────────
|
||||||
|
|
||||||
describe("markAllEpisodesWatched", () => {
|
describe("markAllEpisodesWatched", () => {
|
||||||
@@ -647,6 +682,16 @@ describe("getUserTitleInfo", () => {
|
|||||||
expect(info.episodeWatches).toContain(episodeIds[1]);
|
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", () => {
|
test("returns nulls when no data exists", () => {
|
||||||
insertUser();
|
insertUser();
|
||||||
insertTitle();
|
insertTitle();
|
||||||
|
|||||||
@@ -95,7 +95,23 @@ export function batchInsertEpisodeWatchesTransaction(
|
|||||||
db.transaction((tx) => {
|
db.transaction((tx) => {
|
||||||
const now = watchedAt ?? new Date();
|
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();
|
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) {
|
export function getUserTitleInfo(userId: string, titleId: string) {
|
||||||
const info = db
|
const statusRow = db
|
||||||
.select({
|
.select({ status: userTitleStatus.status })
|
||||||
status: userTitleStatus.status,
|
|
||||||
ratingStars: userRatings.ratingStars,
|
|
||||||
})
|
|
||||||
.from(userTitleStatus)
|
.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)))
|
.where(and(eq(userTitleStatus.userId, userId), eq(userTitleStatus.titleId, titleId)))
|
||||||
.get();
|
.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
|
const watchedEpisodeIds = db
|
||||||
.selectDistinct({ episodeId: userEpisodeWatches.episodeId })
|
.selectDistinct({ episodeId: userEpisodeWatches.episodeId })
|
||||||
.from(userEpisodeWatches)
|
.from(userEpisodeWatches)
|
||||||
@@ -280,8 +293,8 @@ export function getUserTitleInfo(userId: string, titleId: string) {
|
|||||||
.map((w) => w.episodeId);
|
.map((w) => w.episodeId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: info?.status ?? null,
|
status: statusRow?.status ?? null,
|
||||||
rating: info?.ratingStars ?? null,
|
rating: ratingRow?.ratingStars ?? null,
|
||||||
episodeWatches: watchedEpisodeIds,
|
episodeWatches: watchedEpisodeIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user