Privacy HLS Stream Downloader

Automatically download HLS streams from Privacy

Per 23-10-2024. Zie de nieuwste versie.

// ==UserScript==
// @name         Privacy HLS Stream Downloader
// @namespace    http://tampermonkey.net/
// @license      GPL-3.0
// @version      2024-10-23
// @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        none
// ==/UserScript==

(function () {
  "use strict";

  function filterHLS(urlBegin, 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) return fhd;
    let hd = rtn.filter((e) => e.includes("720p"));
    if (hd) return hd;
    return rtn[0];
    return rtn;
  }

  async function downloadFiles(button, urls) {
    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(button, urlBegin, video);
        })
        .catch((err) => console.error("Error downloading file:", err));
    }
  }
  const maxRetries = 10;
  async function helper(button, beginUrl, url) {
    fetch(url)
      .then((response) => response.text())
      .then(async (text) => {
        console.log(text);
        let tsFiles = filterHLS(beginUrl, text);
        await downloadVideos(button, tsFiles);
      })
      .catch((err) => console.error("Error downloading file:", err));
  }
  async function downloadVideos(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
      button.innerText = "Downloading (" + percent.toPrecision(2) + "%)"
    }
    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);
  }

  let allVideos = new Set();

  function find_docs() {
    let elements = document.querySelectorAll("privacy-web-mediahub-carousel");
    for (let element of elements) {
      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)
      console.log(buttonId)
      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(button, videos);
      });
      element.shadowRoot.appendChild(button);
    }
  }

  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(() => find_docs(), 1000);
})();