mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 01:12:39 +00:00
Removes the metadata argument from `WatchHistoryService.publishView` and cleans up the associated caching and resolution logic in `watchHistory.js` and `watchHistoryFeed.js`. Deletes `js/watchHistoryMetadata.js` as it is no longer used. This simplification ensures watch history strictly manages pointers and timestamps, leaving metadata resolution to higher-level consumers. It also reduces the payload size and complexity of persisted history records. Tests have been updated to reflect the API changes.
303 lines
7.8 KiB
JavaScript
303 lines
7.8 KiB
JavaScript
// js/feedEngine/watchHistoryFeed.js
|
|
|
|
import { pointerKey, normalizePointerInput } from "../nostr/watchHistory.js";
|
|
import watchHistoryService from "../watchHistoryService.js";
|
|
import nostrService from "../services/nostrService.js";
|
|
import {
|
|
isWatchHistoryDebugEnabled,
|
|
logWatchHistoryDebug,
|
|
} from "../watchHistoryDebug.js";
|
|
import {
|
|
createBlacklistFilterStage,
|
|
createWatchHistorySuppressionStage,
|
|
} from "./stages.js";
|
|
import { isPlainObject } from "./utils.js";
|
|
|
|
const WATCH_HISTORY_FEED_LOG_NAMESPACE = "watchHistoryFeed";
|
|
|
|
function debugInfo(message, details) {
|
|
logWatchHistoryDebug(WATCH_HISTORY_FEED_LOG_NAMESPACE, "info", message, details);
|
|
}
|
|
|
|
function debugWarn(message, details) {
|
|
logWatchHistoryDebug(WATCH_HISTORY_FEED_LOG_NAMESPACE, "warn", message, details);
|
|
}
|
|
|
|
function normalizeActorCandidate(...values) {
|
|
for (const value of values) {
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
if (trimmed) {
|
|
return trimmed;
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function resolveWatchedAt(...candidates) {
|
|
for (const candidate of candidates) {
|
|
if (!Number.isFinite(candidate)) {
|
|
continue;
|
|
}
|
|
let normalized = Math.floor(Number(candidate));
|
|
if (normalized <= 0) {
|
|
continue;
|
|
}
|
|
if (normalized > 10_000_000_000) {
|
|
normalized = Math.floor(normalized / 1000);
|
|
}
|
|
if (normalized > 0) {
|
|
return normalized;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function resolveResumeSeconds(candidate) {
|
|
const candidates = [
|
|
candidate?.resumeSeconds,
|
|
candidate?.resume,
|
|
candidate?.resumeAt,
|
|
candidate?.resumePosition,
|
|
candidate?.resumeTime,
|
|
candidate?.progress?.resume,
|
|
candidate?.progress?.seconds,
|
|
candidate?.progress?.position,
|
|
candidate?.pointer?.resumeSeconds,
|
|
candidate?.pointer?.resume,
|
|
candidate?.pointer?.resumeAt,
|
|
];
|
|
|
|
for (const entry of candidates) {
|
|
if (!Number.isFinite(entry)) {
|
|
continue;
|
|
}
|
|
const normalized = Math.max(0, Math.floor(entry));
|
|
if (normalized > 0) {
|
|
return normalized;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveCompleted(candidate) {
|
|
const candidates = [
|
|
candidate?.completed,
|
|
candidate?.complete,
|
|
candidate?.progress?.completed,
|
|
candidate?.progress?.complete,
|
|
candidate?.pointer?.completed,
|
|
candidate?.pointer?.complete,
|
|
];
|
|
return candidates.some((value) => value === true);
|
|
}
|
|
|
|
function normalizeHistoryEntry(raw) {
|
|
const pointer = normalizePointerInput(raw?.pointer || raw);
|
|
if (!pointer) {
|
|
return null;
|
|
}
|
|
|
|
const key = pointerKey(pointer);
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
|
|
const watchedAt = resolveWatchedAt(
|
|
raw?.watchedAt,
|
|
raw?.timestamp,
|
|
raw?.created_at,
|
|
pointer?.watchedAt,
|
|
raw?.metadata?.watchedAt,
|
|
pointer?.metadata?.watchedAt,
|
|
);
|
|
|
|
const resumeAt = (() => {
|
|
const fromPointer = Number.isFinite(pointer?.resumeAt)
|
|
? Math.max(0, Math.floor(pointer.resumeAt))
|
|
: Number.isFinite(pointer?.metadata?.resumeAt)
|
|
? Math.max(0, Math.floor(pointer.metadata.resumeAt))
|
|
: null;
|
|
if (fromPointer !== null) {
|
|
return fromPointer;
|
|
}
|
|
return resolveResumeSeconds(raw);
|
|
})();
|
|
|
|
const completed =
|
|
pointer?.completed === true ||
|
|
pointer?.metadata?.completed === true ||
|
|
resolveCompleted(raw);
|
|
|
|
const pointerVideo =
|
|
(pointer?.video && isPlainObject(pointer.video) ? pointer.video : null) ||
|
|
(pointer?.metadata?.video && isPlainObject(pointer.metadata.video)
|
|
? pointer.metadata.video
|
|
: null);
|
|
|
|
const pointerProfile =
|
|
(pointer?.profile && isPlainObject(pointer.profile) ? pointer.profile : null) ||
|
|
(pointer?.metadata?.profile && isPlainObject(pointer.metadata.profile)
|
|
? pointer.metadata.profile
|
|
: null);
|
|
|
|
return {
|
|
pointer,
|
|
pointerKey: key,
|
|
watchedAt,
|
|
video:
|
|
isPlainObject(raw?.video) && raw.video
|
|
? raw.video
|
|
: pointerVideo,
|
|
metadata: {
|
|
source: "watch-history",
|
|
pointerKey: key,
|
|
watchedAt,
|
|
resumeAt,
|
|
completed,
|
|
session: raw?.session === true || pointer.session === true,
|
|
video: pointerVideo,
|
|
profile: pointerProfile,
|
|
},
|
|
};
|
|
}
|
|
|
|
function createWatchHistorySource({ service = watchHistoryService } = {}) {
|
|
return async function watchHistorySource(context = {}) {
|
|
const actor = normalizeActorCandidate(
|
|
context?.runtime?.watchHistory?.actor,
|
|
context?.config?.actor,
|
|
context?.runtime?.actor,
|
|
);
|
|
|
|
if (!service || typeof service.loadLatest !== "function") {
|
|
return [];
|
|
}
|
|
|
|
let items = [];
|
|
try {
|
|
items = await Promise.resolve(service.loadLatest(actor || undefined));
|
|
} catch (error) {
|
|
context?.log?.("[watch-history-feed] Failed to load history", error);
|
|
items = [];
|
|
}
|
|
|
|
const results = [];
|
|
for (const raw of Array.isArray(items) ? items : []) {
|
|
const normalized = normalizeHistoryEntry(raw);
|
|
if (normalized) {
|
|
results.push(normalized);
|
|
}
|
|
}
|
|
|
|
results.sort((a, b) => {
|
|
if (a.watchedAt !== b.watchedAt) {
|
|
return b.watchedAt - a.watchedAt;
|
|
}
|
|
return a.pointerKey.localeCompare(b.pointerKey);
|
|
});
|
|
|
|
return results;
|
|
};
|
|
}
|
|
|
|
function createWatchHistoryHydratorStage({ service = watchHistoryService } = {}) {
|
|
return async function watchHistoryHydratorStage(items = [], context = {}) {
|
|
const results = [];
|
|
|
|
for (const item of Array.isArray(items) ? items : []) {
|
|
if (!item || typeof item !== "object") {
|
|
continue;
|
|
}
|
|
|
|
const pointerKeyValue =
|
|
typeof item.pointerKey === "string" && item.pointerKey
|
|
? item.pointerKey
|
|
: pointerKey(item.pointer);
|
|
|
|
const watchedAtValue = Number.isFinite(item?.watchedAt)
|
|
? Math.max(0, Math.floor(Number(item.watchedAt)))
|
|
: Number.isFinite(item?.metadata?.watchedAt)
|
|
? Math.max(0, Math.floor(Number(item.metadata.watchedAt)))
|
|
: 0;
|
|
|
|
const metadata = {
|
|
...(isPlainObject(item.metadata) ? item.metadata : {}),
|
|
pointerKey: pointerKeyValue,
|
|
watchedAt: watchedAtValue || null,
|
|
resumeAt: item.metadata?.resumeAt ?? null,
|
|
completed: item.metadata?.completed ?? false,
|
|
session: item.metadata?.session === true,
|
|
};
|
|
|
|
const nextItem = {
|
|
...item,
|
|
pointerKey: pointerKeyValue,
|
|
watchedAt: watchedAtValue,
|
|
metadata,
|
|
};
|
|
results.push(nextItem);
|
|
}
|
|
|
|
return results;
|
|
};
|
|
}
|
|
|
|
function createWatchHistorySorter() {
|
|
return function watchHistorySorter(items = []) {
|
|
if (!Array.isArray(items)) {
|
|
return [];
|
|
}
|
|
const copy = [...items];
|
|
copy.sort((a, b) => {
|
|
if (a?.watchedAt !== b?.watchedAt) {
|
|
return (b?.watchedAt || 0) - (a?.watchedAt || 0);
|
|
}
|
|
const aKey = typeof a?.pointerKey === "string" ? a.pointerKey : "";
|
|
const bKey = typeof b?.pointerKey === "string" ? b.pointerKey : "";
|
|
return aKey.localeCompare(bKey);
|
|
});
|
|
return copy;
|
|
};
|
|
}
|
|
|
|
export function createWatchHistoryFeedDefinition({
|
|
service = watchHistoryService,
|
|
nostr = nostrService,
|
|
shouldIncludeVideo,
|
|
} = {}) {
|
|
const blacklistStage = createBlacklistFilterStage({
|
|
shouldIncludeVideo:
|
|
typeof shouldIncludeVideo === "function"
|
|
? shouldIncludeVideo
|
|
: (video, options) =>
|
|
(nostr?.shouldIncludeVideo
|
|
? nostr.shouldIncludeVideo(video, options)
|
|
: true),
|
|
});
|
|
|
|
return {
|
|
source: createWatchHistorySource({ service }),
|
|
stages: [
|
|
createWatchHistoryHydratorStage({ service }),
|
|
blacklistStage,
|
|
createWatchHistorySuppressionStage(),
|
|
],
|
|
sorter: createWatchHistorySorter(),
|
|
defaultConfig: {
|
|
actorFilters: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
export function registerWatchHistoryFeed(engine, options = {}) {
|
|
if (!engine || typeof engine.registerFeed !== "function") {
|
|
return null;
|
|
}
|
|
|
|
const definition = createWatchHistoryFeedDefinition(options);
|
|
return engine.registerFeed("watch-history", definition);
|
|
}
|
|
|