mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add explore feed pipeline
This commit is contained in:
1 parent
bb96aabdae
commit
c3aa42f39c
5 files changed
+336
-32
No files matched your search
@@ -37,6 +37,8 @@ import {
|
||||
createActiveNostrSource,
|
||||
createBlacklistFilterStage,
|
||||
createDedupeByRootStage,
|
||||
createExploreDiversitySorter,
|
||||
createExploreScorerStage,
|
||||
createModerationStage,
|
||||
createResolvePostedAtStage,
|
||||
createTagPreferenceFilterStage,
|
||||
@@ -5603,6 +5605,83 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the "explore" feed pipeline.
|
||||
*/
|
||||
registerExploreFeed() {
|
||||
if (!this.feedEngine || typeof this.feedEngine.registerFeed !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingDefinition =
|
||||
typeof this.feedEngine.getFeedDefinition === "function"
|
||||
? this.feedEngine.getFeedDefinition("explore")
|
||||
: null;
|
||||
if (existingDefinition) {
|
||||
return existingDefinition;
|
||||
}
|
||||
|
||||
try {
|
||||
const app = this;
|
||||
const resolveThresholdFromApp = (key) => ({ runtimeValue, defaultValue }) => {
|
||||
if (
|
||||
Number.isFinite(runtimeValue) ||
|
||||
runtimeValue === Number.POSITIVE_INFINITY
|
||||
) {
|
||||
return runtimeValue;
|
||||
}
|
||||
|
||||
if (app && typeof app.getActiveModerationThresholds === "function") {
|
||||
const active = app.getActiveModerationThresholds();
|
||||
const candidate = active && typeof active === "object" ? active[key] : undefined;
|
||||
if (
|
||||
Number.isFinite(candidate) ||
|
||||
candidate === Number.POSITIVE_INFINITY
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
return this.feedEngine.registerFeed("explore", {
|
||||
source: createActiveNostrSource({ service: this.nostrService }),
|
||||
stages: [
|
||||
createTagPreferenceFilterStage({
|
||||
stageName: "tag-disinterest-filter",
|
||||
matchInterests: false,
|
||||
}),
|
||||
createBlacklistFilterStage({
|
||||
shouldIncludeVideo: (video, options) =>
|
||||
this.nostrService.shouldIncludeVideo(video, options),
|
||||
}),
|
||||
createDedupeByRootStage({
|
||||
dedupe: (videos) => this.dedupeVideosByRoot(videos),
|
||||
}),
|
||||
createModerationStage({
|
||||
getService: () => this.nostrService.getModerationService(),
|
||||
autoplayThreshold: resolveThresholdFromApp("autoplayBlockThreshold"),
|
||||
blurThreshold: resolveThresholdFromApp("blurThreshold"),
|
||||
trustedMuteHideThreshold: resolveThresholdFromApp("trustedMuteHideThreshold"),
|
||||
trustedReportHideThreshold: resolveThresholdFromApp("trustedSpamHideThreshold"),
|
||||
}),
|
||||
createResolvePostedAtStage(),
|
||||
createExploreScorerStage(),
|
||||
],
|
||||
sorter: createExploreDiversitySorter(),
|
||||
hooks: {
|
||||
timestamps: {
|
||||
getKnownVideoPostedAt: (video) => this.getKnownVideoPostedAt(video),
|
||||
resolveVideoPostedAt: (video) => this.resolveVideoPostedAt(video),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
devLogger.warn("[Application] Failed to register explore feed:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
registerSubscriptionsFeed() {
|
||||
if (!this.feedEngine || typeof this.feedEngine.registerFeed !== "function") {
|
||||
return null;
|
||||
@@ -5733,6 +5812,53 @@ class Application {
|
||||
};
|
||||
}
|
||||
|
||||
buildExploreFeedRuntime() {
|
||||
const blacklist =
|
||||
this.blacklistedEventIds instanceof Set
|
||||
? new Set(this.blacklistedEventIds)
|
||||
: new Set();
|
||||
|
||||
const preferenceSource =
|
||||
this.hashtagPreferencesSnapshot &&
|
||||
typeof this.hashtagPreferencesSnapshot === "object"
|
||||
? this.hashtagPreferencesSnapshot
|
||||
: typeof this.createHashtagPreferencesSnapshot === "function"
|
||||
? this.createHashtagPreferencesSnapshot()
|
||||
: typeof this.getHashtagPreferences === "function"
|
||||
? this.getHashtagPreferences()
|
||||
: {};
|
||||
const { interests = [], disinterests = [] } = preferenceSource || {};
|
||||
const moderationThresholds = this.getActiveModerationThresholds();
|
||||
|
||||
const watchHistoryTagCounts =
|
||||
this.watchHistoryTagCounts instanceof Map
|
||||
? new Map(this.watchHistoryTagCounts)
|
||||
: this.watchHistoryTagCounts &&
|
||||
typeof this.watchHistoryTagCounts === "object"
|
||||
? { ...this.watchHistoryTagCounts }
|
||||
: undefined;
|
||||
const exploreTagIdf =
|
||||
this.exploreTagIdf instanceof Map
|
||||
? new Map(this.exploreTagIdf)
|
||||
: this.exploreTagIdf && typeof this.exploreTagIdf === "object"
|
||||
? { ...this.exploreTagIdf }
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
blacklistedEventIds: blacklist,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
tagPreferences: {
|
||||
interests: Array.isArray(interests) ? [...interests] : [],
|
||||
disinterests: Array.isArray(disinterests) ? [...disinterests] : [],
|
||||
},
|
||||
watchHistoryTagCounts,
|
||||
exploreTagIdf,
|
||||
moderationThresholds: moderationThresholds
|
||||
? { ...moderationThresholds }
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
buildRecentFeedRuntime() {
|
||||
const blacklist =
|
||||
this.blacklistedEventIds instanceof Set
|
||||
@@ -5812,6 +5938,63 @@ class Application {
|
||||
});
|
||||
}
|
||||
|
||||
refreshExploreFeed({ reason, fallbackVideos } = {}) {
|
||||
const runtime = this.buildExploreFeedRuntime();
|
||||
const normalizedReason = typeof reason === "string" ? reason : undefined;
|
||||
const fallback = Array.isArray(fallbackVideos) ? fallbackVideos : [];
|
||||
|
||||
if (!this.feedEngine || typeof this.feedEngine.run !== "function") {
|
||||
const metadata = {
|
||||
reason: normalizedReason,
|
||||
engine: "unavailable",
|
||||
};
|
||||
this.latestFeedMetadata = metadata;
|
||||
this.videosMap = this.nostrService.getVideosMap();
|
||||
if (this.videoListView) {
|
||||
this.videoListView.state.videosMap = this.videosMap;
|
||||
}
|
||||
this.renderVideoList({ videos: fallback, metadata });
|
||||
return Promise.resolve({ videos: fallback, metadata });
|
||||
}
|
||||
|
||||
return this.feedEngine
|
||||
.run("explore", { runtime })
|
||||
.then((result) => {
|
||||
const videos = Array.isArray(result?.videos) ? result.videos : [];
|
||||
const metadata = {
|
||||
...(result?.metadata || {}),
|
||||
};
|
||||
if (normalizedReason) {
|
||||
metadata.reason = normalizedReason;
|
||||
}
|
||||
|
||||
this.latestFeedMetadata = metadata;
|
||||
this.videosMap = this.nostrService.getVideosMap();
|
||||
if (this.videoListView) {
|
||||
this.videoListView.state.videosMap = this.videosMap;
|
||||
}
|
||||
|
||||
const payload = { videos, metadata };
|
||||
this.renderVideoList(payload);
|
||||
return payload;
|
||||
})
|
||||
.catch((error) => {
|
||||
devLogger.error("[Application] Failed to run explore feed:", error);
|
||||
const metadata = {
|
||||
reason: normalizedReason || "error:explore-feed",
|
||||
error: true,
|
||||
};
|
||||
this.latestFeedMetadata = metadata;
|
||||
this.videosMap = this.nostrService.getVideosMap();
|
||||
if (this.videoListView) {
|
||||
this.videoListView.state.videosMap = this.videosMap;
|
||||
}
|
||||
const payload = { videos: fallback, metadata };
|
||||
this.renderVideoList(payload);
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
refreshRecentFeed({ reason, fallbackVideos } = {}) {
|
||||
const runtime = this.buildRecentFeedRuntime();
|
||||
const normalizedReason = typeof reason === "string" ? reason : undefined;
|
||||
@@ -5968,6 +6151,54 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
async loadExploreVideos(forceFetch = false) {
|
||||
devLogger.log("Starting loadExploreVideos... (forceFetch =", forceFetch, ")");
|
||||
this.setFeedTelemetryContext("explore");
|
||||
|
||||
const container = this.mountVideoListView({ includeTags: false });
|
||||
const hasCachedVideos =
|
||||
this.nostrService &&
|
||||
Array.isArray(this.nostrService.getFilteredActiveVideos()) &&
|
||||
this.nostrService.getFilteredActiveVideos().length > 0;
|
||||
|
||||
if (!hasCachedVideos) {
|
||||
if (this.videoListView && container) {
|
||||
this.videoListView.showLoading("Fetching explore videos…");
|
||||
} else if (container) {
|
||||
container.innerHTML = getSidebarLoadingMarkup("Fetching explore videos…");
|
||||
}
|
||||
}
|
||||
|
||||
let initialRefreshPromise = null;
|
||||
|
||||
const videos = await this.nostrService.loadVideos({
|
||||
forceFetch,
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
|
||||
onVideos: (payload, detail = {}) => {
|
||||
const promise = this.refreshExploreFeed({
|
||||
reason: detail?.reason,
|
||||
fallbackVideos: payload,
|
||||
});
|
||||
if (!initialRefreshPromise) {
|
||||
initialRefreshPromise = promise;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (initialRefreshPromise) {
|
||||
await initialRefreshPromise;
|
||||
} else if (!Array.isArray(videos) || videos.length === 0) {
|
||||
await this.refreshExploreFeed({ reason: "initial", fallbackVideos: [] });
|
||||
}
|
||||
|
||||
this.videoSubscription = this.nostrService.getVideoSubscription() || null;
|
||||
this.videosMap = this.nostrService.getVideosMap();
|
||||
if (this.videoListView) {
|
||||
this.videoListView.state.videosMap = this.videosMap;
|
||||
}
|
||||
}
|
||||
|
||||
async loadOlderVideos(lastTimestamp) {
|
||||
const olderVideos = await this.nostrService.loadOlderVideos(lastTimestamp, {
|
||||
blacklistedEventIds: this.blacklistedEventIds,
|
||||
@@ -6271,29 +6502,57 @@ class Application {
|
||||
}
|
||||
}
|
||||
|
||||
setFeedTelemetryContext(feedName = "") {
|
||||
getFeedTelemetryState(feedName = "") {
|
||||
if (!this.feedTelemetryState || typeof this.feedTelemetryState !== "object") {
|
||||
this.feedTelemetryState = {
|
||||
activeFeed: "",
|
||||
matchedTagsById: new Map(),
|
||||
matchReasonsById: new Map(),
|
||||
lastImpressionSignature: "",
|
||||
activePlayback: null,
|
||||
feeds: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
if (!(this.feedTelemetryState.feeds instanceof Map)) {
|
||||
this.feedTelemetryState.feeds = new Map();
|
||||
}
|
||||
|
||||
const normalized =
|
||||
typeof feedName === "string" ? feedName.trim().toLowerCase() : "";
|
||||
if (this.feedTelemetryState.activeFeed !== normalized) {
|
||||
this.feedTelemetryState.lastImpressionSignature = "";
|
||||
this.feedTelemetryState.activePlayback = null;
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
this.feedTelemetryState.activeFeed = normalized;
|
||||
|
||||
if (normalized !== "for-you") {
|
||||
this.feedTelemetryState.matchedTagsById = new Map();
|
||||
this.feedTelemetryState.matchReasonsById = new Map();
|
||||
if (this.feedTelemetryState.feeds.has(normalized)) {
|
||||
return this.feedTelemetryState.feeds.get(normalized);
|
||||
}
|
||||
|
||||
const state = {
|
||||
matchedTagsById: new Map(),
|
||||
matchReasonsById: new Map(),
|
||||
lastImpressionSignature: "",
|
||||
activePlayback: null,
|
||||
};
|
||||
this.feedTelemetryState.feeds.set(normalized, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
setFeedTelemetryContext(feedName = "") {
|
||||
const normalized =
|
||||
typeof feedName === "string" ? feedName.trim().toLowerCase() : "";
|
||||
const previousFeed = this.feedTelemetryState?.activeFeed || "";
|
||||
if (previousFeed && previousFeed !== normalized) {
|
||||
const previousState = this.getFeedTelemetryState(previousFeed);
|
||||
if (previousState) {
|
||||
previousState.lastImpressionSignature = "";
|
||||
previousState.activePlayback = null;
|
||||
}
|
||||
}
|
||||
|
||||
const nextState = this.getFeedTelemetryState(normalized);
|
||||
if (previousFeed !== normalized && nextState) {
|
||||
nextState.lastImpressionSignature = "";
|
||||
nextState.activePlayback = null;
|
||||
}
|
||||
|
||||
this.feedTelemetryState.activeFeed = normalized;
|
||||
}
|
||||
|
||||
isForYouFeedActive() {
|
||||
@@ -6305,6 +6564,11 @@ class Application {
|
||||
return;
|
||||
}
|
||||
|
||||
const forYouState = this.getFeedTelemetryState("for-you");
|
||||
if (!forYouState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedTagsById = new Map();
|
||||
const matchReasonsById = new Map();
|
||||
|
||||
@@ -6340,8 +6604,8 @@ class Application {
|
||||
matchReasonsById.set(videoId, reasons);
|
||||
});
|
||||
|
||||
this.feedTelemetryState.matchedTagsById = matchedTagsById;
|
||||
this.feedTelemetryState.matchReasonsById = matchReasonsById;
|
||||
forYouState.matchedTagsById = matchedTagsById;
|
||||
forYouState.matchReasonsById = matchReasonsById;
|
||||
}
|
||||
|
||||
resolveVideoForTelemetry(videoId) {
|
||||
@@ -6376,6 +6640,11 @@ class Application {
|
||||
return null;
|
||||
}
|
||||
|
||||
const forYouState = this.getFeedTelemetryState("for-you");
|
||||
if (!forYouState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const eventId =
|
||||
typeof videoId === "string" && videoId
|
||||
? videoId
|
||||
@@ -6386,8 +6655,7 @@ class Application {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matchedTagsRaw =
|
||||
this.feedTelemetryState?.matchedTagsById?.get(eventId) || [];
|
||||
const matchedTagsRaw = forYouState.matchedTagsById?.get(eventId) || [];
|
||||
const matchedTags = Array.isArray(matchedTagsRaw)
|
||||
? Array.from(
|
||||
new Set(
|
||||
@@ -6399,7 +6667,7 @@ class Application {
|
||||
)
|
||||
: [];
|
||||
|
||||
const whyRaw = this.feedTelemetryState?.matchReasonsById?.get(eventId) || [];
|
||||
const whyRaw = forYouState.matchReasonsById?.get(eventId) || [];
|
||||
const why = Array.isArray(whyRaw)
|
||||
? whyRaw.map((entry) => ({
|
||||
stage: typeof entry.stage === "string" ? entry.stage : "",
|
||||
@@ -6477,19 +6745,21 @@ class Application {
|
||||
return;
|
||||
}
|
||||
|
||||
const forYouState = this.getFeedTelemetryState("for-you");
|
||||
if (!forYouState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signature = videos
|
||||
.map((video) => (typeof video?.id === "string" ? video.id : ""))
|
||||
.filter(Boolean)
|
||||
.join("|");
|
||||
|
||||
if (
|
||||
signature &&
|
||||
signature === this.feedTelemetryState.lastImpressionSignature
|
||||
) {
|
||||
if (signature && signature === forYouState.lastImpressionSignature) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.feedTelemetryState.lastImpressionSignature = signature;
|
||||
forYouState.lastImpressionSignature = signature;
|
||||
|
||||
videos.forEach((video, index) => {
|
||||
this.emitForYouTelemetryEvent("for_you_impression", {
|
||||
@@ -6505,10 +6775,15 @@ class Application {
|
||||
return;
|
||||
}
|
||||
|
||||
const forYouState = this.getFeedTelemetryState("for-you");
|
||||
if (!forYouState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const video = this.resolveVideoForTelemetry(videoId);
|
||||
const position = this.resolveVideoIndex(videoId);
|
||||
|
||||
this.feedTelemetryState.activePlayback = {
|
||||
forYouState.activePlayback = {
|
||||
feed: "for-you",
|
||||
videoId,
|
||||
};
|
||||
@@ -6525,7 +6800,12 @@ class Application {
|
||||
return;
|
||||
}
|
||||
|
||||
const activePlayback = this.feedTelemetryState?.activePlayback;
|
||||
const forYouState = this.getFeedTelemetryState("for-you");
|
||||
if (!forYouState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activePlayback = forYouState.activePlayback;
|
||||
if (!activePlayback || activePlayback.feed !== "for-you") {
|
||||
return;
|
||||
}
|
||||
@@ -6560,7 +6840,7 @@ class Application {
|
||||
}
|
||||
|
||||
this.emitTelemetryEvent("for_you_watch", payload);
|
||||
this.feedTelemetryState.activePlayback = null;
|
||||
forYouState.activePlayback = null;
|
||||
}
|
||||
|
||||
async renderVideoList(payload) {
|
||||
@@ -8469,12 +8749,12 @@ class Application {
|
||||
this.currentVideo.pointerKey = this.currentVideoPointerKey;
|
||||
}
|
||||
|
||||
const forYouState = this.getFeedTelemetryState("for-you");
|
||||
if (
|
||||
this.feedTelemetryState?.activePlayback?.feed === "for-you" &&
|
||||
this.feedTelemetryState.activePlayback.videoId === eventId
|
||||
forYouState?.activePlayback?.feed === "for-you" &&
|
||||
forYouState.activePlayback.videoId === eventId
|
||||
) {
|
||||
this.feedTelemetryState.activePlayback.pointerKey =
|
||||
this.currentVideoPointerKey || null;
|
||||
forYouState.activePlayback.pointerKey = this.currentVideoPointerKey || null;
|
||||
}
|
||||
|
||||
this.subscribeModalViewCount(
|
||||
|
||||
@@ -275,8 +275,10 @@ export function createResolvePostedAtStage({
|
||||
|
||||
export function createTagPreferenceFilterStage({
|
||||
stageName = "tag-preference-filter",
|
||||
matchInterests = true,
|
||||
} = {}) {
|
||||
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);
|
||||
@@ -340,7 +342,7 @@ export function createTagPreferenceFilterStage({
|
||||
let matchedInterests = [];
|
||||
if (interests.size) {
|
||||
matchedInterests = [...videoTags].filter((tag) => interests.has(tag));
|
||||
if (matchedInterests.length === 0) {
|
||||
if (shouldMatchInterests && matchedInterests.length === 0) {
|
||||
context?.addWhy?.({
|
||||
stage: stageName,
|
||||
type: "filter",
|
||||
|
||||
@@ -264,6 +264,7 @@ export default class ApplicationBootstrap {
|
||||
}
|
||||
app.registerRecentFeed();
|
||||
app.registerForYouFeed();
|
||||
app.registerExploreFeed();
|
||||
app.registerSubscriptionsFeed();
|
||||
app.registerWatchHistoryFeed();
|
||||
|
||||
|
||||
+12
-1
@@ -119,11 +119,22 @@ export const viewInitRegistry = {
|
||||
},
|
||||
explore: () => {
|
||||
devLogger.log("Explore view loaded.");
|
||||
const app = getApplication();
|
||||
if (app && typeof app.loadExploreVideos === "function") {
|
||||
if (typeof app.mountVideoListView === "function") {
|
||||
app.mountVideoListView({ includeTags: false });
|
||||
}
|
||||
app.loadExploreVideos();
|
||||
}
|
||||
const refreshApp = getApplication();
|
||||
if (refreshApp && typeof refreshApp.forceRefreshAllProfiles === "function") {
|
||||
refreshApp.forceRefreshAllProfiles();
|
||||
}
|
||||
const infoTrigger = document.getElementById("exploreInfoTrigger");
|
||||
if (infoTrigger) {
|
||||
attachFeedInfoPopover(
|
||||
infoTrigger,
|
||||
"Coming soon."
|
||||
"Discovery feed blended for freshness, novelty, and diversity."
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+11
-1
@@ -25,5 +25,15 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-muted-strong">Coming Soon...</p>
|
||||
<div id="videoList" class="bv-grid-video">
|
||||
<div class="sidebar-loading-wrapper" role="status" aria-live="polite">
|
||||
<div class="sidebar-loading-indicator">
|
||||
<span
|
||||
class="status-spinner status-spinner--inline"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="sidebar-loading-text">Loading explore…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
Reference in new issue
Block a user