mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Preserve playback metadata in watch history
This commit is contained in:
1 parent
b5ee063260
commit
4d367109d5
5 files changed
+302
-3
No files matched your search
+1
-1
@@ -338,7 +338,7 @@ function getPointerVideoId(video, pointer) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function buildHistoryCard({
|
||||
export function buildHistoryCard({
|
||||
item,
|
||||
video,
|
||||
profile,
|
||||
|
||||
@@ -1374,6 +1374,9 @@ function cloneVideoMetadata(video) {
|
||||
created_at: createdAt,
|
||||
url: typeof video.url === "string" ? video.url : "",
|
||||
magnet: typeof video.magnet === "string" ? video.magnet : "",
|
||||
infoHash: typeof video.infoHash === "string" ? video.infoHash : "",
|
||||
legacyInfoHash:
|
||||
typeof video.legacyInfoHash === "string" ? video.legacyInfoHash : "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ function sanitizeVideoMetadata(video) {
|
||||
thumbnail: typeof video.thumbnail === "string" ? video.thumbnail : "",
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : "",
|
||||
created_at: createdAt,
|
||||
url: typeof video.url === "string" ? video.url : "",
|
||||
magnet: typeof video.magnet === "string" ? video.magnet : "",
|
||||
infoHash: typeof video.infoHash === "string" ? video.infoHash : "",
|
||||
legacyInfoHash:
|
||||
typeof video.legacyInfoHash === "string" ? video.legacyInfoHash : "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -253,6 +253,8 @@ function sanitizeVideoForStorage(video) {
|
||||
pubkey: typeof video.pubkey === "string" ? video.pubkey : "",
|
||||
created_at: createdAt,
|
||||
infoHash: typeof video.infoHash === "string" ? video.infoHash : "",
|
||||
legacyInfoHash:
|
||||
typeof video.legacyInfoHash === "string" ? video.legacyInfoHash : "",
|
||||
mode: typeof video.mode === "string" ? video.mode : "",
|
||||
isPrivate: video?.isPrivate === true,
|
||||
description:
|
||||
@@ -274,6 +276,13 @@ function sanitizeVideoForHistory(video) {
|
||||
created_at: sanitized.created_at,
|
||||
url: sanitized.url,
|
||||
magnet: sanitized.magnet,
|
||||
infoHash: sanitized.infoHash,
|
||||
legacyInfoHash:
|
||||
typeof video?.legacyInfoHash === "string"
|
||||
? video.legacyInfoHash
|
||||
: typeof sanitized.legacyInfoHash === "string"
|
||||
? sanitized.legacyInfoHash
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ const {
|
||||
normalizeActorKey,
|
||||
} = await import("../js/nostr.js");
|
||||
const { watchHistoryService } = await import("../js/watchHistoryService.js");
|
||||
const { buildHistoryCard } = await import("../js/historyView.js");
|
||||
const { getApplication, setApplication } = await import(
|
||||
"../js/applicationContext.js"
|
||||
);
|
||||
@@ -218,6 +219,43 @@ function createFakeSimplePool() {
|
||||
|
||||
const poolHarness = createFakeSimplePool();
|
||||
nostrClient.pool = poolHarness;
|
||||
|
||||
function extractVideoMetadataFromItem(item) {
|
||||
if (!item || typeof item !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const directVideo = item.video;
|
||||
if (directVideo && typeof directVideo === "object") {
|
||||
return directVideo;
|
||||
}
|
||||
|
||||
const metadataVideo =
|
||||
item.metadata && typeof item.metadata === "object"
|
||||
? item.metadata.video
|
||||
: null;
|
||||
if (metadataVideo && typeof metadataVideo === "object") {
|
||||
return metadataVideo;
|
||||
}
|
||||
|
||||
const pointer = item.pointer && typeof item.pointer === "object"
|
||||
? item.pointer
|
||||
: null;
|
||||
if (pointer) {
|
||||
if (pointer.video && typeof pointer.video === "object") {
|
||||
return pointer.video;
|
||||
}
|
||||
const pointerMetadata =
|
||||
pointer.metadata && typeof pointer.metadata === "object"
|
||||
? pointer.metadata.video
|
||||
: null;
|
||||
if (pointerMetadata && typeof pointerMetadata === "object") {
|
||||
return pointerMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
nostrClient.relays = ["wss://relay.test"];
|
||||
nostrClient.readRelays = ["wss://relay.test"];
|
||||
nostrClient.writeRelays = ["wss://relay.test"];
|
||||
@@ -1252,6 +1290,15 @@ async function testWatchHistoryServiceIntegration() {
|
||||
poolHarness.setResolver(() => ({ ok: true }));
|
||||
|
||||
const actor = "service-actor";
|
||||
const pointerVideo = {
|
||||
id: "video-one",
|
||||
title: "Video One",
|
||||
url: "https://cdn.example/video-one.mp4",
|
||||
magnet:
|
||||
"magnet:?xt=urn:btih:89abcdef0123456789abcdef0123456789abcdef",
|
||||
infoHash: "89abcdef0123456789abcdef0123456789abcdef",
|
||||
legacyInfoHash: "89abcdef0123456789abcdef0123456789abcdef",
|
||||
};
|
||||
const restoreCrypto = installSessionCrypto({ privateKey: "service-priv" });
|
||||
const originalEnsure = nostrClient.ensureSessionActor;
|
||||
const originalSession = nostrClient.sessionActor;
|
||||
@@ -1290,13 +1337,13 @@ async function testWatchHistoryServiceIntegration() {
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "video-one" },
|
||||
viewCreatedAt,
|
||||
{ actor },
|
||||
{ actor, video: pointerVideo },
|
||||
);
|
||||
viewCreatedAt += 60;
|
||||
await watchHistoryService.publishView(
|
||||
{ type: "e", value: "video-one" },
|
||||
viewCreatedAt,
|
||||
{ actor },
|
||||
{ actor, video: pointerVideo },
|
||||
);
|
||||
viewCreatedAt += 30;
|
||||
await watchHistoryService.publishView(
|
||||
@@ -1313,6 +1360,36 @@ async function testWatchHistoryServiceIntegration() {
|
||||
reason: "integration",
|
||||
});
|
||||
assert.ok(snapshotResult.ok, "snapshot should publish queued pointers");
|
||||
const snapshotItems = Array.isArray(snapshotResult.items)
|
||||
? snapshotResult.items
|
||||
: [];
|
||||
const snapshotVideo = extractVideoMetadataFromItem(
|
||||
snapshotItems.find(
|
||||
(entry) =>
|
||||
(entry?.value || entry?.pointer?.value || "") === "video-one",
|
||||
),
|
||||
);
|
||||
assert(snapshotVideo, "snapshot should retain pointer video metadata");
|
||||
assert.equal(
|
||||
snapshotVideo?.url,
|
||||
pointerVideo.url,
|
||||
"snapshot pointer video should preserve url",
|
||||
);
|
||||
assert.equal(
|
||||
snapshotVideo?.magnet,
|
||||
pointerVideo.magnet,
|
||||
"snapshot pointer video should preserve magnet",
|
||||
);
|
||||
assert.equal(
|
||||
snapshotVideo?.infoHash,
|
||||
pointerVideo.infoHash,
|
||||
"snapshot pointer video should preserve infoHash",
|
||||
);
|
||||
assert.equal(
|
||||
snapshotVideo?.legacyInfoHash,
|
||||
pointerVideo.legacyInfoHash,
|
||||
"snapshot pointer video should preserve legacy info hash",
|
||||
);
|
||||
assert.equal(
|
||||
watchHistoryService.getQueuedPointers(actor).length,
|
||||
0,
|
||||
@@ -1325,6 +1402,33 @@ async function testWatchHistoryServiceIntegration() {
|
||||
snapshotResult.items,
|
||||
"loadLatest should return decrypted canonical pointers",
|
||||
);
|
||||
const resolvedVideo = extractVideoMetadataFromItem(
|
||||
resolvedItems.find(
|
||||
(entry) =>
|
||||
(entry?.value || entry?.pointer?.value || "") === "video-one",
|
||||
),
|
||||
);
|
||||
assert(resolvedVideo, "decrypted history should include pointer video");
|
||||
assert.equal(
|
||||
resolvedVideo?.url,
|
||||
pointerVideo.url,
|
||||
"decrypted pointer video should expose url",
|
||||
);
|
||||
assert.equal(
|
||||
resolvedVideo?.magnet,
|
||||
pointerVideo.magnet,
|
||||
"decrypted pointer video should expose magnet",
|
||||
);
|
||||
assert.equal(
|
||||
resolvedVideo?.infoHash,
|
||||
pointerVideo.infoHash,
|
||||
"decrypted pointer video should expose infoHash",
|
||||
);
|
||||
assert.equal(
|
||||
resolvedVideo?.legacyInfoHash,
|
||||
pointerVideo.legacyInfoHash,
|
||||
"decrypted pointer video should expose legacy info hash",
|
||||
);
|
||||
assert(resolvedItems[0].watchedAt >= resolvedItems[1].watchedAt);
|
||||
assert.equal(
|
||||
resolvedItems[0].session,
|
||||
@@ -1359,6 +1463,183 @@ async function testWatchHistoryServiceIntegration() {
|
||||
}
|
||||
}
|
||||
|
||||
async function testHistoryCardsUseDecryptedPlaybackMetadata() {
|
||||
console.log("Running watch history card playback metadata test...");
|
||||
|
||||
const pointerVideo = {
|
||||
id: "history-card",
|
||||
title: "History Card Video",
|
||||
url: "https://cdn.example/history-card.mp4",
|
||||
magnet:
|
||||
"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567",
|
||||
infoHash: "0123456789abcdef0123456789abcdef01234567",
|
||||
legacyInfoHash: "0123456789abcdef0123456789abcdef01234567",
|
||||
};
|
||||
|
||||
const item = {
|
||||
pointerKey: "e:history-card",
|
||||
pointer: { type: "e", value: "history-card" },
|
||||
watchedAt: 1_700_000_500,
|
||||
};
|
||||
|
||||
const originalDocument = globalThis.document;
|
||||
const originalHTMLElement = globalThis.HTMLElement;
|
||||
|
||||
class FakeClassList {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
_sync() {
|
||||
this.element._className = Array.from(this.element._classSet).join(" ");
|
||||
}
|
||||
|
||||
add(...tokens) {
|
||||
tokens.forEach((token) => {
|
||||
if (token) {
|
||||
this.element._classSet.add(token);
|
||||
}
|
||||
});
|
||||
this._sync();
|
||||
}
|
||||
|
||||
remove(...tokens) {
|
||||
tokens.forEach((token) => this.element._classSet.delete(token));
|
||||
this._sync();
|
||||
}
|
||||
|
||||
toggle(token, force) {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
if (force === true) {
|
||||
this.element._classSet.add(token);
|
||||
this._sync();
|
||||
return true;
|
||||
}
|
||||
if (force === false) {
|
||||
this.element._classSet.delete(token);
|
||||
this._sync();
|
||||
return false;
|
||||
}
|
||||
if (this.element._classSet.has(token)) {
|
||||
this.element._classSet.delete(token);
|
||||
this._sync();
|
||||
return false;
|
||||
}
|
||||
this.element._classSet.add(token);
|
||||
this._sync();
|
||||
return true;
|
||||
}
|
||||
|
||||
contains(token) {
|
||||
return this.element._classSet.has(token);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeElement {
|
||||
constructor(tagName) {
|
||||
this.tagName = typeof tagName === "string" ? tagName.toUpperCase() : "";
|
||||
this.children = [];
|
||||
this.parentNode = null;
|
||||
this.dataset = {};
|
||||
this.attributes = {};
|
||||
this.textContent = "";
|
||||
this._classSet = new Set();
|
||||
this._className = "";
|
||||
this.classList = new FakeClassList(this);
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
if (child && typeof child === "object") {
|
||||
child.parentNode = this;
|
||||
}
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
get className() {
|
||||
return this._className;
|
||||
}
|
||||
|
||||
set className(value) {
|
||||
const tokens =
|
||||
typeof value === "string"
|
||||
? value
|
||||
.split(/\s+/)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
this._classSet = new Set(tokens);
|
||||
this._className = tokens.join(" ");
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = String(value);
|
||||
}
|
||||
|
||||
removeAttribute(name) {
|
||||
delete this.attributes[name];
|
||||
}
|
||||
}
|
||||
|
||||
function collectElements(root, predicate, results = []) {
|
||||
if (!(root instanceof FakeElement)) {
|
||||
return results;
|
||||
}
|
||||
if (predicate(root)) {
|
||||
results.push(root);
|
||||
}
|
||||
for (const child of root.children) {
|
||||
collectElements(child, predicate, results);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const fakeDocument = {
|
||||
createElement(tagName) {
|
||||
return new FakeElement(tagName);
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.document = fakeDocument;
|
||||
globalThis.HTMLElement = FakeElement;
|
||||
|
||||
try {
|
||||
const card = buildHistoryCard({
|
||||
item,
|
||||
video: pointerVideo,
|
||||
profile: null,
|
||||
metadataPreference: "encrypted-only",
|
||||
});
|
||||
|
||||
assert(card instanceof FakeElement);
|
||||
assert.equal(card.dataset.pointerKey, item.pointerKey);
|
||||
|
||||
const playLinks = collectElements(
|
||||
card,
|
||||
(element) =>
|
||||
element.tagName === "A" &&
|
||||
element.dataset.historyAction === "play",
|
||||
);
|
||||
|
||||
assert(playLinks.length >= 1, "card should expose play actions");
|
||||
assert.equal(
|
||||
playLinks[0].dataset.playUrl,
|
||||
encodeURIComponent(pointerVideo.url),
|
||||
"play action should encode url from metadata",
|
||||
);
|
||||
assert.equal(
|
||||
playLinks[0].dataset.playMagnet,
|
||||
pointerVideo.magnet,
|
||||
"play action should surface magnet from metadata",
|
||||
);
|
||||
} finally {
|
||||
globalThis.document = originalDocument;
|
||||
globalThis.HTMLElement = originalHTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
async function testWatchHistoryStaleCacheRefresh() {
|
||||
console.log("Running watch history stale cache refresh test...");
|
||||
|
||||
@@ -1696,6 +1977,7 @@ await testPublishSnapshotFailureRetry();
|
||||
await testWatchHistoryPartialRelayRetry();
|
||||
await testResolveWatchHistoryBatchingWindow();
|
||||
await testWatchHistoryServiceIntegration();
|
||||
await testHistoryCardsUseDecryptedPlaybackMetadata();
|
||||
await testWatchHistoryStaleCacheRefresh();
|
||||
await testWatchHistoryLocalFallbackWhenDisabled();
|
||||
await testWatchHistorySyncEnabledForLoggedInUsers();
|
||||
|
||||
Reference in new issue
Block a user