Privacy HLS Stream Downloader

Automatically download HLS streams from Privacy

// ==UserScript==
// @name         Privacy HLS Stream Downloader
// @namespace    http://tampermonkey.net/
// @license      GPL-3.0
// @version      2025.03.24
// @description  Automatically download HLS streams from Privacy
// @author       Rvnsxmwvrx
// @match        https://privacy.com.br/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=privacy.com.br
// @grant        GM_cookie
// @grant        GM_xmlhttpRequest
// @run-at documentEnd
// ==/UserScript==

(function () {
    "use strict";
    async function decryptSegment(encryptedData, key) {
  const iv = new Uint8Array(16); // Use a fixed IV or derive it as needed
  const algorithm = { name: 'AES-CBC', iv };

  const cryptoKey = await window.crypto.subtle.importKey(
    'raw',
    key,
    algorithm,
    false,
    ['decrypt']
  );

  const decryptedData = await window.crypto.subtle.decrypt(
    algorithm,
    cryptoKey,
    encryptedData
  );

  return new Uint8Array(decryptedData); // Return decrypted data
}


    async function filterHLS(urlBegin, text, content) {
        const is_encrypted = text.includes("keyaes")
        let key = undefined
        if(is_encrypted) {
        const key_uri_begin = text.indexOf(`URI="`) + 5
        const key_uri_end = text.substring(key_uri_begin).indexOf('"')
        const key_uri = text.substring(key_uri_begin, key_uri_begin + key_uri_end)
        console.log(key_uri)
        const key_request = await fetch(key_uri, {
        headers: {
        "Host": "keyaes.privacy.com.br",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
    "Accept": "*/*",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Content-Type": "application/json",
            "content": content,
    "x-content-uri": "keyaes.key",
    "Origin": "https://privacy.com.br",
    "Connection": "keep-alive",
    "Referer": "https://privacy.com.br/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-site"
        }
        })
        if(!key_request.ok) {
            console.warn(key_request.status)
            return []
        }
        key = await key_request.arrayBuffer()
        }
        let rtn = [];
        for (let line of text.split("\n")) {
            if (line.startsWith("#")) continue;
            rtn.push({ key: key, url: urlBegin + line } );
        }
        let fhd = rtn.filter((e) => e.url.includes("1080p"));
        if (fhd.length > 0) return fhd;
        let hd = rtn.filter((e) => e.url.includes("720p"));
        if (hd.length > 0) return hd;
        return rtn[0];
    }

    async function downloadFiles(div, button, urls) {
        let count = 1;
        for (let url of urls) {
            let start = url.lastIndexOf("/");
            let filename = url.substring(start);
            let split = url.indexOf("hls/") + 4;
            let urlBegin = url.substring(0, split);
            const content = button.getAttribute("content")
            await fetch(url, {
                headers: {
                    "Host": "video.privacy.com.br",
                    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
                    "Accept": "*/*",
                    "Accept-Language": "en-US,en;q=0.5",
                    "Accept-Encoding": "gzip, deflate, br, zstd",
                    "Content-Type": "application/json",
                    "content": content,
                    "x-content-uri": filename.substring(1),
                    "Origin": "https://privacy.com.br",
                    "Connection": "keep-alive",
                    "Referer": "https://privacy.com.br/",
                    "Sec-Fetch-Dest": "empty",
                    "Sec-Fetch-Mode": "cors",
                    "Sec-Fetch-Site": "same-site"
                },
            })
                .then((response) => response.text())
                .then(async (text) => {
                    const fileContent = text;
                    let video = await filterHLS(urlBegin, text, button.getAttribute(content));
                    await helper(div, button, urlBegin, video[0], url);
                })
                .catch((err) => console.error("Error downloading file:", err));
        }
    }

    const maxRetries = 10;
    async function helper(div, button, beginUrl, url, eUrl) {
        const x_content_uri_begin = url.url.indexOf("hls/") + 4
            const x_content_uri = url.url.substring(x_content_uri_begin)

        fetch(url.url, {
        headers: {
    "Host": "video.privacy.com.br",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
    "Accept": "*/*",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Content-Type": "application/json",
            "content": button.getAttribute("content"),
    "x-content-uri": x_content_uri,
    "Origin": "https://privacy.com.br",
    "Connection": "keep-alive",
    "Referer": "https://privacy.com.br/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-site"
  }
        })
            .then((response) => response.text())
            .then(async (text) => {
                let element = div.getElementsByClassName(eUrl)[0];
                let tsFiles = await filterHLS(beginUrl, text, button.getAttribute("content"));
                await downloadVideos(element, button, tsFiles);
            })
            .catch((err) => console.error("Error downloading file:", err));
    }

    async function downloadVideos(element, button, tsFiles) {
    const combinedBuffers = [];
    const end = tsFiles[0].url.indexOf("--");
    const name = tsFiles[0].url.substring(0, end);
    const oldName = button.innerText;

    // Fetch all the ts files and push the buffers into the combinedBuffers array
    for (const ts of tsFiles) {
        const tsFile = ts.url;
        let retries = 0;
        const content = button.getAttribute("content");
        const x_content_uri_begin = tsFile.indexOf("hls/") + 4;
        const x_content_uri = tsFile.substring(x_content_uri_begin);
        const headers = {
            "Host": "video.privacy.com.br",
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
            "Accept": "*/*",
            "Accept-Language": "en-US,en;q=0.5",
            "Accept-Encoding": "gzip, deflate, br, zstd",
            "Content-Type": "application/json",
            "content": content,
            "x-content-uri": x_content_uri,
            "Origin": "https://privacy.com.br",
            "Connection": "keep-alive",
            "Referer": "https://privacy.com.br/",
            "Sec-Fetch-Dest": "empty",
            "Sec-Fetch-Mode": "cors",
            "Sec-Fetch-Site": "same-site"
        };

        let response = await fetch(tsFile, { headers });
        if (!response.ok) return;

        while (!response.ok && retries < maxRetries) {
            setTimeout(() => { }, 500);
            response = await fetch(tsFile, { headers });
            retries += 1;
        }

        let arrayBuffer = await response.arrayBuffer();
        if(ts.key) {
          arrayBuffer = await decryptSegment(arrayBuffer, ts.key)
        }
        combinedBuffers.push(arrayBuffer);

        let percent = (combinedBuffers.length / tsFiles.length) * 100;
        element.innerText = `(${percent.toPrecision(2)}%) `;
        button.innerText = "Downloading...";
    }

    // Combine all ArrayBuffers into one single ArrayBuffer
    let totalLength = combinedBuffers.reduce((sum, buffer) => sum + buffer.byteLength, 0);
    let combinedArrayBuffer = new ArrayBuffer(totalLength);
    let view = new Uint8Array(combinedArrayBuffer);
    let offset = 0;

    for (let buffer of combinedBuffers) {
        view.set(new Uint8Array(buffer), offset);
        offset += buffer.byteLength;
    }

    // Update UI and prepare the video file for download
    element.innerText = "(0%)";
    button.innerText = oldName;

    const videoBlob = new Blob([combinedArrayBuffer], { type: "video/mp2t" });
    const url = URL.createObjectURL(videoBlob);
    const downloadLink = document.createElement("a");
    downloadLink.href = url;
    downloadLink.download = name + ".ts";
    downloadLink.textContent = "Download Combined Video";
    document.body.appendChild(downloadLink);
    downloadLink.click();
    URL.revokeObjectURL(url);
}


    async function waitForShadowRoot(element) {
        while (!element.shadowRoot) {
            console.log("waiting for shadow root");
            console.log(element.shadowRoot);
            await new Promise((resolve) => setTimeout(resolve, 500)); // Wait 50ms before checking again
        }
        console.log("out of loop");
    }

    let allVideos = new Set();
    let elementIds = new Set();

    async function find_docs() {
        let elements = document.querySelectorAll("privacy-web-mediahub-carousel");
        for (let i = 0; i < elements.length; i++) {
            let element = elements[i];
            if (elementIds.has(element.getAttribute("id"))) continue;
            elementIds.add(element.getAttribute("id"));
            let mediasStr = element.getAttribute("medias");
            let medias = collectObjects(mediasStr);
            let videos = medias
                .filter(
                    (e) => e.url && e.url.endsWith(".m3u8") && !allVideos.has(e.url)
                )
                .map((e) => e.url);
            videos.forEach((e) => allVideos.add(e));
            if (videos.length < 1) continue;
            let start = videos[0].indexOf("hls/") + 4;
            const fstart = videos[0].indexOf(".br/") + 4
            const file_id = videos[0].substring(fstart, start - 5)
            const token = element.getAttribute("token")
            const content_request = await fetch("https://service.privacy.com.br/media/video/token", {
              method: "POST",
                headers: {
                    "Host": "service.privacy.com.br",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Content-Type": "application/json",
    "Authorization": `Bearer ${token}`,
    "Content-Length": token.length,
    "Origin": "https://privacy.com.br",
    "Connection": "keep-alive",
    "Referer": "https://privacy.com.br/",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-site",
    "TE": "trailers"
                },
                body: JSON.stringify({
                    exp: 3600,
                    file_id: file_id
                })
            })
            if(!content_request.ok) {
                console.warn(content_request.status)
                continue
            }
            const content = (await content_request.json()).content
            console.log("FILE_ID: " + file_id)
            let end = videos[0].indexOf("--");
            let buttonId = videos[0].substring(start, end);
            await waitForShadowRoot(element);
            console.log("Got shadow root for " + element.getAttribute("id"));
            let shadow = element.shadowRoot;
            let div = document.createElement("div");
            div.setAttribute("style", "display:flex;");
            console.log(videos[0]);
            let button = document.createElement("button");
            if (videos.length == 1) {
                button.innerText = "Download Video";
            } else {
                button.innerText = "Download " + videos.length + " Videos";
            }
            button.setAttribute("content", content)
            button.setAttribute("id", buttonId);
            button.addEventListener("click", function () {
                downloadFiles(div, button, videos);
            });
            div.appendChild(button);
            for (let video of videos) {
                let element = document.createElement("p");
                element.setAttribute("class", video);
                element.innerText = "(0%)";
                div.appendChild(element);
            }
            shadow.appendChild(div);
            elementIds.add(element.getAttribute("id"));
        }
    }
    let count = 0;
    let cookie = {};
    GM_cookie.list(
        { name: "__cf_bm", httpOnly: true },
        function (cookies, error) {
            if (!error) {
                cookie = cookies[0].value;
            } else {
                console.error(error);
            }
        }
    );
    async function find_mp4() {
        let elements = document.querySelectorAll("privacy-web-mediahub-carousel");
        for (let i = 0; i < elements.length; i++) {
            let element = elements[i];
            elementIds.add(element.getAttribute("id"));
            let mediasStr = element.getAttribute("medias");
            let medias = collectObjects(mediasStr);
            let videos = medias
                .filter((e) => e.url && e.url.endsWith(".mp4") && !allVideos.has(e.url))
                .map((e) => e.url);
            videos.forEach((e) => allVideos.add(e));
            if (videos.length < 1) continue;
            let start = videos[0].indexOf("mp4/") + 4;
            let end = videos[0].indexOf("--");
            let buttonId = videos[0].substring(start, end);
            await waitForShadowRoot(element);
            console.log("Got shadow root for " + element.getAttribute("id"));
            let shadow = element.shadowRoot;
            let div = document.createElement("div");
            div.setAttribute("style", "display:flex;");
            console.log(videos[0]);
            let button = document.createElement("button");
            if (videos.length == 1) {
                button.innerText = "Download Video";
            } else {
                button.innerText = "Download " + videos.length + " Videos";
            }
            button.setAttribute("id", buttonId);
            button.addEventListener("click", async function () {
                await downloadMp4(videos);
            });
            div.appendChild(button);
            for (let video of videos) {
                let element = document.createElement("p");
                element.setAttribute("class", video);
                element.innerText = "(0%)";
                div.appendChild(element);
            }
            shadow.appendChild(div);
            elementIds.add(element.getAttribute("id"));
        }
    }
    let allPhotos = new Set()

    async function find_images() {
        let elements = document.querySelectorAll("privacy-web-mediahub-carousel");
        for (let i = 0; i < elements.length; i++) {
            let element = elements[i];
            elementIds.add(element.getAttribute("id"));
            let mediasStr = element.getAttribute("medias");
            let medias = collectObjects(mediasStr);
            let photos = medias
                .filter((e) => e.type && e.type == "image" && !allPhotos.has(e.url))
                .map((e) => e.url);
            photos.forEach((e) => allPhotos.add(e));
            if (photos.length < 1) continue;
            let start = photos[0].indexOf(".br/") + 4;
            let end = photos[0].indexOf("--");
            let buttonId = photos[0].substring(start, end);
            await waitForShadowRoot(element);
            console.log("Got shadow root for " + element.getAttribute("id"));
            let shadow = element.shadowRoot;
            let div = document.createElement("div");
            div.setAttribute("style", "display:flex;");
            console.log("PHOTO " + photos[0]);
            let button = document.createElement("button");
            if (photos.length == 1) {
                button.innerText = "Download Video";
            } else {
                button.innerText = "Download " + photos.length + " Images";
            }
            button.setAttribute("id", buttonId);
            button.addEventListener("click", async function () {
                await downloadImages(photos);
            });
            div.appendChild(button);
            shadow.appendChild(div);
            elementIds.add(element.getAttribute("id"));
        }
    }

    function getCookie(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) return parts.pop().split(";").shift();
    }

    async function downloadMp4(videos) {
        for (let url of videos) {
            try {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    headers: {
                        Host: "video.privacy.com.br",
                        Accept: "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5",
                        "Accept-Language": "en-US,en;q=0.5",
                        Range: "bytes=0-",
                        Connection: "keep-alive",
                        Referer: "https://privacy.com.br/",
                        Cookie: cookie,
                        "Sec-Fetch-Dest": "video",
                        "Sec-Fetch-Mode": "no-cors",
                        "Sec-Fetch-Site": "same-site",
                        "Accept-Encoding": "identity",
                        Priority: "u=4",
                        TE: "trailers",
                    },
                    responseType: "arraybuffer", // Set responseType to arraybuffer
                    onload: function (response) {
                        // Convert the ArrayBuffer to a Blob
                        const blob = new Blob([response.response], { type: "video/mp4" }); // adjust MIME type as needed

                        // Create a downloadable link
                        const link = document.createElement("a");
                        link.href = URL.createObjectURL(blob);
                        link.download = url; // Set a default filename, or use `url` if needed

                        // Append, click, and remove the link to start the download
                        document.body.appendChild(link);
                        link.click();
                        document.body.removeChild(link);

                        // Clean up the object URL
                        URL.revokeObjectURL(link.href);
                        console.log(`Download started: ${url}`);
                    },
                    onerror: function (error) {
                        console.error("Download failed:", error);
                    },
                });

            } catch (e) {
                console.error(e)
            }
        }
    }


    async function downloadImages(images) {
        for (let url of images) {
            try {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    headers: {
                        Host: "image.privacy.com.br",
                        Accept: "image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5",
                        "Accept-Language": "en-US,en;q=0.5",
                        Connection: "keep-alive",
                        Referer: "https://privacy.com.br/",
                        Cookie: cookie,
                        "Sec-Fetch-Dest": "image",
                        "Sec-Fetch-Mode": "no-cors",
                        "Sec-Fetch-Site": "same-site",
                        "Accept-Encoding": "gzip, deflate, br, zstd",
                        Priority: "u=5",
                        TE: "trailers",
                    },
                    responseType: "arraybuffer", // Set responseType to arraybuffer
                    onload: function (response) {
                        // Convert the ArrayBuffer to a Blob
                        const blob = new Blob([response.response], { type: "image/jpeg" }); // adjust MIME type as needed

                        // Create a downloadable link
                        const link = document.createElement("a");
                        link.href = URL.createObjectURL(blob);
                        console.log(url)
                        link.download = url.split("/").pop();

                        // Append, click, and remove the link to start the download
                        document.body.appendChild(link);
                        link.click();
                        document.body.removeChild(link);

                        // Clean up the object URL
                        URL.revokeObjectURL(link.href);
                    },
                    onerror: function (error) {
                        console.error("Download failed:", error);
                    },
                });

            } catch (e) {
                console.error(e)
            }
        }
    }

    function collectObjects(mediasStr) {
        let start = 0;
        let offset = 0;
        let objects = [];
        for (let i = 0; i < mediasStr.length; i++) {
            let char = mediasStr[i];
            if (char == "{") {
                start = i;
            } else if (char == "}") {
                let objStr = mediasStr.substring(start, start + offset + 1);
                objects.push(JSON.parse(objStr));
                offset = 0;
            } else {
                offset += 1;
            }
        }
        return objects;
    }

    setInterval(async () => {
        await find_docs()
        await find_mp4()
        await find_images()
    }, 1000);
})();