fix(discovery): exclude rated and watched titles from recommendations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-06 12:26:36 -04:00
1 parent 4eadce286b
commit c4a322948b
3 files changed
+76 -8

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
import { localDateString } from "@sofa/config";
import {
getAllTrackedTitleIds,
getKnownTitleIds,
getAvailabilityByTitleIds,
getEngagedTitleIds,
getEpisodesBySeasonIds,
@@ -310,8 +310,8 @@ export function getRecommendationsFeed(userId: string) {
const sourceIds = [...new Set([...userCompletedOrRated, ...ratedIds])];
if (sourceIds.length === 0) return [];
// Get all tracked title IDs to exclude
const trackedIds = new Set(getAllTrackedTitleIds(userId));
// Exclude anything the user has tracked, rated or watched
const knownIds = getKnownTitleIds(userId);
// Batch fetch all recommendations for all source IDs (1 query)
const allRecRows = getRecommendationRows(sourceIds);
@@ -319,7 +319,7 @@ export function getRecommendationsFeed(userId: string) {
const recs: Map<string, { titleId: string; score: number }> = new Map();
for (const rec of allRecRows) {
if (trackedIds.has(rec.recommendedTitleId)) continue;
if (knownIds.has(rec.recommendedTitleId)) continue;
const existing = recs.get(rec.recommendedTitleId);
const score = 100 - rec.rank;
if (existing) {
+45
View File
@@ -454,6 +454,51 @@ describe("getRecommendationsFeed", () => {
expect(feed).toHaveLength(0);
});
test("excludes rated titles that are not in the library", () => {
insertUser();
insertTitle({ id: "m1", tmdbId: 1 });
insertTitle({ id: "m2", tmdbId: 2 });
insertTitle({ id: "m3", tmdbId: 3 });
insertRating("user-1", "m1", 5);
insertRating("user-1", "m2", 5);
insertRecommendation("m1", "m2", { rank: 1 });
insertRecommendation("m1", "m3", { rank: 2 });
const ids = getRecommendationsFeed("user-1").map((t) => t?.id);
expect(ids).toContain("m3");
expect(ids).not.toContain("m2");
});
test("excludes watched movies that are not in the library", () => {
insertUser();
insertTitle({ id: "m1", tmdbId: 1 });
insertTitle({ id: "m2", tmdbId: 2 });
insertTitle({ id: "m3", tmdbId: 3 });
insertStatus("user-1", "m1", "completed");
insertMovieWatch("user-1", "m2");
insertRecommendation("m1", "m2", { rank: 1 });
insertRecommendation("m1", "m3", { rank: 2 });
const ids = getRecommendationsFeed("user-1").map((t) => t?.id);
expect(ids).toContain("m3");
expect(ids).not.toContain("m2");
});
test("excludes shows with watched episodes that are not in the library", () => {
insertUser();
insertTitle({ id: "m1", tmdbId: 1 });
insertTitle({ id: "m3", tmdbId: 3 });
const { episodeIds } = insertTvShow("tv-1", 99999, 1, 3);
insertStatus("user-1", "m1", "completed");
insertEpisodeWatch("user-1", episodeIds[0]!);
insertRecommendation("m1", "tv-1", { rank: 1 });
insertRecommendation("m1", "m3", { rank: 2 });
const ids = getRecommendationsFeed("user-1").map((t) => t?.id);
expect(ids).toContain("m3");
expect(ids).not.toContain("tv-1");
});
test("returns empty when no source titles", () => {
insertUser();
const feed = getRecommendationsFeed("user-1");
+27 -4
View File
@@ -222,13 +222,36 @@ export function getHighlyRatedTitleIds(userId: string) {
.map((r) => r.titleId);
}
export function getAllTrackedTitleIds(userId: string) {
return db
/** Title IDs the user has tracked, rated or watched (anything they already know). */
export function getKnownTitleIds(userId: string): Set<string> {
const ids = new Set<string>();
for (const r of db
.select({ titleId: userTitleStatus.titleId })
.from(userTitleStatus)
.where(eq(userTitleStatus.userId, userId))
.all()
.map((r) => r.titleId);
.all())
ids.add(r.titleId);
for (const r of db
.select({ titleId: userRatings.titleId })
.from(userRatings)
.where(eq(userRatings.userId, userId))
.all())
ids.add(r.titleId);
for (const r of db
.selectDistinct({ titleId: userMovieWatches.titleId })
.from(userMovieWatches)
.where(eq(userMovieWatches.userId, userId))
.all())
ids.add(r.titleId);
for (const r of db
.selectDistinct({ titleId: seasons.titleId })
.from(userEpisodeWatches)
.innerJoin(episodes, eq(episodes.id, userEpisodeWatches.episodeId))
.innerJoin(seasons, eq(seasons.id, episodes.seasonId))
.where(eq(userEpisodeWatches.userId, userId))
.all())
ids.add(r.titleId);
return ids;
}
export function getRecommendationRows(sourceIds: string[]) {