mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #1079 from PR0M3TH3AN/simplify-watch-history-metadata-13037020337635795983
Simplify Watch History Metadata Handling
This commit is contained in:
7 files changed
+26
-839
No files matched your search
@@ -3,7 +3,6 @@
|
||||
import { pointerKey, normalizePointerInput } from "../nostr/watchHistory.js";
|
||||
import watchHistoryService from "../watchHistoryService.js";
|
||||
import nostrService from "../services/nostrService.js";
|
||||
import { createWatchHistoryMetadataResolver } from "../watchHistoryMetadata.js";
|
||||
import {
|
||||
isWatchHistoryDebugEnabled,
|
||||
logWatchHistoryDebug,
|
||||
@@ -203,15 +202,8 @@ function createWatchHistorySource({ service = watchHistoryService } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function createWatchHistoryHydratorStage({
|
||||
service = watchHistoryService,
|
||||
metadataResolver,
|
||||
} = {}) {
|
||||
const resolver = metadataResolver || createWatchHistoryMetadataResolver();
|
||||
|
||||
function createWatchHistoryHydratorStage({ service = watchHistoryService } = {}) {
|
||||
return async function watchHistoryHydratorStage(items = [], context = {}) {
|
||||
const shouldStoreMetadata = false;
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of Array.isArray(items) ? items : []) {
|
||||
@@ -224,150 +216,12 @@ function createWatchHistoryHydratorStage({
|
||||
? item.pointerKey
|
||||
: pointerKey(item.pointer);
|
||||
|
||||
const pointerType =
|
||||
typeof item?.pointer?.type === "string" ? item.pointer.type : null;
|
||||
const pointerValue =
|
||||
typeof item?.pointer?.value === "string" ? item.pointer.value : null;
|
||||
|
||||
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;
|
||||
|
||||
let video = item.video || null;
|
||||
let profile = item.metadata?.profile || null;
|
||||
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Hydrating watch history item.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pointerType,
|
||||
pointerValue,
|
||||
hasInitialVideo: Boolean(video),
|
||||
hasInitialProfile: Boolean(profile),
|
||||
watchedAt: watchedAtValue || null,
|
||||
});
|
||||
}
|
||||
|
||||
if (pointerKeyValue && typeof service?.getLocalMetadata === "function") {
|
||||
try {
|
||||
const stored = service.getLocalMetadata(pointerKeyValue);
|
||||
if (stored) {
|
||||
const hadVideo = Boolean(video);
|
||||
const hadProfile = Boolean(profile);
|
||||
video = video || stored.video || null;
|
||||
profile = profile || stored.profile || null;
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Loaded cached metadata for pointer.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
hadVideo,
|
||||
hadProfile,
|
||||
cachedVideo: Boolean(stored.video),
|
||||
cachedProfile: Boolean(stored.profile),
|
||||
resultingVideo: Boolean(video),
|
||||
resultingProfile: Boolean(profile),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to read cached metadata",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Failed to read cached metadata for pointer.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!video && resolver?.resolveVideo) {
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Resolving video via metadata resolver.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pointerType,
|
||||
pointerValue,
|
||||
});
|
||||
}
|
||||
try {
|
||||
video = await resolver.resolveVideo(item.pointer);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
if (video) {
|
||||
debugInfo("Metadata resolver returned video candidate.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
title:
|
||||
typeof video.title === "string" && video.title.trim()
|
||||
? video.title
|
||||
: null,
|
||||
invalid: Boolean(video.invalid),
|
||||
});
|
||||
} else {
|
||||
debugWarn("Metadata resolver returned no video candidate.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to resolve video from pointer",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Metadata resolver threw while resolving video.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (video?.pubkey && !profile && resolver?.resolveProfile) {
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Resolving profile for video author.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
});
|
||||
}
|
||||
try {
|
||||
profile = resolver.resolveProfile(video.pubkey) || null;
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
if (profile) {
|
||||
const label =
|
||||
profile.display_name ||
|
||||
profile.name ||
|
||||
profile.username ||
|
||||
profile.nip05 ||
|
||||
null;
|
||||
debugInfo("Resolved profile for video author.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
label: label || null,
|
||||
});
|
||||
} else {
|
||||
debugWarn("Resolver returned no profile for video author.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to resolve profile for pointer",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Profile resolver threw while hydrating metadata.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
...(isPlainObject(item.metadata) ? item.metadata : {}),
|
||||
pointerKey: pointerKeyValue,
|
||||
@@ -375,88 +229,15 @@ function createWatchHistoryHydratorStage({
|
||||
resumeAt: item.metadata?.resumeAt ?? null,
|
||||
completed: item.metadata?.completed ?? false,
|
||||
session: item.metadata?.session === true,
|
||||
video: video || null,
|
||||
profile: profile || null,
|
||||
};
|
||||
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
const summary = {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pointerType,
|
||||
pointerValue,
|
||||
watchedAt: watchedAtValue || null,
|
||||
videoId: typeof video?.id === "string" ? video.id : null,
|
||||
title:
|
||||
typeof video?.title === "string" && video.title?.trim()
|
||||
? video.title
|
||||
: null,
|
||||
invalid: Boolean(video?.invalid),
|
||||
hasProfile: Boolean(profile),
|
||||
};
|
||||
if (video) {
|
||||
debugInfo("Hydrated watch history entry metadata.", summary);
|
||||
} else {
|
||||
debugWarn("Hydrated entry is missing resolved video metadata.", summary);
|
||||
}
|
||||
}
|
||||
|
||||
const nextItem = {
|
||||
...item,
|
||||
pointerKey: pointerKeyValue,
|
||||
watchedAt: watchedAtValue,
|
||||
video: video || null,
|
||||
metadata,
|
||||
};
|
||||
results.push(nextItem);
|
||||
|
||||
if (pointerKeyValue) {
|
||||
if (shouldStoreMetadata) {
|
||||
try {
|
||||
service?.setLocalMetadata?.(pointerKeyValue, {
|
||||
video: metadata.video,
|
||||
profile: metadata.profile,
|
||||
});
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Stored metadata cache entry for pointer.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
cachedVideo: Boolean(metadata.video),
|
||||
cachedProfile: Boolean(metadata.profile),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to persist metadata cache",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Failed to persist metadata cache entry.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (typeof service?.removeLocalMetadata === "function") {
|
||||
try {
|
||||
service.removeLocalMetadata(pointerKeyValue);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Cleared metadata cache entry for pointer.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to clear cached metadata",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Failed to clear cached metadata entry.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -484,11 +265,8 @@ function createWatchHistorySorter() {
|
||||
export function createWatchHistoryFeedDefinition({
|
||||
service = watchHistoryService,
|
||||
nostr = nostrService,
|
||||
metadataResolverFactory = createWatchHistoryMetadataResolver,
|
||||
shouldIncludeVideo,
|
||||
} = {}) {
|
||||
const metadataResolver = metadataResolverFactory({ nostr });
|
||||
|
||||
const blacklistStage = createBlacklistFilterStage({
|
||||
shouldIncludeVideo:
|
||||
typeof shouldIncludeVideo === "function"
|
||||
@@ -502,7 +280,7 @@ export function createWatchHistoryFeedDefinition({
|
||||
return {
|
||||
source: createWatchHistorySource({ service }),
|
||||
stages: [
|
||||
createWatchHistoryHydratorStage({ service, metadataResolver }),
|
||||
createWatchHistoryHydratorStage({ service }),
|
||||
blacklistStage,
|
||||
createWatchHistorySuppressionStage(),
|
||||
],
|
||||
|
||||
+11
-144
@@ -39,42 +39,6 @@ const WATCH_HISTORY_ENCRYPTION_FALLBACK_ORDER = Object.freeze([
|
||||
"nip04",
|
||||
]);
|
||||
|
||||
function cloneVideoMetadata(video) {
|
||||
if (!video || typeof video !== "object") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(video));
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] Failed to clone watch history video metadata", error);
|
||||
return { ...video };
|
||||
}
|
||||
}
|
||||
|
||||
function cloneProfileMetadata(profile) {
|
||||
if (!profile || typeof profile !== "object") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(profile));
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] Failed to clone watch history profile metadata", error);
|
||||
return { ...profile };
|
||||
}
|
||||
}
|
||||
|
||||
function clonePointerMetadata(metadata) {
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(metadata));
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] Failed to clone watch history pointer metadata", error);
|
||||
return { ...metadata };
|
||||
}
|
||||
}
|
||||
|
||||
function mergePointerDetails(target, source) {
|
||||
if (!target || typeof target !== "object" || !source || typeof source !== "object") {
|
||||
return target;
|
||||
@@ -96,19 +60,6 @@ function mergePointerDetails(target, source) {
|
||||
target.relay = source.relay.trim();
|
||||
}
|
||||
}
|
||||
if (!target.metadata && source.metadata && typeof source.metadata === "object") {
|
||||
target.metadata = clonePointerMetadata(source.metadata);
|
||||
} else if (target.metadata && source.metadata) {
|
||||
const merged = clonePointerMetadata(target.metadata) || {};
|
||||
const additional = clonePointerMetadata(source.metadata) || {};
|
||||
target.metadata = { ...merged, ...additional };
|
||||
}
|
||||
if (!target.video && source.video) {
|
||||
target.video = cloneVideoMetadata(source.video);
|
||||
}
|
||||
if (!target.profile && source.profile) {
|
||||
target.profile = cloneProfileMetadata(source.profile);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -146,29 +97,6 @@ function clonePointerItem(pointer) {
|
||||
cloned.session = true;
|
||||
}
|
||||
|
||||
const metadata = clonePointerMetadata(pointer.metadata);
|
||||
if (metadata) {
|
||||
cloned.metadata = metadata;
|
||||
}
|
||||
|
||||
const video = cloneVideoMetadata(pointer.video) || metadata?.video || null;
|
||||
if (video) {
|
||||
cloned.video = video;
|
||||
}
|
||||
|
||||
const profile = cloneProfileMetadata(pointer.profile) || metadata?.profile || null;
|
||||
if (profile) {
|
||||
cloned.profile = profile;
|
||||
}
|
||||
|
||||
if (pointer.completed === true || metadata?.completed === true) {
|
||||
cloned.completed = true;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(cloned.resumeAt) && Number.isFinite(metadata?.resumeAt)) {
|
||||
cloned.resumeAt = metadata.resumeAt;
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
@@ -544,15 +472,7 @@ function canonicalizeWatchHistoryItems(rawItems, maxItems = WATCH_HISTORY_MAX_IT
|
||||
}
|
||||
|
||||
function sanitizeWatchHistoryMetadata(metadata) {
|
||||
if (!metadata || typeof metadata !== "object") {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(metadata));
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] Failed to sanitize watch history metadata:", error);
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function serializeWatchHistoryItems(items) {
|
||||
@@ -580,47 +500,14 @@ function serializeWatchHistoryItems(items) {
|
||||
if (watchedAt !== undefined) {
|
||||
payload.watchedAt = watchedAt;
|
||||
}
|
||||
const metadata = clonePointerMetadata(item?.metadata);
|
||||
if (metadata) {
|
||||
payload.metadata = metadata;
|
||||
}
|
||||
const video = metadata?.video || cloneVideoMetadata(item?.video);
|
||||
if (video) {
|
||||
if (payload.metadata) {
|
||||
payload.metadata.video = payload.metadata.video || video;
|
||||
} else {
|
||||
payload.video = video;
|
||||
}
|
||||
}
|
||||
const profile = metadata?.profile || cloneProfileMetadata(item?.profile);
|
||||
if (profile) {
|
||||
if (payload.metadata) {
|
||||
payload.metadata.profile = payload.metadata.profile || profile;
|
||||
} else {
|
||||
payload.profile = profile;
|
||||
}
|
||||
}
|
||||
const resumeAt = Number.isFinite(item?.resumeAt)
|
||||
? Math.max(0, Math.floor(item.resumeAt))
|
||||
: undefined;
|
||||
if (resumeAt !== undefined) {
|
||||
if (payload.metadata) {
|
||||
if (!Number.isFinite(payload.metadata.resumeAt)) {
|
||||
payload.metadata.resumeAt = resumeAt;
|
||||
}
|
||||
} else {
|
||||
payload.resumeAt = resumeAt;
|
||||
}
|
||||
payload.resumeAt = resumeAt;
|
||||
}
|
||||
if (item?.completed === true) {
|
||||
if (payload.metadata) {
|
||||
payload.metadata.completed = true;
|
||||
} else {
|
||||
payload.completed = true;
|
||||
}
|
||||
}
|
||||
if (payload.metadata && !Object.keys(payload.metadata).length) {
|
||||
delete payload.metadata;
|
||||
payload.completed = true;
|
||||
}
|
||||
return payload;
|
||||
})
|
||||
@@ -1106,7 +993,6 @@ class WatchHistoryManager {
|
||||
items = Object.values(records).flat();
|
||||
}
|
||||
|
||||
const metadata = sanitizeWatchHistoryMetadata(entry?.metadata);
|
||||
const snapshotId = typeof entry?.snapshotId === "string" ? entry.snapshotId : "";
|
||||
const fingerprint = typeof entry?.fingerprint === "string" ? entry.fingerprint : "";
|
||||
sanitizedActors[actorKey] = {
|
||||
@@ -1119,7 +1005,7 @@ class WatchHistoryManager {
|
||||
savedAt,
|
||||
records,
|
||||
items,
|
||||
metadata,
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
const storage = {
|
||||
@@ -1169,7 +1055,6 @@ class WatchHistoryManager {
|
||||
records = canonicalizeWatchHistoryItems(entry.items, WATCH_HISTORY_MAX_ITEMS);
|
||||
}
|
||||
|
||||
const metadata = sanitizeWatchHistoryMetadata(entry.metadata);
|
||||
const snapshotId = typeof entry.snapshotId === "string" ? entry.snapshotId : "";
|
||||
const fingerprint = typeof entry.fingerprint === "string" ? entry.fingerprint : "";
|
||||
const savedAt = Number.isFinite(entry.savedAt) && entry.savedAt > 0 ? entry.savedAt : now;
|
||||
@@ -1183,7 +1068,7 @@ class WatchHistoryManager {
|
||||
fingerprint,
|
||||
savedAt,
|
||||
records,
|
||||
metadata,
|
||||
metadata: {},
|
||||
};
|
||||
mutated = true;
|
||||
}
|
||||
@@ -1362,17 +1247,13 @@ class WatchHistoryManager {
|
||||
return fetchResult;
|
||||
}
|
||||
const storageEntry = this.getStorage().actors?.[actorKey];
|
||||
const metadata = sanitizeWatchHistoryMetadata(storageEntry?.metadata);
|
||||
const alreadyAttempted = metadata.autoSnapshotAttempted === true;
|
||||
|
||||
const records = storageEntry?.records || {};
|
||||
const flatItems = Object.values(records).flat();
|
||||
|
||||
if (!flatItems.length || alreadyAttempted) {
|
||||
if (!flatItems.length) {
|
||||
return fetchResult;
|
||||
}
|
||||
metadata.autoSnapshotAttempted = true;
|
||||
metadata.autoSnapshotAttemptedAt = Date.now();
|
||||
|
||||
// We publish all records
|
||||
const publishResult = await this.publishRecords(records, {
|
||||
@@ -1390,7 +1271,7 @@ class WatchHistoryManager {
|
||||
chunkEvents: [],
|
||||
savedAt: Date.now(),
|
||||
fingerprint,
|
||||
metadata,
|
||||
metadata: {},
|
||||
};
|
||||
this.cache.set(actorKey, entry);
|
||||
this.persistEntry(actorKey, entry);
|
||||
@@ -1776,16 +1657,6 @@ class WatchHistoryManager {
|
||||
success: !!publishResult.ok,
|
||||
retryable: !!publishResult.retryable,
|
||||
});
|
||||
const metadata = sanitizeWatchHistoryMetadata(cachedEntry.metadata);
|
||||
metadata.updatedAt = Date.now();
|
||||
metadata.status = publishResult.ok ? "ok" : "error";
|
||||
metadata.lastPublishResults = publishResult.publishResults; // This will likely be array or complex obj
|
||||
metadata.skippedCount = publishResult.skippedCount || 0;
|
||||
if (!publishResult.ok) {
|
||||
metadata.lastError = publishResult.error || "publish-failed";
|
||||
} else {
|
||||
delete metadata.lastError;
|
||||
}
|
||||
const entry = {
|
||||
actor: resolvedActor,
|
||||
records,
|
||||
@@ -1794,7 +1665,7 @@ class WatchHistoryManager {
|
||||
pointerEvent: publishResult.pointerEvent || cachedEntry.pointerEvent || null,
|
||||
savedAt: Date.now(),
|
||||
fingerprint,
|
||||
metadata,
|
||||
metadata: {},
|
||||
};
|
||||
this.cache.set(actorKey, entry);
|
||||
this.persistEntry(actorKey, entry);
|
||||
@@ -1938,7 +1809,7 @@ class WatchHistoryManager {
|
||||
pointerEvent: null,
|
||||
savedAt: now,
|
||||
fingerprint,
|
||||
metadata: sanitizeWatchHistoryMetadata(storageEntry?.metadata),
|
||||
metadata: {},
|
||||
};
|
||||
this.cache.set(actorKey, entry);
|
||||
this.persistEntry(actorKey, entry);
|
||||
@@ -2094,10 +1965,6 @@ class WatchHistoryManager {
|
||||
resolvedActor,
|
||||
records,
|
||||
);
|
||||
const metadata = sanitizeWatchHistoryMetadata(
|
||||
this.getStorage().actors?.[actorKey]?.metadata,
|
||||
);
|
||||
metadata.lastFetchedAt = now;
|
||||
|
||||
const entry = {
|
||||
actor: resolvedActor,
|
||||
@@ -2107,7 +1974,7 @@ class WatchHistoryManager {
|
||||
pointerEvent: latestEvent,
|
||||
savedAt: now,
|
||||
fingerprint,
|
||||
metadata,
|
||||
metadata: {},
|
||||
};
|
||||
this.cache.set(actorKey, entry);
|
||||
this.persistEntry(actorKey, entry);
|
||||
@@ -2196,7 +2063,7 @@ class WatchHistoryManager {
|
||||
pointerEvent: fetchResult.pointerEvent || null,
|
||||
savedAt: Date.now(),
|
||||
fingerprint,
|
||||
metadata: sanitizeWatchHistoryMetadata(storage.actors?.[actorKey]?.metadata),
|
||||
metadata: {},
|
||||
};
|
||||
this.cache.set(actorKey, entry);
|
||||
this.persistEntry(actorKey, entry);
|
||||
|
||||
@@ -72,7 +72,6 @@ export default class WatchHistoryTelemetry {
|
||||
? Math.max(0, viewThresholdSeconds)
|
||||
: DEFAULT_VIEW_THRESHOLD_SECONDS;
|
||||
|
||||
this.watchHistoryMetadataEnabled = false;
|
||||
this.watchHistoryPreferenceUnsubscribe = null;
|
||||
this.playbackTelemetryState = null;
|
||||
this.loggedViewPointerKeys = new Set();
|
||||
@@ -81,17 +80,14 @@ export default class WatchHistoryTelemetry {
|
||||
destroy() {
|
||||
this.resetPlaybackLoggingState();
|
||||
this._clearPreferenceSubscription();
|
||||
this.watchHistoryMetadataEnabled = false;
|
||||
}
|
||||
|
||||
async initPreferenceSync() {
|
||||
this.watchHistoryMetadataEnabled = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
refreshPreferenceSettings() {
|
||||
this.watchHistoryMetadataEnabled = false;
|
||||
return this.watchHistoryMetadataEnabled;
|
||||
return false;
|
||||
}
|
||||
|
||||
isMetadataPreferenceEnabled() {
|
||||
@@ -99,54 +95,11 @@ export default class WatchHistoryTelemetry {
|
||||
}
|
||||
|
||||
persistMetadataForVideo(video, pointerInfo) {
|
||||
if (
|
||||
!this.watchHistoryMetadataEnabled ||
|
||||
!pointerInfo ||
|
||||
!pointerInfo.key ||
|
||||
typeof this.watchHistoryService?.setLocalMetadata !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!video || typeof video !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
// Persist only sanitized metadata so local history never stores playback
|
||||
// transports or magnet fingerprints.
|
||||
video: sanitizeVideoMetadata(video),
|
||||
};
|
||||
|
||||
try {
|
||||
this.watchHistoryService.setLocalMetadata(pointerInfo.key, metadata);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[watchHistoryTelemetry] Failed to persist local metadata for pointer:",
|
||||
pointerInfo.key,
|
||||
error,
|
||||
);
|
||||
}
|
||||
// Deprecated
|
||||
}
|
||||
|
||||
dropMetadata(pointerKey) {
|
||||
if (!pointerKey || typeof pointerKey !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.watchHistoryService?.removeLocalMetadata !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.watchHistoryService.removeLocalMetadata(pointerKey);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[watchHistoryTelemetry] Failed to remove cached metadata for pointer:",
|
||||
pointerKey,
|
||||
error,
|
||||
);
|
||||
}
|
||||
// Deprecated
|
||||
}
|
||||
|
||||
async handleRemoval(payload = {}) {
|
||||
@@ -272,58 +225,11 @@ export default class WatchHistoryTelemetry {
|
||||
try {
|
||||
const canUseWatchHistoryService =
|
||||
typeof this.watchHistoryService?.publishView === "function";
|
||||
const resolveWatchActor = () => {
|
||||
const normalizedUser = this.normalizeHexPubkey(
|
||||
this.getActiveUserPubkey(),
|
||||
);
|
||||
if (normalizedUser) {
|
||||
return normalizedUser;
|
||||
}
|
||||
|
||||
const normalizedClient = this.normalizeHexPubkey(
|
||||
this.nostrClient?.pubkey,
|
||||
);
|
||||
if (normalizedClient) {
|
||||
return normalizedClient;
|
||||
}
|
||||
|
||||
const normalizedSession = this.normalizeHexPubkey(
|
||||
this.nostrClient?.sessionActor?.pubkey,
|
||||
);
|
||||
if (normalizedSession) {
|
||||
return normalizedSession;
|
||||
}
|
||||
|
||||
if (normalizeString(this.nostrClient?.pubkey)) {
|
||||
return normalizeString(this.nostrClient.pubkey).toLowerCase();
|
||||
}
|
||||
|
||||
if (normalizeString(this.nostrClient?.sessionActor?.pubkey)) {
|
||||
return normalizeString(
|
||||
this.nostrClient.sessionActor.pubkey,
|
||||
).toLowerCase();
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const activeWatchActor = resolveWatchActor();
|
||||
const watchMetadata = {};
|
||||
if (activeWatchActor) {
|
||||
watchMetadata.actor = activeWatchActor;
|
||||
}
|
||||
if (state.videoMetadata) {
|
||||
watchMetadata.video = state.videoMetadata;
|
||||
}
|
||||
const metadataPayload = Object.keys(watchMetadata).length
|
||||
? watchMetadata
|
||||
: undefined;
|
||||
|
||||
if (canUseWatchHistoryService) {
|
||||
viewResult = await this.watchHistoryService.publishView(
|
||||
thresholdPointer,
|
||||
undefined,
|
||||
metadataPayload,
|
||||
);
|
||||
} else if (typeof this.nostrClient?.recordVideoView === "function") {
|
||||
viewResult = await this.nostrClient.recordVideoView(
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
// js/watchHistoryMetadata.js
|
||||
|
||||
import { getApplication } from "./applicationContext.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import { nostrClient } from "./nostrClientFacade.js";
|
||||
import { logWatchHistoryDebug } from "./watchHistoryDebug.js";
|
||||
|
||||
function logInfo(message, details) {
|
||||
logWatchHistoryDebug("watchHistoryMetadata", "info", message, details);
|
||||
}
|
||||
|
||||
function logWarn(message, details) {
|
||||
logWatchHistoryDebug("watchHistoryMetadata", "warn", message, details);
|
||||
}
|
||||
|
||||
function normalizeVideoCandidate(candidate) {
|
||||
if (!candidate || typeof candidate !== "object") {
|
||||
logInfo("Discarded non-object video candidate while normalizing.");
|
||||
return null;
|
||||
}
|
||||
if (candidate.deleted) {
|
||||
logInfo("Discarded deleted video candidate while normalizing.", {
|
||||
eventId: candidate.id || null,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function createWatchHistoryMetadataResolver({
|
||||
appResolver = getApplication,
|
||||
nostr = nostrService,
|
||||
client = nostrClient,
|
||||
} = {}) {
|
||||
const caches = {
|
||||
activeVideos: null,
|
||||
catalogPromise: null,
|
||||
};
|
||||
|
||||
function getAppInstance() {
|
||||
try {
|
||||
return typeof appResolver === "function" ? appResolver() : getApplication();
|
||||
} catch (error) {
|
||||
logWarn("Failed to resolve application instance.", { error });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveVideo(pointer) {
|
||||
if (!pointer || typeof pointer !== "object") {
|
||||
logInfo("Received invalid pointer when resolving video metadata.", {
|
||||
pointerType: typeof pointer,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const app = getAppInstance();
|
||||
const type = pointer.type === "a" ? "a" : "e";
|
||||
const value = typeof pointer.value === "string" ? pointer.value.trim() : "";
|
||||
logInfo("Attempting to resolve watch history pointer.", {
|
||||
pointerType: type,
|
||||
pointerValue: value || null,
|
||||
hasAppInstance: Boolean(app),
|
||||
});
|
||||
if (!value) {
|
||||
logInfo("Pointer value was empty after trimming. Skipping resolution.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === "e") {
|
||||
const fromAppCache = app?.videosMap?.get?.(value);
|
||||
const normalizedAppCache = normalizeVideoCandidate(fromAppCache);
|
||||
if (normalizedAppCache) {
|
||||
logInfo("Resolved pointer from application videos map.", {
|
||||
pointerValue: value,
|
||||
});
|
||||
return normalizedAppCache;
|
||||
}
|
||||
|
||||
const fromClientCache = client?.allEvents instanceof Map
|
||||
? client.allEvents.get(value)
|
||||
: null;
|
||||
const normalizedClientCache = normalizeVideoCandidate(fromClientCache);
|
||||
if (normalizedClientCache) {
|
||||
logInfo("Resolved pointer from nostr client event cache.", {
|
||||
pointerValue: value,
|
||||
});
|
||||
return normalizedClientCache;
|
||||
}
|
||||
|
||||
if (typeof app?.getOldEventById === "function") {
|
||||
try {
|
||||
const fetched = await app.getOldEventById(value);
|
||||
const normalized = normalizeVideoCandidate(fetched);
|
||||
if (normalized) {
|
||||
logInfo("Resolved pointer via app.getOldEventById.", {
|
||||
pointerValue: value,
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
} catch (error) {
|
||||
logWarn("Failed to fetch old event by id.", {
|
||||
pointerValue: value,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof client?.getEventById === "function") {
|
||||
try {
|
||||
const fetched = await client.getEventById(value);
|
||||
const normalized = normalizeVideoCandidate(fetched);
|
||||
if (normalized) {
|
||||
logInfo("Resolved pointer via nostrClient.getEventById.", {
|
||||
pointerValue: value,
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
} catch (error) {
|
||||
logWarn("Failed to fetch event by id via nostrClient.", {
|
||||
pointerValue: value,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logInfo("Failed to resolve pointer by event id.", { pointerValue: value });
|
||||
return null;
|
||||
}
|
||||
|
||||
const compareAddress = (candidate) => {
|
||||
if (!candidate || typeof candidate !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (typeof app?.getVideoAddressPointer !== "function") {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const address = app.getVideoAddressPointer(candidate);
|
||||
const isMatch = typeof address === "string" && address.trim() === value;
|
||||
if (isMatch) {
|
||||
logInfo("Candidate matched address pointer.", {
|
||||
pointerValue: value,
|
||||
candidateId: candidate.id || null,
|
||||
});
|
||||
}
|
||||
return isMatch;
|
||||
} catch (error) {
|
||||
logWarn("Failed to compute address pointer for candidate.", {
|
||||
pointerValue: value,
|
||||
error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (app?.videosMap instanceof Map) {
|
||||
for (const cached of app.videosMap.values()) {
|
||||
if (compareAddress(cached)) {
|
||||
const normalized = normalizeVideoCandidate(cached);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blacklist = app?.blacklistedEventIds instanceof Set
|
||||
? app.blacklistedEventIds
|
||||
: undefined;
|
||||
const filterOptions = {
|
||||
blacklistedEventIds: blacklist,
|
||||
isAuthorBlocked: (pubkey) => {
|
||||
try {
|
||||
return (
|
||||
typeof app?.isAuthorBlocked === "function" &&
|
||||
app.isAuthorBlocked(pubkey)
|
||||
);
|
||||
} catch (error) {
|
||||
logWarn("Failed to evaluate isAuthorBlocked callback.", {
|
||||
pointerValue: value,
|
||||
error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!Array.isArray(caches.activeVideos)) {
|
||||
try {
|
||||
caches.activeVideos = nostr.getFilteredActiveVideos
|
||||
? nostr.getFilteredActiveVideos(filterOptions)
|
||||
: [];
|
||||
logInfo("Loaded active videos cache for pointer resolution.", {
|
||||
pointerValue: value,
|
||||
candidateCount: Array.isArray(caches.activeVideos)
|
||||
? caches.activeVideos.length
|
||||
: 0,
|
||||
});
|
||||
} catch (error) {
|
||||
logWarn("Failed to read active videos cache.", {
|
||||
pointerValue: value,
|
||||
error,
|
||||
});
|
||||
caches.activeVideos = [];
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of Array.isArray(caches.activeVideos)
|
||||
? caches.activeVideos
|
||||
: []) {
|
||||
if (compareAddress(candidate)) {
|
||||
const normalized = normalizeVideoCandidate(candidate);
|
||||
if (normalized) {
|
||||
logInfo("Resolved pointer from active videos cache.", {
|
||||
pointerValue: value,
|
||||
candidateId: normalized.id || null,
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!caches.catalogPromise && typeof nostr.fetchVideos === "function") {
|
||||
logInfo("Fetching video catalog for pointer resolution.", {
|
||||
pointerValue: value,
|
||||
});
|
||||
caches.catalogPromise = nostr
|
||||
.fetchVideos(filterOptions)
|
||||
.catch((error) => {
|
||||
logWarn("Failed to fetch video catalog while resolving pointer.", {
|
||||
pointerValue: value,
|
||||
error,
|
||||
});
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
if (caches.catalogPromise) {
|
||||
try {
|
||||
const catalog = await caches.catalogPromise;
|
||||
for (const candidate of Array.isArray(catalog) ? catalog : []) {
|
||||
if (compareAddress(candidate)) {
|
||||
const normalized = normalizeVideoCandidate(candidate);
|
||||
if (normalized) {
|
||||
logInfo("Resolved pointer from fetched video catalog.", {
|
||||
pointerValue: value,
|
||||
candidateId: normalized.id || null,
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logWarn("Failed to resolve pointer from fetched catalog.", {
|
||||
pointerValue: value,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logInfo("Failed to resolve pointer after exhausting lookup strategies.", {
|
||||
pointerValue: value,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveProfile(pubkey) {
|
||||
if (typeof pubkey !== "string" || !pubkey.trim()) {
|
||||
return null;
|
||||
}
|
||||
const app = getAppInstance();
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const cacheEntry =
|
||||
typeof app.getProfileCacheEntry === "function"
|
||||
? app.getProfileCacheEntry(pubkey)
|
||||
: null;
|
||||
if (cacheEntry && typeof cacheEntry === "object") {
|
||||
logInfo("Resolved profile from application cache.", { pubkey });
|
||||
return cacheEntry.profile || null;
|
||||
}
|
||||
} catch (error) {
|
||||
logWarn("Failed to read profile cache entry.", { pubkey, error });
|
||||
}
|
||||
logInfo("Profile cache miss during watch history hydration.", { pubkey });
|
||||
return null;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
caches.activeVideos = null;
|
||||
logInfo("Reset active videos cache for watch history metadata resolver.");
|
||||
caches.catalogPromise = null;
|
||||
logInfo("Reset catalog promise for watch history metadata resolver.");
|
||||
}
|
||||
|
||||
return {
|
||||
resolveVideo,
|
||||
resolveProfile,
|
||||
reset,
|
||||
caches,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -698,22 +698,11 @@ async function updateFingerprintCache(actorKey, items) {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
async function publishView(pointerInput, createdAt) {
|
||||
const recordOptions = {};
|
||||
if (Number.isFinite(createdAt)) {
|
||||
recordOptions.created_at = createdAt;
|
||||
}
|
||||
if (metadata && typeof metadata === "object") {
|
||||
if (Array.isArray(metadata.additionalTags)) {
|
||||
recordOptions.additionalTags = metadata.additionalTags;
|
||||
}
|
||||
if (Array.isArray(metadata.relays)) {
|
||||
recordOptions.relays = metadata.relays;
|
||||
}
|
||||
if (metadata.content != null) {
|
||||
recordOptions.content = metadata.content;
|
||||
}
|
||||
}
|
||||
|
||||
const viewResult = await nostrClient.recordVideoView(pointerInput, recordOptions);
|
||||
|
||||
@@ -722,7 +711,6 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
{
|
||||
pointer: pointerInput,
|
||||
createdAt,
|
||||
hasMetadata: !!metadata,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -736,7 +724,6 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
}
|
||||
|
||||
const actorCandidate =
|
||||
(typeof metadata.actor === "string" && metadata.actor.trim()) ||
|
||||
viewResult?.event?.pubkey ||
|
||||
nostrClient?.pubkey ||
|
||||
nostrClient?.sessionActor?.pubkey ||
|
||||
@@ -775,7 +762,6 @@ async function publishView(pointerInput, createdAt, metadata = {}) {
|
||||
viewResult?.event?.created_at
|
||||
);
|
||||
|
||||
delete normalizedPointer.metadata;
|
||||
delete normalizedPointer.video;
|
||||
delete normalizedPointer.profile;
|
||||
delete normalizedPointer.resumeAt;
|
||||
|
||||
@@ -1160,7 +1160,6 @@ async function testWatchHistoryPartialRelayRetry() {
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "partial-pointer" },
|
||||
Math.floor(Date.now() / 1000),
|
||||
{ actor },
|
||||
);
|
||||
|
||||
const queuedBefore = watchHistoryService.getQueuedPointers(actor);
|
||||
@@ -1359,7 +1358,6 @@ async function testWatchHistorySnapshotRetainsNewQueueEntriesDuringPublish() {
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "inflight-initial" },
|
||||
createdAt,
|
||||
{ actor },
|
||||
);
|
||||
|
||||
const queuedBefore = watchHistoryService.getQueuedPointers(actor);
|
||||
@@ -1401,7 +1399,6 @@ async function testWatchHistorySnapshotRetainsNewQueueEntriesDuringPublish() {
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "inflight-new" },
|
||||
createdAt,
|
||||
{ actor },
|
||||
);
|
||||
|
||||
const queuedDuring = watchHistoryService.getQueuedPointers(actor);
|
||||
@@ -1566,19 +1563,16 @@ async function testWatchHistoryServiceIntegration() {
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "video-one" },
|
||||
viewCreatedAt,
|
||||
{ actor, video: pointerVideo },
|
||||
);
|
||||
viewCreatedAt += 60;
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "video-one" },
|
||||
viewCreatedAt,
|
||||
{ actor, video: pointerVideo },
|
||||
);
|
||||
viewCreatedAt += 30;
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "a", value: "30023:pub:episode" },
|
||||
viewCreatedAt,
|
||||
{ actor },
|
||||
);
|
||||
|
||||
const queued = watchHistoryService.getQueuedPointers(actor);
|
||||
@@ -1823,7 +1817,6 @@ async function testWatchHistorySnapshotMergesQueuedWithCachedItems() {
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "fresh-entry" },
|
||||
1_700_000_200,
|
||||
{ actor },
|
||||
);
|
||||
|
||||
const mergeResult = await watchHistoryService.snapshot(null, {
|
||||
@@ -2366,7 +2359,6 @@ async function testWatchHistoryLocalFallbackWhenDisabled() {
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "local-pointer" },
|
||||
createdAt,
|
||||
{ actor },
|
||||
);
|
||||
|
||||
const queued = watchHistoryService.getQueuedPointers(actor);
|
||||
|
||||
@@ -83,39 +83,10 @@ async function flushAsyncOperations() {
|
||||
}
|
||||
|
||||
async function testMetadataPreferenceSync() {
|
||||
let subscribeTopic = null;
|
||||
let subscribeHandler = null;
|
||||
let cleared = false;
|
||||
|
||||
const { telemetry } = createTelemetry({
|
||||
serviceOverrides: {
|
||||
subscribe: (topic, handler) => {
|
||||
subscribeTopic = topic;
|
||||
subscribeHandler = handler;
|
||||
return () => {};
|
||||
},
|
||||
clearLocalMetadata: () => {
|
||||
cleared = true;
|
||||
},
|
||||
getSettings: () => ({ metadata: { storeLocally: true } }),
|
||||
},
|
||||
options: {
|
||||
getActiveUserPubkey: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const { telemetry } = createTelemetry();
|
||||
const unsubscribe = await telemetry.initPreferenceSync();
|
||||
assert.equal(subscribeTopic, "metadata-preference");
|
||||
assert.equal(typeof unsubscribe, "function");
|
||||
assert.equal(telemetry.isMetadataPreferenceEnabled(), true);
|
||||
|
||||
subscribeHandler({ enabled: false });
|
||||
assert.equal(unsubscribe, null, "initPreferenceSync should no-op");
|
||||
assert.equal(telemetry.isMetadataPreferenceEnabled(), false);
|
||||
assert.equal(cleared, true);
|
||||
|
||||
subscribeHandler({ enabled: true });
|
||||
assert.equal(telemetry.isMetadataPreferenceEnabled(), true);
|
||||
|
||||
}
|
||||
|
||||
async function testPreparePlaybackLoggingPublishesOnce() {
|
||||
@@ -127,8 +98,8 @@ async function testPreparePlaybackLoggingPublishesOnce() {
|
||||
|
||||
const { telemetry } = createTelemetry({
|
||||
serviceOverrides: {
|
||||
publishView: async (ptr, _relay, metadata) => {
|
||||
publishCalls.push({ ptr, metadata });
|
||||
publishView: async (ptr, _relay) => {
|
||||
publishCalls.push({ ptr });
|
||||
return {
|
||||
ok: true,
|
||||
event: { pubkey: hexPubkey },
|
||||
@@ -174,7 +145,7 @@ async function testPreparePlaybackLoggingPublishesOnce() {
|
||||
);
|
||||
}
|
||||
|
||||
async function testPreparePlaybackLoggingIncludesVideoMetadata() {
|
||||
async function testPreparePlaybackLoggingIgnoresMetadata() {
|
||||
const publishCalls = [];
|
||||
|
||||
const pointer = { type: "e", value: "event-id" };
|
||||
@@ -182,8 +153,8 @@ async function testPreparePlaybackLoggingIncludesVideoMetadata() {
|
||||
|
||||
const { telemetry } = createTelemetry({
|
||||
serviceOverrides: {
|
||||
publishView: async (ptr, _relay, metadata) => {
|
||||
publishCalls.push({ ptr, metadata });
|
||||
publishView: async (ptr, _relay) => {
|
||||
publishCalls.push({ ptr });
|
||||
return {
|
||||
ok: true,
|
||||
event: { pubkey: hexPubkey },
|
||||
@@ -216,14 +187,7 @@ async function testPreparePlaybackLoggingIncludesVideoMetadata() {
|
||||
|
||||
assert.equal(publishCalls.length, 1, "publishView should be invoked once");
|
||||
assert.equal(publishCalls[0].ptr, pointer);
|
||||
assert.ok(publishCalls[0].metadata, "metadata payload should exist");
|
||||
assert.deepEqual(publishCalls[0].metadata.video, {
|
||||
id: "evt1",
|
||||
title: "Example Video",
|
||||
thumbnail: "https://example.com/thumb.jpg",
|
||||
pubkey: hexPubkey,
|
||||
created_at: 1_700_000_000,
|
||||
});
|
||||
assert.equal(publishCalls[0].metadata, undefined, "metadata should NOT be passed");
|
||||
}
|
||||
|
||||
async function testCancelPlaybackLoggingFlushes() {
|
||||
@@ -254,7 +218,7 @@ async function testCancelPlaybackLoggingFlushes() {
|
||||
|
||||
await testMetadataPreferenceSync();
|
||||
await testPreparePlaybackLoggingPublishesOnce();
|
||||
await testPreparePlaybackLoggingIncludesVideoMetadata();
|
||||
await testPreparePlaybackLoggingIgnoresMetadata();
|
||||
await testCancelPlaybackLoggingFlushes();
|
||||
|
||||
console.log("watch-history-telemetry tests passed");
|
||||
Reference in new issue
Block a user