Free javascript Hosting


leakemup.js

Uploaded on Nov 25 2021 14:08 by bthxyz

// ==UserScript==
// @name XenForo Message Media Downloader
// @version 1.5
// @author js2k
// @description Download all (1st party) media found within a message on XenForo forums in a zip file.
// @match https://forums.leakemup.com/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @noframes
// @connect self
// @run-at document-end
// ==/UserScript==

/* globals JSZip saveAs */

'use strict';

/**
* Determines if emoji should be allowed in the zip name.
* @type {boolean} If set to true, emoji in thread titles will be allowed in the zip name.
*/
const ALLOW_THREAD_TITLE_EMOJI = false;

/**
* Edit this to change the replacement for illegal characters.
* Bad things may happen if you set this to an empty string, and the
* resulting title after replacement contains illegal characters or phrases.
* @type {string} Illegal characters in the thread title will be replaced with this string.
*/
const ILLEGAL_CHAR_REPLACEMENT = '_';

/**
* Determines if a string is null or empty.
* @param {string} str The string to be tested.
* @returns {boolean} True if the string is null or empty, false if the string is not nul or empty.
*/
const isNullOrEmpty = (str) => {
return !str;
};

/**
* Gets the thread title, removes illegal characters.
* @returns {string} String containing the thread title with illegal characters replaced.
*/
const getThreadTitle = () => {
// Define file name regexps
const REGEX_EMOJI = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/gu;
const REGEX_WINDOWS = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$|([<>:"\/\\|?*])|(\.|\s)$/gi;

// Strip label buttons
let threadTitle = [...document.querySelector('.p-title').childNodes].reduce((title, child) => {
return child.nodeType === 3 && !isNullOrEmpty(child.textContent) ? (title += child.textContent) : '';
});

// Remove emoji from title
if (!ALLOW_THREAD_TITLE_EMOJI) {
threadTitle = threadTitle.replaceAll(REGEX_EMOJI, ILLEGAL_CHAR_REPLACEMENT);
}

// Remove illegal chars and names (Windows)
threadTitle = threadTitle.replaceAll(REGEX_WINDOWS, ILLEGAL_CHAR_REPLACEMENT);

threadTitle = threadTitle.trim();

return threadTitle;
};

/**
* Gets the post number of a message.
* @param {HTMLElement} messageReference Reference to the message containing media.
* @returns {number} The post number of the message.
*/
const getPostNumber = (messageRef) => {
// Define excess whitespace regexp
const REGEX_EXCESS = /[\n\r]+|[\s]{2,}/g;

// Get post number from message reference, normalize and trim whitespace
const postNumber = messageRef.querySelector('.message-attribution-opposite > li:last-child > a').textContent.replaceAll(REGEX_EXCESS, ' ').trim();

return postNumber;
};

/**
* Gets URLs of media found within a message.
* @param {HTMLElement} messageRef Reference to the message containing media.
* @returns {Array<string>} Array of URL strings.
*/
const getMessageMediaUrls = (messageRef) => {
// Get all media elements found within the message
const messageMedia = messageRef.querySelectorAll('.js-lbImage, .attachment-icon a, .lbContainer-zoomer, video');

// Get urls from message media elements
const urls = [...messageMedia].map((curr) => {
// Videos can have a 'source' element
if (curr.querySelector('source')) {
return curr.querySelector('source').src;
} else {
// Return href, or src if attachment
return curr.getAttribute('href') ? curr.href : curr.dataset.src;
}
});

return urls;
};

/**
* Gets the name of a file from the URL string.
* @param {string} url
* @returns {string} The file name.
*/
const getFileNameFromUrl = (url) => {
let fileName = url.replace(/\/$/, '').split('/').pop();

// If fileName equals e.g."3840x2160_image-jpg.124135", convert to 3840x2160_image.jpg
if (fileName.split('.').pop().match(/^[\d]+$/)) {
// Remove incorrect extension
fileName = fileName.split('.').slice(0, -1).join('.');

// Get proper extension
let ext = fileName.split('-').pop();

fileName = fileName.split('-').slice(0, -1).join('-');

fileName = `leakemup.com - ${fileName}.${ext}`;
}

return fileName;
};

/**
* Downloads all media found in a message.
* @param {HTMLElement} messageRef Reference to the message containing media elements.
*/
const downloadMessageMedia = async (messageRef) => {
const downloadLink = messageRef.querySelector('.message-attribution-opposite > li:first-child > a');

const zip = new JSZip();

const urls = getMessageMediaUrls(messageRef);
const total = urls.length;
let current = 0;

for await (const url of urls) {
current++;
const percent = 0;
downloadLink.textContent = `Downloading... ${current}/${total} (${percent}%)`;

let data = [];
let blob;

const response = await fetch(url, { mode: 'cors' });

if (response.ok) {
const contentLength = +response.headers.get('Content-Length');

let fileName = '';

// Get filename from response, else get filename from url string
try {
fileName = response.headers.get('Content-Disposition').split('filename=')[1].replace(/\"/g, '');
} catch (e) {
fileName = getFileNameFromUrl(url);
}

// If content length is known, update download percentage on progress
if (contentLength) {
const reader = response.body.getReader();

let receivedLength = 0;
while (true) {
const { done, value } = await reader.read();

if (done) {
break;
}

data.push(value);
receivedLength += value.length;
const newPercent = ((receivedLength / contentLength) * 100).toFixed(0);

downloadLink.textContent = downloadLink.textContent.replace(/\([\d]{1,3}%\)/g, `(${newPercent}%)`);
}
} else {
data = await response.arrayBuffer();
}

// Add file to zip
blob = new Blob(data);
zip.file(fileName, blob);
} else {
downloadLink.textContent = `Downloading... ${current}/${total} failed, skipping`;
continue;
}
}

// Generate and save zip
downloadLink.textContent = 'Generating zip...';

const threadTitle = getThreadTitle();
const postNum = getPostNumber(messageRef);
const zipName = `leakemup.com - ${postNum}.zip`;

await zip.generateAsync({ type: 'blob' }).then((blob) => {
downloadLink.textContent = 'Saving zip...';
saveAs(blob, zipName);
downloadLink.textContent = 'Download complete!';
});

// Reset download link text after 5 seconds
await new Promise((resolve) => setTimeout(resolve, 5000));
downloadLink.textContent = '⭳ Download';
};

/**
* Adds a download link to the attribution of a message.
* @param {HTMLElement} messageRef Reference to the message containing media.
*/
const addDownloadLink = (messageRef) => {
// Get message attribution opposite
const messageAttrOpp = messageRef.querySelector('.message-attribution-opposite');

// Get first list child
const messageAttrOppFirstChild = messageAttrOpp.querySelector('li:first-child');

// Create download link
const downloadLinkList = document.createElement('LI');
const downloadLinkAnchor = document.createElement('A');
const downloadLinkText = '⭳ Download';

downloadLinkAnchor.setAttribute('href', '#');
downloadLinkAnchor.textContent = downloadLinkText;

downloadLinkList.appendChild(downloadLinkAnchor);

const downloadLink = downloadLinkList;

// Add download link to message attribution opposite
messageAttrOpp.insertBefore(downloadLink, messageAttrOppFirstChild);

// Add click event listener to download link
downloadLink.addEventListener('click', (e) => {
e.preventDefault();

// Download media found in message
downloadMessageMedia(messageRef);
});
};

// Get all messages with media elements
const messages = [...document.querySelectorAll('.message, .message--post')].filter((message) => {
return message.querySelector('.js-lbImage, .attachment-icon a, .lbContainer-zoomer, video');
});

// Add download link to messages
messages.forEach((msg) => {
addDownloadLink(msg);
});

Back to list