mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(settings): derive the recommendations seed heading from the visible cards
The rail kept only the seeds that contributed a card to the original selection, so a seed whose recommendations filled in after a relock (or an offline prune) was missing from the "Because you watched" heading. Every seed of the load is kept in order and the heading lists those that contribute a card now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fb8830584e
commit
45a42ed57d
2 files changed
+43
-19
No files matched your search
+32
@@ -1044,4 +1044,36 @@ describe('DashboardRecommendationsService', () => {
|
||||
|
||||
expect(service.seedTitles()).toEqual(['The Matrix']);
|
||||
});
|
||||
|
||||
it('derives the seed heading from the cards visible now', async () => {
|
||||
recentVod = [
|
||||
{ title: 'The Matrix', type: 'movie' },
|
||||
{ title: 'Blade Runner', type: 'movie' },
|
||||
];
|
||||
recentAll = [...recentVod];
|
||||
const second = ['Akira', 'Ghost in the Shell', 'Paprika'];
|
||||
enrichMovie.mockImplementation(async (query: { title: string }) => ({
|
||||
recommendations: {
|
||||
results:
|
||||
query.title === 'The Matrix'
|
||||
? recTitles.map((t, i) => rec(100 + i, t))
|
||||
: second.map((t, i) => rec(300 + i, t)),
|
||||
},
|
||||
}));
|
||||
// The second seed's recommendations sit in category 8.
|
||||
matchTitles.mockImplementation(async (titles: string[]) =>
|
||||
titles.map((title) =>
|
||||
match(title, second.includes(title) ? { categoryId: 8 } : {})
|
||||
)
|
||||
);
|
||||
// Locked at load: the second seed has no visible card yet.
|
||||
withheldCategories.set(new Set([8]));
|
||||
const service = createService();
|
||||
await service.load();
|
||||
expect(service.seedTitles()).toEqual(['The Matrix']);
|
||||
|
||||
// Unlocked: its cards appear, and so does its heading.
|
||||
withheldCategories.set(new Set());
|
||||
expect(service.seedTitles()).toEqual(['The Matrix', 'Blade Runner']);
|
||||
});
|
||||
});
|
||||
@@ -70,7 +70,12 @@ export class DashboardRecommendationsService {
|
||||
|
||||
/** The last load's candidates and ALL their catalog matches. */
|
||||
private readonly loaded = signal<LoadedRecommendations>(EMPTY_LOAD);
|
||||
private readonly matchedSeedTitles = signal<readonly string[]>([]);
|
||||
/**
|
||||
* Every seed of the last load, most recent first. `seedTitles` keeps
|
||||
* those that contribute a card NOW: a relock can hide some cards and let
|
||||
* another seed's recommendations fill their places.
|
||||
*/
|
||||
private readonly seedOrder = signal<readonly string[]>([]);
|
||||
/**
|
||||
* The cards, built on READ from the matches the parental lock does not
|
||||
* withhold: a relock hides a card matched while unlocked, and a title
|
||||
@@ -89,9 +94,7 @@ export class DashboardRecommendationsService {
|
||||
/** Seeds that contributed at least one visible card, most recent first */
|
||||
readonly seedTitles = computed(() => {
|
||||
const contributed = new Set(this.items().map((item) => item.seedTitle));
|
||||
return this.matchedSeedTitles().filter((title) =>
|
||||
contributed.has(title)
|
||||
);
|
||||
return this.seedOrder().filter((title) => contributed.has(title));
|
||||
});
|
||||
readonly loading = signal(false);
|
||||
|
||||
@@ -117,7 +120,7 @@ export class DashboardRecommendationsService {
|
||||
// The service outlives the dashboard (root-provided), so a
|
||||
// cleared watch history must clear the rail too.
|
||||
this.loaded.set(EMPTY_LOAD);
|
||||
this.matchedSeedTitles.set([]);
|
||||
this.seedOrder.set([]);
|
||||
this.loadedKey = null;
|
||||
return;
|
||||
}
|
||||
@@ -161,15 +164,8 @@ export class DashboardRecommendationsService {
|
||||
MAX_ITEMS
|
||||
);
|
||||
if (matched.length >= MIN_RECOMMENDATION_MATCHES) {
|
||||
const contributed = new Set(
|
||||
matched.map((item) => item.seedTitle)
|
||||
);
|
||||
this.loaded.set(load);
|
||||
this.matchedSeedTitles.set(
|
||||
perSeed
|
||||
.map((seed) => seed.seedTitle)
|
||||
.filter((title) => contributed.has(title))
|
||||
);
|
||||
this.seedOrder.set(perSeed.map((seed) => seed.seedTitle));
|
||||
// Latch only once EVERY seed answered. A seed that did
|
||||
// not resolve may have failed transiently, and latching
|
||||
// on its behalf would drop its recommendations for the
|
||||
@@ -193,7 +189,7 @@ export class DashboardRecommendationsService {
|
||||
// exact inputs (say, un-favoriting again) would
|
||||
// otherwise hit the equality guard and stay empty.
|
||||
this.loaded.set(EMPTY_LOAD);
|
||||
this.matchedSeedTitles.set([]);
|
||||
this.seedOrder.set([]);
|
||||
this.loadedKey = null;
|
||||
}
|
||||
}
|
||||
@@ -253,15 +249,11 @@ export class DashboardRecommendationsService {
|
||||
);
|
||||
if (cards.length < MIN_RECOMMENDATION_MATCHES) {
|
||||
this.loaded.set(EMPTY_LOAD);
|
||||
this.matchedSeedTitles.set([]);
|
||||
this.seedOrder.set([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const contributed = new Set(cards.map((item) => item.seedTitle));
|
||||
this.loaded.set(kept);
|
||||
this.matchedSeedTitles.set(
|
||||
this.matchedSeedTitles().filter((title) => contributed.has(title))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user