mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add disinterest tag filter stage
This commit is contained in:
1 parent
abf6c003cc
commit
4abe9f3d1e
4 files changed
+139
-99
No files matched your search
@@ -68,6 +68,8 @@ constructed today behave exactly like the existing UI.
|
||||
| --- | --- |
|
||||
| `createDedupeByRootStage` | Reuses the application’s `dedupeVideosByRoot` helper (falling back to `dedupeToNewestByRoot`) to drop older versions of the same videoRoot. Adds "why" metadata for removed entries. |
|
||||
| `createBlacklistFilterStage` | Calls `nostrService.shouldIncludeVideo(...)` so moderators and block lists stay enforced. Each rejection logs a "blacklist" reason in the why-trail. |
|
||||
| `createTagPreferenceFilterStage` | Normalizes interest/disinterest tags via `normalizeHashtag`, drops videos that match disinterests, and (by default) filters out videos that do not match interest tags. Set `enforceInterests: false` to keep items even when they lack interest tags. |
|
||||
| `createDisinterestFilterStage` | Normalizes tag preferences via `normalizeHashtag` and drops videos that match disinterests without excluding items that lack interest tags (useful for Explore-style feeds). |
|
||||
| `createWatchHistorySuppressionStage` | Invokes feed-provided hooks to optionally suppress watched items. Useful for per-feed watch history preferences. |
|
||||
|
||||
Stages receive `(items, context)` and should return the transformed list. They
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
createFeedEngine,
|
||||
createActiveNostrSource,
|
||||
createBlacklistFilterStage,
|
||||
createDisinterestFilterStage,
|
||||
createDedupeByRootStage,
|
||||
createExploreDiversitySorter,
|
||||
createExploreScorerStage,
|
||||
@@ -5647,10 +5648,7 @@ class Application {
|
||||
return this.feedEngine.registerFeed("explore", {
|
||||
source: createActiveNostrSource({ service: this.nostrService }),
|
||||
stages: [
|
||||
createTagPreferenceFilterStage({
|
||||
stageName: "tag-disinterest-filter",
|
||||
matchInterests: false,
|
||||
}),
|
||||
createDisinterestFilterStage(),
|
||||
createBlacklistFilterStage({
|
||||
shouldIncludeVideo: (video, options) =>
|
||||
this.nostrService.shouldIncludeVideo(video, options),
|
||||
|
||||
@@ -4,6 +4,7 @@ export { createFeedEngine } from "./engine.js";
|
||||
export {
|
||||
createDedupeByRootStage,
|
||||
createBlacklistFilterStage,
|
||||
createDisinterestFilterStage,
|
||||
createTagPreferenceFilterStage,
|
||||
createWatchHistorySuppressionStage,
|
||||
createModerationStage,
|
||||
|
||||
+134
-95
@@ -275,107 +275,146 @@ export function createResolvePostedAtStage({
|
||||
|
||||
export function createTagPreferenceFilterStage({
|
||||
stageName = "tag-preference-filter",
|
||||
enforceInterests,
|
||||
matchInterests = true,
|
||||
} = {}) {
|
||||
const shouldMatchInterests =
|
||||
typeof enforceInterests === "boolean"
|
||||
? enforceInterests
|
||||
: matchInterests !== false;
|
||||
|
||||
return async function tagPreferenceFilterStage(items = [], context = {}) {
|
||||
const shouldMatchInterests = matchInterests !== false;
|
||||
const tagPreferences = context?.runtime?.tagPreferences;
|
||||
const interests = normalizeTagSet(tagPreferences?.interests);
|
||||
const disinterests = normalizeTagSet(tagPreferences?.disinterests);
|
||||
if (!interests.size && !disinterests.size) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
const video = item?.video;
|
||||
if (!video || typeof video !== "object") {
|
||||
results.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const videoTags = new Set();
|
||||
if (Array.isArray(video.tags)) {
|
||||
for (const tag of video.tags) {
|
||||
if (Array.isArray(tag) && tag[0] === "t" && typeof tag[1] === "string") {
|
||||
const normalized = normalizeHashtag(tag[1]);
|
||||
if (normalized) {
|
||||
videoTags.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(video.nip71?.hashtags)) {
|
||||
for (const tag of video.nip71.hashtags) {
|
||||
if (typeof tag === "string") {
|
||||
const normalized = normalizeHashtag(tag);
|
||||
if (normalized) {
|
||||
videoTags.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let disinterested = false;
|
||||
if (disinterests.size) {
|
||||
for (const tag of videoTags) {
|
||||
if (disinterests.has(tag)) {
|
||||
disinterested = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disinterested) {
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
reason: "disinterested-tag",
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let matchedInterests = [];
|
||||
if (interests.size) {
|
||||
matchedInterests = [...videoTags].filter((tag) => interests.has(tag));
|
||||
if (shouldMatchInterests && matchedInterests.length === 0) {
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
reason: "no-interest-match",
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedInterests.length > 0) {
|
||||
if (!isPlainObject(item.metadata)) {
|
||||
item.metadata = {};
|
||||
}
|
||||
item.metadata.matchedInterests = matchedInterests;
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
reason: "matched-interests",
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
|
||||
tags: matchedInterests,
|
||||
});
|
||||
}
|
||||
|
||||
results.push(item);
|
||||
}
|
||||
|
||||
return results;
|
||||
return filterByTagPreferences({
|
||||
items,
|
||||
context,
|
||||
stageName,
|
||||
enforceInterests: shouldMatchInterests,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createDisinterestFilterStage({
|
||||
stageName = "tag-disinterest-filter",
|
||||
} = {}) {
|
||||
return async function disinterestFilterStage(items = [], context = {}) {
|
||||
return filterByTagPreferences({
|
||||
items,
|
||||
context,
|
||||
stageName,
|
||||
enforceInterests: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function collectVideoTags(video) {
|
||||
const videoTags = new Set();
|
||||
|
||||
if (Array.isArray(video.tags)) {
|
||||
for (const tag of video.tags) {
|
||||
if (Array.isArray(tag) && tag[0] === "t" && typeof tag[1] === "string") {
|
||||
const normalized = normalizeHashtag(tag[1]);
|
||||
if (normalized) {
|
||||
videoTags.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(video.nip71?.hashtags)) {
|
||||
for (const tag of video.nip71.hashtags) {
|
||||
if (typeof tag === "string") {
|
||||
const normalized = normalizeHashtag(tag);
|
||||
if (normalized) {
|
||||
videoTags.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return videoTags;
|
||||
}
|
||||
|
||||
function filterByTagPreferences({
|
||||
items = [],
|
||||
context = {},
|
||||
stageName,
|
||||
enforceInterests,
|
||||
}) {
|
||||
const tagPreferences = context?.runtime?.tagPreferences;
|
||||
const interests = normalizeTagSet(tagPreferences?.interests);
|
||||
const disinterests = normalizeTagSet(tagPreferences?.disinterests);
|
||||
if (!interests.size && !disinterests.size) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
const video = item?.video;
|
||||
if (!video || typeof video !== "object") {
|
||||
results.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const videoTags = collectVideoTags(video);
|
||||
|
||||
let disinterested = false;
|
||||
if (disinterests.size) {
|
||||
for (const tag of videoTags) {
|
||||
if (disinterests.has(tag)) {
|
||||
disinterested = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (disinterested) {
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
reason: "disinterested-tag",
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let matchedInterests = [];
|
||||
if (interests.size) {
|
||||
matchedInterests = [...videoTags].filter((tag) => interests.has(tag));
|
||||
if (enforceInterests && matchedInterests.length === 0) {
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
reason: "no-interest-match",
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedInterests.length > 0) {
|
||||
if (!isPlainObject(item.metadata)) {
|
||||
item.metadata = {};
|
||||
}
|
||||
item.metadata.matchedInterests = matchedInterests;
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
reason: "matched-interests",
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : null,
|
||||
tags: matchedInterests,
|
||||
});
|
||||
}
|
||||
|
||||
results.push(item);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function createBlacklistFilterStage({
|
||||
stageName = "blacklist-filter",
|
||||
shouldIncludeVideo,
|
||||
|
||||
Reference in new issue
Block a user