diff --git a/js/app.js b/js/app.js index 65a4f1ed..cea4d77f 100644 --- a/js/app.js +++ b/js/app.js @@ -720,12 +720,19 @@ class bitvidApp { } }; - const encodedUrl = trigger.getAttribute("data-play-url") || ""; - const encodedMagnet = trigger.getAttribute("data-play-magnet") || ""; + const rawUrlValue = + (trigger.dataset && typeof trigger.dataset.playUrl === "string" + ? trigger.dataset.playUrl + : null) ?? trigger.getAttribute("data-play-url") ?? ""; + const rawMagnetValue = + (trigger.dataset && typeof trigger.dataset.playMagnet === "string" + ? trigger.dataset.playMagnet + : null) ?? trigger.getAttribute("data-play-magnet") ?? ""; - const url = decodeDataValue(encodedUrl); - const rawMagnet = decodeDataValue(encodedMagnet); - const magnet = safeDecodeMagnet(rawMagnet) || rawMagnet; + const url = decodeDataValue(rawUrlValue); + const decodedMagnetValue = decodeDataValue(rawMagnetValue); + const magnet = + safeDecodeMagnet(decodedMagnetValue) || decodedMagnetValue; const eventId = trigger.getAttribute("data-video-id"); if (eventId) { @@ -1365,11 +1372,6 @@ class bitvidApp { const authorSet = new Set(); // 3) Build each card - const encodeDataValue = (value) => - typeof value === "string" && value.length > 0 - ? encodeURIComponent(value) - : ""; - videos.forEach((video, index) => { if (!video.id || !video.title) { console.error("Video missing ID/title:", video); @@ -1452,8 +1454,8 @@ class bitvidApp { const magnetCandidate = trimmedMagnet || legacyInfoHash; const magnetSupported = isValidMagnetUri(magnetCandidate); const magnetProvided = magnetCandidate.length > 0; - const encodedMagnet = encodeDataValue(magnetCandidate); - const encodedUrl = encodeDataValue(trimmedUrl); + const playbackUrl = trimmedUrl; + const playbackMagnet = magnetCandidate; const showUnsupportedTorrentBadge = !trimmedUrl && magnetProvided && !magnetSupported; const torrentBadge = showUnsupportedTorrentBadge @@ -1474,8 +1476,8 @@ class bitvidApp { @@ -1492,8 +1494,8 @@ class bitvidApp {

${this.escapeHTML(video.title)} @@ -1537,6 +1539,15 @@ class bitvidApp { } else if (magnetProvided && magnetSupported) { cardEl.dataset.torrentSupported = "true"; } + const interactiveEls = cardEl.querySelectorAll("[data-video-id]"); + interactiveEls.forEach((el) => { + if (!el.dataset) return; + el.dataset.playUrl = playbackUrl || ""; + el.dataset.playMagnet = playbackMagnet || ""; + if (magnetProvided) { + el.dataset.torrentSupported = magnetSupported ? "true" : "false"; + } + }); } fragment.appendChild(cardEl); }); diff --git a/js/channelProfile.js b/js/channelProfile.js index e8bc1ecd..42909fd5 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -8,6 +8,7 @@ import { isWhitelistEnabled } from "./config.js"; import { deriveTitleFromEvent, parseVideoEventPayload, + findLegacyMagnetInEvent, } from "./videoEventUtils.js"; /** @@ -244,11 +245,6 @@ async function loadUserVideos(pubkey) { const fragment = document.createDocumentFragment(); const allKnownEventsArray = Array.from(nostrClient.allEvents.values()); - const encodeDataValue = (value) => - typeof value === "string" && value.length > 0 - ? encodeURIComponent(value) - : ""; - videos.forEach((video, index) => { // Decrypt if user owns a private video if ( @@ -349,15 +345,16 @@ async function loadUserVideos(pubkey) { const legacyInfoHash = typeof video.infoHash === "string" ? video.infoHash.trim() : ""; const magnetCandidate = trimmedMagnet || legacyInfoHash; - const encodedUrl = encodeDataValue(video.url); - const encodedMagnet = encodeDataValue(magnetCandidate); + const playbackUrl = + typeof video.url === "string" ? video.url : ""; + const playbackMagnet = magnetCandidate || ""; cardEl.innerHTML = `
${safeTitle}

@@ -385,6 +382,13 @@ async function loadUserVideos(pubkey) { `; + const interactiveEls = cardEl.querySelectorAll("[data-video-id]"); + interactiveEls.forEach((el) => { + if (!el.dataset) return; + el.dataset.playUrl = playbackUrl || ""; + el.dataset.playMagnet = playbackMagnet || ""; + }); + fragment.appendChild(cardEl); }); @@ -488,8 +492,12 @@ function localConvertEventToVideo(event) { const trimmedUrl = typeof url === "string" ? url.trim() : ""; const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; + const legacyMagnet = findLegacyMagnetInEvent(event); + const trimmedLegacyMagnet = + typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = trimmedMagnet || trimmedInfoHash; + const playbackMagnet = + trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; const parsedVersion = typeof version === "number" ? version @@ -533,7 +541,7 @@ function localConvertEventToVideo(event) { title: resolvedTitle, url: trimmedUrl, magnet: playbackMagnet, - rawMagnet: trimmedMagnet, + rawMagnet: trimmedMagnet || trimmedLegacyMagnet, infoHash: trimmedInfoHash, thumbnail: parsedContent.thumbnail ?? "", description: parsedContent.description ?? "", diff --git a/js/nostr.js b/js/nostr.js index 06c65763..b13765cd 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -5,6 +5,7 @@ import { accessControl } from "./accessControl.js"; import { deriveTitleFromEvent, parseVideoEventPayload, + findLegacyMagnetInEvent, } from "./videoEventUtils.js"; /** @@ -106,8 +107,12 @@ function convertEventToVideo(event) { const trimmedUrl = typeof url === "string" ? url.trim() : ""; const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; + const legacyMagnet = findLegacyMagnetInEvent(event); + const trimmedLegacyMagnet = + typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = trimmedMagnet || trimmedInfoHash; + const playbackMagnet = + trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; const numericVersion = Number.isFinite(version) ? version : 0; const hasPlayableSource = Boolean(trimmedUrl) || Boolean(playbackMagnet); @@ -148,7 +153,7 @@ function convertEventToVideo(event) { title: resolvedTitle, url: trimmedUrl, magnet: playbackMagnet, - rawMagnet: trimmedMagnet, + rawMagnet: trimmedMagnet || trimmedLegacyMagnet, infoHash: trimmedInfoHash, thumbnail: parsedContent.thumbnail ?? "", description: parsedContent.description ?? "", diff --git a/js/subscriptions.js b/js/subscriptions.js index 20f85344..83e581b1 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -3,6 +3,7 @@ import { nostrClient } from "./nostr.js"; import { deriveTitleFromEvent, parseVideoEventPayload, + findLegacyMagnetInEvent, } from "./videoEventUtils.js"; /** @@ -254,11 +255,6 @@ class SubscriptionsManager { // Only declare localAuthorSet once const localAuthorSet = new Set(); - const encodeDataValue = (value) => - typeof value === "string" && value.length > 0 - ? encodeURIComponent(value) - : ""; - videos.forEach((video, index) => { if (!video.id || !video.title) { console.error("Missing ID or title:", video); @@ -342,15 +338,17 @@ class SubscriptionsManager { const safeTitle = window.app?.escapeHTML(video.title) || "Untitled"; const safeThumb = window.app?.escapeHTML(video.thumbnail) || ""; - const encodedMagnet = encodeDataValue(video.magnet); - const encodedUrl = encodeDataValue(video.url); + const playbackUrl = + typeof video.url === "string" ? video.url : ""; + const playbackMagnet = + typeof video.magnet === "string" ? video.magnet : ""; const cardHtml = `
@@ -365,8 +363,8 @@ class SubscriptionsManager {

${safeTitle}

@@ -401,6 +399,14 @@ class SubscriptionsManager { const t = document.createElement("template"); t.innerHTML = cardHtml.trim(); const cardEl = t.content.firstElementChild; + if (cardEl) { + const interactiveEls = cardEl.querySelectorAll("[data-video-id]"); + interactiveEls.forEach((el) => { + if (!el.dataset) return; + el.dataset.playUrl = playbackUrl || ""; + el.dataset.playMagnet = playbackMagnet || ""; + }); + } fragment.appendChild(cardEl); }); @@ -516,8 +522,13 @@ class SubscriptionsManager { const trimmedUrl = typeof url === "string" ? url.trim() : ""; const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; - const trimmedInfoHash = typeof infoHash === "string" ? infoHash.trim() : ""; - const playbackMagnet = trimmedMagnet || trimmedInfoHash; + const legacyMagnet = findLegacyMagnetInEvent(evt); + const trimmedLegacyMagnet = + typeof legacyMagnet === "string" ? legacyMagnet.trim() : ""; + const trimmedInfoHash = + typeof infoHash === "string" ? infoHash.trim() : ""; + const playbackMagnet = + trimmedMagnet || trimmedLegacyMagnet || trimmedInfoHash; const parsedVersion = typeof version === "number" ? version @@ -571,7 +582,7 @@ class SubscriptionsManager { title: resolvedTitle, url: trimmedUrl, magnet: playbackMagnet, - rawMagnet: trimmedMagnet, + rawMagnet: trimmedMagnet || trimmedLegacyMagnet, infoHash: trimmedInfoHash, thumbnail: parsedContent.thumbnail ?? "", description: parsedContent.description ?? "", diff --git a/js/videoEventUtils.js b/js/videoEventUtils.js index 4f835f7d..a9ac597c 100644 --- a/js/videoEventUtils.js +++ b/js/videoEventUtils.js @@ -6,6 +6,7 @@ */ const MAGNET_URI_PATTERN = /^magnet:\?/i; const HEX_INFO_HASH_PATTERN = /\b[0-9a-f]{40}\b/gi; +const LEGACY_MAGNET_PATTERN = /magnet:\?xt=urn:btih:[0-9a-z]{40,}[^\s"'<>]*/i; function safeTrim(value) { return typeof value === "string" ? value.trim() : ""; @@ -251,3 +252,39 @@ export function parseVideoEventPayload(event = {}) { version, }; } + +function magnetFromText(value) { + if (typeof value !== "string") { + return ""; + } + const match = value.match(LEGACY_MAGNET_PATTERN); + return match ? match[0] : ""; +} + +export function findLegacyMagnetInEvent(event = {}) { + const fromContent = magnetFromText(event?.content); + if (fromContent) { + return fromContent.trim(); + } + + const tags = Array.isArray(event?.tags) ? event.tags : []; + for (const tag of tags) { + if (!Array.isArray(tag) || tag.length < 2) { + continue; + } + const key = typeof tag[0] === "string" ? tag[0].trim().toLowerCase() : ""; + const value = typeof tag[1] === "string" ? tag[1].trim() : ""; + if (!value) { + continue; + } + if (key === "magnet" && value) { + return value; + } + const fromTag = magnetFromText(value); + if (fromTag) { + return fromTag.trim(); + } + } + + return ""; +}