Merge pull request #1427 from PR0M3TH3AN/ai/refactor-similar-content-20231026-2020523280376033819

Refactor Similar Content Logic to Controller
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-20 00:11:20 -05:00
commit af84ded449
3 files changed
+544 -260

No files matched your search

+48 -260
View File
@@ -150,6 +150,7 @@ import {
urlHealthConstants,
} from "./state/cache.js";
import ApplicationBootstrap from "./ui/applicationBootstrap.js";
import SimilarContentController from "./ui/similarContentController.js";
import VideoModalCommentController from "./ui/videoModalCommentController.js";
import TorrentStatusController from "./ui/torrentStatusController.js";
import ModerationActionController from "./services/moderationActionController.js";
@@ -285,6 +286,7 @@ class Application {
});
this.initializeModerationActionController();
this.initializeSimilarContentController();
}
get modalVideo() {
@@ -1136,7 +1138,40 @@ class Application {
};
}
initializeSimilarContentController() {
if (this.similarContentController) {
return;
}
this.similarContentController = new SimilarContentController({
services: {
nostrClient,
},
callbacks: {
isAuthorBlocked: (pubkey) => this.isAuthorBlocked(pubkey),
decorateVideoModeration: (video) => this.decorateVideoModeration(video),
decorateVideoCreatorIdentity: (video) => this.decorateVideoCreatorIdentity(video),
},
ui: {
videoModal: this.videoModal,
},
state: {
getVideoListView: () => this.videoListView,
getVideosMap: () => this.videosMap,
},
helpers: {
getKnownVideoPostedAt: (video) => this.getKnownVideoPostedAt(video),
buildShareUrlFromEventId: (id) => this.buildShareUrlFromEventId(id),
formatTimeAgo: (ts) => this.formatTimeAgo(ts),
}
});
}
extractDTagValue(tags) {
if (this.similarContentController) {
return this.similarContentController.extractDTagValue(tags);
}
// Fallback if controller not initialized (though it should be)
if (!Array.isArray(tags)) {
return "";
}
@@ -1152,6 +1187,10 @@ class Application {
}
deriveVideoPointerInfo(video) {
if (this.similarContentController) {
return this.similarContentController.deriveVideoPointerInfo(video);
}
// Fallback if controller not initialized
if (!video || typeof video !== "object") {
return null;
}
@@ -1168,271 +1207,20 @@ class Application {
});
}
computeSimilarContentCandidates({ activeVideo = this.currentVideo, maxItems = 5 } = {}) {
const decorateCandidate = (video) => {
if (!video || typeof video !== "object") {
return video;
}
let decoratedVideo = video;
if (typeof this.decorateVideoModeration === "function") {
try {
const decorated = this.decorateVideoModeration(video);
if (decorated && typeof decorated === "object") {
decoratedVideo = decorated;
}
} catch (error) {
devLogger.warn(
"[Application] Failed to decorate similar content candidate",
error,
);
}
}
if (typeof this.decorateVideoCreatorIdentity === "function") {
try {
const identityDecorated = this.decorateVideoCreatorIdentity(
decoratedVideo,
);
if (identityDecorated && typeof identityDecorated === "object") {
decoratedVideo = identityDecorated;
}
} catch (error) {
devLogger.warn(
"[Application] Failed to decorate similar content identity",
error,
);
}
}
return decoratedVideo;
};
const target = activeVideo && typeof activeVideo === "object" ? decorateCandidate(activeVideo) : null;
if (!target) {
return [];
}
const activeTagsSource = Array.isArray(target.displayTags) && target.displayTags.length
? target.displayTags
: collectVideoTags(target);
const activeTagSet = new Set();
for (const tag of activeTagsSource) {
if (typeof tag !== "string") {
continue;
}
const normalized = tag.trim().toLowerCase();
if (normalized) {
activeTagSet.add(normalized);
}
}
if (activeTagSet.size === 0) {
return [];
}
const limit = Number.isFinite(maxItems) && maxItems > 0
? Math.max(1, Math.floor(maxItems))
: 5;
let candidateSource = [];
if (Array.isArray(this.videoListView?.currentVideos) && this.videoListView.currentVideos.length) {
candidateSource = this.videoListView.currentVideos;
} else if (this.videosMap instanceof Map && this.videosMap.size) {
candidateSource = Array.from(this.videosMap.values());
} else if (nostrClient && typeof nostrClient.getActiveVideos === "function") {
try {
const activeVideos = nostrClient.getActiveVideos();
if (Array.isArray(activeVideos)) {
candidateSource = activeVideos;
}
} catch (error) {
devLogger.warn("[Application] Failed to read active videos for similar content:", error);
}
}
if (!Array.isArray(candidateSource) || candidateSource.length === 0) {
return [];
}
const activeId = typeof target.id === "string" ? target.id : "";
const activePointerKey = typeof target.pointerKey === "string" ? target.pointerKey : "";
const seenKeys = new Set();
if (activeId) {
const normalizedId = activeId.trim().toLowerCase();
if (normalizedId) {
seenKeys.add(normalizedId);
}
}
if (activePointerKey) {
const normalizedPointer = activePointerKey.trim().toLowerCase();
if (normalizedPointer) {
seenKeys.add(normalizedPointer);
}
}
const results = [];
for (const candidate of candidateSource) {
const decoratedCandidate = decorateCandidate(candidate);
if (!decoratedCandidate || typeof decoratedCandidate !== "object") {
continue;
}
if (decoratedCandidate === target) {
continue;
}
const candidateId = typeof decoratedCandidate.id === "string" ? decoratedCandidate.id : "";
if (candidateId && candidateId === activeId) {
continue;
}
if (decoratedCandidate.deleted === true) {
continue;
}
if (decoratedCandidate.isPrivate === true) {
continue;
}
if (decoratedCandidate.isNsfw === true && ALLOW_NSFW_CONTENT !== true) {
continue;
}
const candidatePubkey = typeof decoratedCandidate.pubkey === "string" ? decoratedCandidate.pubkey : "";
if (candidatePubkey && this.isAuthorBlocked(candidatePubkey)) {
continue;
}
const candidateTagsSource = Array.isArray(decoratedCandidate.displayTags) && decoratedCandidate.displayTags.length
? decoratedCandidate.displayTags
: collectVideoTags(decoratedCandidate);
if (!Array.isArray(candidateTagsSource) || candidateTagsSource.length === 0) {
continue;
}
const candidateTagSet = new Set();
for (const tag of candidateTagsSource) {
if (typeof tag !== "string") {
continue;
}
const normalized = tag.trim().toLowerCase();
if (normalized) {
candidateTagSet.add(normalized);
}
}
if (candidateTagSet.size === 0) {
continue;
}
let sharedCount = 0;
for (const tag of candidateTagSet) {
if (activeTagSet.has(tag)) {
sharedCount += 1;
}
}
if (sharedCount === 0) {
continue;
}
const pointerInfo = this.deriveVideoPointerInfo(candidate);
const pointerKey = typeof candidate.pointerKey === "string" && candidate.pointerKey.trim()
? candidate.pointerKey.trim()
: typeof pointerInfo?.key === "string" && pointerInfo.key
? pointerInfo.key
: "";
const dedupeKeyRaw = (candidateId || pointerKey || "").trim();
if (dedupeKeyRaw) {
const dedupeKey = dedupeKeyRaw.toLowerCase();
if (seenKeys.has(dedupeKey)) {
continue;
}
seenKeys.add(dedupeKey);
}
if (!Array.isArray(candidate.displayTags) || candidate.displayTags.length === 0) {
candidate.displayTags = Array.isArray(candidateTagsSource)
? candidateTagsSource.slice()
: [];
}
let postedAt = this.getKnownVideoPostedAt(decoratedCandidate);
if (!Number.isFinite(postedAt) && Number.isFinite(decoratedCandidate.rootCreatedAt)) {
postedAt = Math.floor(decoratedCandidate.rootCreatedAt);
}
if (!Number.isFinite(postedAt) && Number.isFinite(decoratedCandidate.created_at)) {
postedAt = Math.floor(decoratedCandidate.created_at);
}
if (!Number.isFinite(postedAt)) {
postedAt = null;
}
let shareUrl = "";
if (typeof decoratedCandidate.shareUrl === "string" && decoratedCandidate.shareUrl.trim()) {
shareUrl = decoratedCandidate.shareUrl.trim();
} else if (candidateId) {
shareUrl = this.buildShareUrlFromEventId(candidateId) || "";
}
results.push({
video: decoratedCandidate,
pointerInfo: pointerInfo || null,
shareUrl,
postedAt,
sharedTagCount: sharedCount,
computeSimilarContentCandidates(options = {}) {
if (this.similarContentController) {
return this.similarContentController.computeCandidates({
activeVideo: options.activeVideo || this.currentVideo,
maxItems: options.maxItems,
});
}
if (results.length === 0) {
return [];
}
results.sort((a, b) => {
if (b.sharedTagCount !== a.sharedTagCount) {
return b.sharedTagCount - a.sharedTagCount;
}
const tsA = Number.isFinite(a.postedAt) ? a.postedAt : 0;
const tsB = Number.isFinite(b.postedAt) ? b.postedAt : 0;
return tsB - tsA;
});
return results.slice(0, limit).map((entry) => {
const normalizedPostedAt = Number.isFinite(entry.postedAt)
? Math.floor(entry.postedAt)
: null;
const timeAgo = normalizedPostedAt !== null ? this.formatTimeAgo(normalizedPostedAt) : "";
return {
video: entry.video,
pointerInfo: entry.pointerInfo,
shareUrl: entry.shareUrl,
postedAt: normalizedPostedAt,
timeAgo,
sharedTagCount: entry.sharedTagCount,
};
});
return [];
}
updateModalSimilarContent({ activeVideo = this.currentVideo, maxItems } = {}) {
if (!this.videoModal) {
return;
}
const target = activeVideo && typeof activeVideo === "object" ? activeVideo : null;
if (!target) {
if (typeof this.videoModal.clearSimilarContent === "function") {
this.videoModal.clearSimilarContent();
}
return;
}
const matches = this.computeSimilarContentCandidates({ activeVideo: target, maxItems });
if (matches.length > 0) {
if (typeof this.videoModal.setSimilarContent === "function") {
this.videoModal.setSimilarContent(matches);
}
return;
}
if (typeof this.videoModal.clearSimilarContent === "function") {
this.videoModal.clearSimilarContent();
if (this.similarContentController) {
this.similarContentController.ui.videoModal = this.videoModal;
this.similarContentController.updateModal({ activeVideo, maxItems });
}
}
+332
View File
@@ -0,0 +1,332 @@
import { devLogger } from "../utils/logger.js";
import { collectVideoTags } from "../utils/videoTags.js";
import { ALLOW_NSFW_CONTENT } from "../config.js";
import resolveVideoPointer from "../utils/videoPointer.js";
/**
* Controller for managing "Similar Content" logic and updates for the video modal.
*/
export default class SimilarContentController {
constructor({
services = {},
callbacks = {},
ui = {},
state = {},
helpers = {}
}) {
this.services = services;
this.callbacks = callbacks;
this.ui = ui;
this.state = state;
this.helpers = helpers;
}
extractDTagValue(tags) {
if (!Array.isArray(tags)) {
return "";
}
for (const tag of tags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
if (tag[0] === "d" && typeof tag[1] === "string") {
return tag[1];
}
}
return "";
}
deriveVideoPointerInfo(video) {
if (!video || typeof video !== "object") {
return null;
}
const dTagValue = (this.extractDTagValue(video.tags) || "").trim();
return resolveVideoPointer({
kind: video.kind,
pubkey: video.pubkey,
videoRootId: video.videoRootId,
dTag: dTagValue,
fallbackEventId: video.id,
relay: video.relay,
});
}
computeCandidates({ activeVideo, maxItems = 5 } = {}) {
const decorateCandidate = (video) => {
if (!video || typeof video !== "object") {
return video;
}
let decoratedVideo = video;
if (typeof this.callbacks.decorateVideoModeration === "function") {
try {
const decorated = this.callbacks.decorateVideoModeration(video);
if (decorated && typeof decorated === "object") {
decoratedVideo = decorated;
}
} catch (error) {
devLogger.warn(
"[SimilarContentController] Failed to decorate similar content candidate",
error,
);
}
}
if (typeof this.callbacks.decorateVideoCreatorIdentity === "function") {
try {
const identityDecorated = this.callbacks.decorateVideoCreatorIdentity(
decoratedVideo,
);
if (identityDecorated && typeof identityDecorated === "object") {
decoratedVideo = identityDecorated;
}
} catch (error) {
devLogger.warn(
"[SimilarContentController] Failed to decorate similar content identity",
error,
);
}
}
return decoratedVideo;
};
const target = activeVideo && typeof activeVideo === "object" ? decorateCandidate(activeVideo) : null;
if (!target) {
return [];
}
const activeTagsSource = Array.isArray(target.displayTags) && target.displayTags.length
? target.displayTags
: collectVideoTags(target);
const activeTagSet = new Set();
for (const tag of activeTagsSource) {
if (typeof tag !== "string") {
continue;
}
const normalized = tag.trim().toLowerCase();
if (normalized) {
activeTagSet.add(normalized);
}
}
if (activeTagSet.size === 0) {
return [];
}
const limit = Number.isFinite(maxItems) && maxItems > 0
? Math.max(1, Math.floor(maxItems))
: 5;
let candidateSource = [];
const videoListView = this.state.getVideoListView ? this.state.getVideoListView() : null;
const videosMap = this.state.getVideosMap ? this.state.getVideosMap() : null;
const nostrClient = this.services.nostrClient;
if (videoListView && Array.isArray(videoListView.currentVideos) && videoListView.currentVideos.length) {
candidateSource = videoListView.currentVideos;
} else if (videosMap instanceof Map && videosMap.size) {
candidateSource = Array.from(videosMap.values());
} else if (nostrClient && typeof nostrClient.getActiveVideos === "function") {
try {
const activeVideos = nostrClient.getActiveVideos();
if (Array.isArray(activeVideos)) {
candidateSource = activeVideos;
}
} catch (error) {
devLogger.warn("[SimilarContentController] Failed to read active videos for similar content:", error);
}
}
if (!Array.isArray(candidateSource) || candidateSource.length === 0) {
return [];
}
const activeId = typeof target.id === "string" ? target.id : "";
const activePointerKey = typeof target.pointerKey === "string" ? target.pointerKey : "";
const seenKeys = new Set();
if (activeId) {
const normalizedId = activeId.trim().toLowerCase();
if (normalizedId) {
seenKeys.add(normalizedId);
}
}
if (activePointerKey) {
const normalizedPointer = activePointerKey.trim().toLowerCase();
if (normalizedPointer) {
seenKeys.add(normalizedPointer);
}
}
const results = [];
for (const candidate of candidateSource) {
const decoratedCandidate = decorateCandidate(candidate);
if (!decoratedCandidate || typeof decoratedCandidate !== "object") {
continue;
}
if (decoratedCandidate === target) {
continue;
}
const candidateId = typeof decoratedCandidate.id === "string" ? decoratedCandidate.id : "";
if (candidateId && candidateId === activeId) {
continue;
}
if (decoratedCandidate.deleted === true) {
continue;
}
if (decoratedCandidate.isPrivate === true) {
continue;
}
if (decoratedCandidate.isNsfw === true && ALLOW_NSFW_CONTENT !== true) {
continue;
}
const candidatePubkey = typeof decoratedCandidate.pubkey === "string" ? decoratedCandidate.pubkey : "";
if (candidatePubkey && typeof this.callbacks.isAuthorBlocked === "function" && this.callbacks.isAuthorBlocked(candidatePubkey)) {
continue;
}
const candidateTagsSource = Array.isArray(decoratedCandidate.displayTags) && decoratedCandidate.displayTags.length
? decoratedCandidate.displayTags
: collectVideoTags(decoratedCandidate);
if (!Array.isArray(candidateTagsSource) || candidateTagsSource.length === 0) {
continue;
}
const candidateTagSet = new Set();
for (const tag of candidateTagsSource) {
if (typeof tag !== "string") {
continue;
}
const normalized = tag.trim().toLowerCase();
if (normalized) {
candidateTagSet.add(normalized);
}
}
if (candidateTagSet.size === 0) {
continue;
}
let sharedCount = 0;
for (const tag of candidateTagSet) {
if (activeTagSet.has(tag)) {
sharedCount += 1;
}
}
if (sharedCount === 0) {
continue;
}
const pointerInfo = this.deriveVideoPointerInfo(candidate);
const pointerKey = typeof candidate.pointerKey === "string" && candidate.pointerKey.trim()
? candidate.pointerKey.trim()
: typeof pointerInfo?.key === "string" && pointerInfo.key
? pointerInfo.key
: "";
const dedupeKeyRaw = (candidateId || pointerKey || "").trim();
if (dedupeKeyRaw) {
const dedupeKey = dedupeKeyRaw.toLowerCase();
if (seenKeys.has(dedupeKey)) {
continue;
}
seenKeys.add(dedupeKey);
}
if (!Array.isArray(candidate.displayTags) || candidate.displayTags.length === 0) {
candidate.displayTags = Array.isArray(candidateTagsSource)
? candidateTagsSource.slice()
: [];
}
let postedAt = null;
if (typeof this.helpers.getKnownVideoPostedAt === "function") {
postedAt = this.helpers.getKnownVideoPostedAt(decoratedCandidate);
}
if (!Number.isFinite(postedAt) && Number.isFinite(decoratedCandidate.rootCreatedAt)) {
postedAt = Math.floor(decoratedCandidate.rootCreatedAt);
}
if (!Number.isFinite(postedAt) && Number.isFinite(decoratedCandidate.created_at)) {
postedAt = Math.floor(decoratedCandidate.created_at);
}
if (!Number.isFinite(postedAt)) {
postedAt = null;
}
let shareUrl = "";
if (typeof decoratedCandidate.shareUrl === "string" && decoratedCandidate.shareUrl.trim()) {
shareUrl = decoratedCandidate.shareUrl.trim();
} else if (candidateId && typeof this.helpers.buildShareUrlFromEventId === "function") {
shareUrl = this.helpers.buildShareUrlFromEventId(candidateId) || "";
}
results.push({
video: decoratedCandidate,
pointerInfo: pointerInfo || null,
shareUrl,
postedAt,
sharedTagCount: sharedCount,
});
}
if (results.length === 0) {
return [];
}
results.sort((a, b) => {
if (b.sharedTagCount !== a.sharedTagCount) {
return b.sharedTagCount - a.sharedTagCount;
}
const tsA = Number.isFinite(a.postedAt) ? a.postedAt : 0;
const tsB = Number.isFinite(b.postedAt) ? b.postedAt : 0;
return tsB - tsA;
});
return results.slice(0, limit).map((entry) => {
const normalizedPostedAt = Number.isFinite(entry.postedAt)
? Math.floor(entry.postedAt)
: null;
const timeAgo = normalizedPostedAt !== null && typeof this.helpers.formatTimeAgo === "function"
? this.helpers.formatTimeAgo(normalizedPostedAt)
: "";
return {
video: entry.video,
pointerInfo: entry.pointerInfo,
shareUrl: entry.shareUrl,
postedAt: normalizedPostedAt,
timeAgo,
sharedTagCount: entry.sharedTagCount,
};
});
}
updateModal({ activeVideo, maxItems } = {}) {
if (!this.ui.videoModal) {
return;
}
const target = activeVideo && typeof activeVideo === "object" ? activeVideo : null;
if (!target) {
if (typeof this.ui.videoModal.clearSimilarContent === "function") {
this.ui.videoModal.clearSimilarContent();
}
return;
}
const matches = this.computeCandidates({ activeVideo: target, maxItems });
if (matches.length > 0) {
if (typeof this.ui.videoModal.setSimilarContent === "function") {
this.ui.videoModal.setSimilarContent(matches);
}
return;
}
if (typeof this.ui.videoModal.clearSimilarContent === "function") {
this.ui.videoModal.clearSimilarContent();
}
}
}
+164
View File
@@ -0,0 +1,164 @@
import { describe, it, mock, beforeEach } from "node:test";
import assert from "node:assert";
import SimilarContentController from "../../js/ui/similarContentController.js";
describe("SimilarContentController", () => {
let controller;
let services;
let callbacks;
let ui;
let state;
let helpers;
beforeEach(() => {
services = {
nostrClient: {
getActiveVideos: mock.fn(() => []),
},
};
callbacks = {
isAuthorBlocked: mock.fn(() => false),
decorateVideoModeration: mock.fn((v) => v),
decorateVideoCreatorIdentity: mock.fn((v) => v),
};
ui = {
videoModal: {
setSimilarContent: mock.fn(),
clearSimilarContent: mock.fn(),
},
};
state = {
getVideoListView: mock.fn(() => ({ currentVideos: [] })),
getVideosMap: mock.fn(() => new Map()),
};
helpers = {
getKnownVideoPostedAt: mock.fn(() => Date.now()),
buildShareUrlFromEventId: mock.fn((id) => `https://example.com/v/${id}`),
formatTimeAgo: mock.fn(() => "just now"),
};
controller = new SimilarContentController({
services,
callbacks,
ui,
state,
helpers,
});
});
describe("extractDTagValue", () => {
it("should return the 'd' tag value", () => {
const tags = [["e", "123"], ["d", "my-d-tag"]];
const result = controller.extractDTagValue(tags);
assert.strictEqual(result, "my-d-tag");
});
it("should return empty string if no 'd' tag", () => {
const tags = [["e", "123"]];
const result = controller.extractDTagValue(tags);
assert.strictEqual(result, "");
});
});
describe("computeCandidates", () => {
it("should return empty array if no active video", () => {
const result = controller.computeCandidates();
assert.deepStrictEqual(result, []);
});
it("should compute similar candidates based on tags", () => {
const activeVideo = {
id: "active",
displayTags: ["fun", "music"],
pubkey: "abc"
};
const candidate1 = {
id: "c1",
displayTags: ["fun", "other"],
pubkey: "def"
};
const candidate2 = {
id: "c2",
displayTags: ["boring"],
pubkey: "ghi"
};
state.getVideoListView.mock.mockImplementationOnce(() => ({
currentVideos: [candidate1, candidate2]
}));
const result = controller.computeCandidates({ activeVideo });
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].video.id, "c1");
assert.strictEqual(result[0].sharedTagCount, 1);
});
it("should exclude blocked authors", () => {
const activeVideo = { id: "active", displayTags: ["fun"] };
const candidate1 = { id: "c1", displayTags: ["fun"], pubkey: "bad-guy" };
state.getVideoListView.mock.mockImplementationOnce(() => ({
currentVideos: [candidate1]
}));
callbacks.isAuthorBlocked.mock.mockImplementationOnce((pubkey) => pubkey === "bad-guy");
const result = controller.computeCandidates({ activeVideo });
assert.strictEqual(result.length, 0);
});
it("should prioritize higher shared tag count", () => {
const activeVideo = { id: "active", displayTags: ["a", "b", "c"] };
const candidate1 = { id: "c1", displayTags: ["a"] };
const candidate2 = { id: "c2", displayTags: ["a", "b"] };
state.getVideoListView.mock.mockImplementationOnce(() => ({
currentVideos: [candidate1, candidate2]
}));
const result = controller.computeCandidates({ activeVideo });
assert.strictEqual(result.length, 2);
assert.strictEqual(result[0].video.id, "c2"); // 2 shared tags
assert.strictEqual(result[1].video.id, "c1"); // 1 shared tag
});
});
describe("updateModal", () => {
it("should clear similar content if no active video", () => {
controller.updateModal();
assert.strictEqual(ui.videoModal.clearSimilarContent.mock.callCount(), 1);
});
it("should set similar content if matches found", () => {
const activeVideo = { id: "active", displayTags: ["fun"] };
const candidate1 = { id: "c1", displayTags: ["fun"] };
state.getVideoListView.mock.mockImplementationOnce(() => ({
currentVideos: [candidate1]
}));
controller.updateModal({ activeVideo });
assert.strictEqual(ui.videoModal.setSimilarContent.mock.callCount(), 1);
const args = ui.videoModal.setSimilarContent.mock.calls[0].arguments;
assert.strictEqual(args[0].length, 1);
assert.strictEqual(args[0][0].video.id, "c1");
});
it("should clear similar content if no matches found", () => {
const activeVideo = { id: "active", displayTags: ["fun"] };
const candidate1 = { id: "c1", displayTags: ["boring"] };
state.getVideoListView.mock.mockImplementationOnce(() => ({
currentVideos: [candidate1]
}));
controller.updateModal({ activeVideo });
assert.strictEqual(ui.videoModal.clearSimilarContent.mock.callCount(), 1);
});
});
});