mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
fix(discovery): exclude rated and watched titles from recommendations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+76
-8
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
Reference in new issue
Block a user