diff --git a/js/app.js b/js/app.js index 79bf45c3..437772d5 100644 --- a/js/app.js +++ b/js/app.js @@ -898,16 +898,6 @@ class bitvidApp { if (event.button === 0 && !event.ctrlKey && !event.metaKey) { event.preventDefault(); - const decodeDataValue = (value) => { - if (!value) return ""; - try { - return decodeURIComponent(value); - } catch (err) { - console.warn("Failed to decode data attribute:", err); - return value; - } - }; - const rawUrlValue = (trigger.dataset && typeof trigger.dataset.playUrl === "string" ? trigger.dataset.playUrl @@ -917,10 +907,17 @@ class bitvidApp { ? trigger.dataset.playMagnet : null) ?? trigger.getAttribute("data-play-magnet") ?? ""; - const url = decodeDataValue(rawUrlValue); - const decodedMagnetValue = decodeDataValue(rawMagnetValue); - const magnet = - safeDecodeMagnet(decodedMagnetValue) || decodedMagnetValue; + let url = ""; + if (rawUrlValue) { + try { + url = decodeURIComponent(rawUrlValue); + } catch (err) { + console.warn("Failed to decode data-play-url attribute:", err); + url = rawUrlValue; + } + } + + const magnet = safeDecodeMagnet(rawMagnetValue) || rawMagnetValue; const eventId = trigger.getAttribute("data-video-id"); if (eventId) { @@ -1739,9 +1736,12 @@ class bitvidApp { // assign them after template parsing so the raw URL/magnet strings avoid // HTML entity escaping in the literal markup and keep any sensitive // magnet payloads out of the static DOM text. + // The play URL is stored URL-encoded to keep spaces and query params + // intact inside data-* attributes; attachVideoListHandler() decodes it + // before playback. interactiveEls.forEach((el) => { if (!el.dataset) return; - el.dataset.playUrl = playbackUrl || ""; + el.dataset.playUrl = encodeURIComponent(playbackUrl || ""); el.dataset.playMagnet = playbackMagnet || ""; if (magnetProvided) { el.dataset.torrentSupported = magnetSupported ? "true" : "false"; diff --git a/js/channelProfile.js b/js/channelProfile.js index 60d5df9b..833c1262 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -382,11 +382,14 @@ async function loadUserVideos(pubkey) { // Leave the data-play-* attributes empty in the template markup so the raw // URL/magnet strings can be assigned after parsing without HTML entity - // escaping, keeping this renderer consistent with app.js. + // escaping, keeping this renderer consistent with app.js. The stored URL is + // encoded so it stays intact within data-* attributes, and the click + // handler decodes it while leaving magnets untouched until + // safeDecodeMagnet() runs. const interactiveEls = cardEl.querySelectorAll("[data-video-id]"); interactiveEls.forEach((el) => { if (!el.dataset) return; - el.dataset.playUrl = playbackUrl || ""; + el.dataset.playUrl = encodeURIComponent(playbackUrl || ""); el.dataset.playMagnet = playbackMagnet || ""; }); diff --git a/js/subscriptions.js b/js/subscriptions.js index fff42045..be192aee 100644 --- a/js/subscriptions.js +++ b/js/subscriptions.js @@ -408,11 +408,13 @@ class SubscriptionsManager { if (cardEl) { // Leave the data-play-* attributes empty in the literal markup so we can // assign the raw URL/magnet strings post-parsing without HTML entity - // escaping, mirroring the approach in app.js. + // escaping, mirroring the approach in app.js. The URL is encoded so that + // special characters survive storage in data-* attributes; the click + // handler decodes it right before playback while keeping the magnet raw. const interactiveEls = cardEl.querySelectorAll("[data-video-id]"); interactiveEls.forEach((el) => { if (!el.dataset) return; - el.dataset.playUrl = playbackUrl || ""; + el.dataset.playUrl = encodeURIComponent(playbackUrl || ""); el.dataset.playMagnet = playbackMagnet || ""; }); }