// ==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);
})();