Privacy HLS Stream Downloader

Automatically download HLS streams from Privacy

נכון ליום 01-11-2024. ראה הגרסה האחרונה.

// ==UserScript==
// @name         Privacy HLS Stream Downloader
// @namespace    http://tampermonkey.net/
// @license      GPL-3.0
// @version      2024.11.1
// @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";

  function filterHLS(urlBegin, text) {
    console.log(text);
    let rtn = [];
    for (let line of text.split("\n")) {
      if (line.startsWith("#")) continue;
      rtn.push(urlBegin + line);
    }
    let fhd = rtn.filter((e) => e.includes("1080p"));
    if (fhd.length > 0) return fhd;
    let hd = rtn.filter((e) => e.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);
      await fetch(url)
        .then((response) => response.text())
        .then(async (text) => {
          const fileContent = text;
          let video = filterHLS(urlBegin, text);
          await helper(div, button, urlBegin, video, url);
        })
        .catch((err) => console.error("Error downloading file:", err));
    }
  }
  const maxRetries = 10;
  async function helper(div, button, beginUrl, url, eUrl) {
    fetch(url)
      .then((response) => response.text())
      .then(async (text) => {
        console.log(text);
        console.log(url);
        let element = div.getElementsByClassName(eUrl)[0];
        let tsFiles = filterHLS(beginUrl, text);
        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].indexOf("--");
    const name = tsFiles[0].substring(0, end);
    const oldName = button.innerText;
    for (const tsFile of tsFiles) {
      let retries = 0;
      let response = await fetch(tsFile);
      while (!response.ok && retries < maxRetries) {
        setTimeout(() => {}, 500);
        response = await fetch(tsFile);
        retries += 1;
      }
      const arrayBuffer = await response.arrayBuffer();
      combinedBuffers.push(arrayBuffer);
      let percent = (combinedBuffers.length / tsFiles.length) * 100;
      element.innerText = "(" + percent.toPrecision(2) + ") ";
      button.innerText = "Downloading...";
    }
    element.innerText = "(0%)";
    button.innerText = oldName;
    console.log(combinedBuffers.length);
    const videoBlob = new Blob(combinedBuffers, { 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;
      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", 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) {
        console.log("OBJ: " + cookies[0].value);
        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"));
    }
  }
  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)
      }
    }
  }

  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(), 1000);
  setInterval(async () => await find_mp4(), 1000);
})();