// ==UserScript==
// @name All in One Injector.V1
// @namespace Mordern
// @version Mordern.5
// @description Youtube Downloader, Discord Delete Message Injector, Discord BackGround Injector, Discord Emotes Injector (Use all emotes), AntiVertify Injector
// @author Mordern
// @license GNU Public License V3
// @match https://discordapp.com/library/*
// @match https://discordapp.com/store/*
// @match https://discordapp.com/channels/*
// @match https://discord.com/library/*
// @match https://discord.com/store/*
// @match https://discord.com/channels/*
// @icon https://cdn.discordapp.com/avatars/834842435204284527/8ab16792caaa54dd6ff2a3e9de7a57b0.png?size=4096
// @match https://discord.com/channels/*
// @match https://discord.com/channels/*/*
// @grant GM.xmlHttpRequest
// @grant GM.registerMenuCommand
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.addElement
// @connect i.imgur.com
// @run-at document-end
// @match https://discord.com/*
// @homepageURL https://github.com/victornpb/deleteDiscordMessages
// @supportURL https://github.com/victornpb/deleteDiscordMessages/issues// @require https://code.jquery.com/jquery-3.6.0.min.js
// @grant GM.registerMenuCommand
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM.setValue
// @grant GM.getValue
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @grant GM.registerMenuCommand
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM.setValue
// @grant GM.getValue
// @match https://*.youtube.com/*
// @require https://unpkg.com/[email protected]/dist/vue.js
// @require https://unpkg.com/[email protected]/xfetch.min.js
// @require https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js
// @require https://bundle.run/[email protected]
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-end
// @connect googlevideo.com
// @compatible firefox >=52
// @compatible chrome >=55
// ==/UserScript==
(function() {
'use strict';
setTimeout(function(){
document.querySelector('[aria-label="VERIFICATION"]').remove();
document.querySelector("#app-mount > div:nth-of-type(2)").children[0].children[1].children[0].style = ""
}, 15000);
})();
(async window => {
'use strict';
let intervalTime = 30 * 1000;
let urls = [
'https://i.imgur.com/MMJhM6R.jpeg',
'https://i.imgur.com/6Y0waqQ.png',
'https://i.imgur.com/SGbjs36.png',
'https://i.imgur.com/87njjbP.jpeg',
'https://i.imgur.com/y5tyz7v.jpeg',
'https://i.imgur.com/7vEByIE.png',
'https://i.imgur.com/N8FAHgN.jpeg',
'https://i.imgur.com/CTAgXGz.jpeg',
'https://i.imgur.com/ax6pZjY.jpeg',
'https://i.imgur.com/GQWtuEg.jpeg',
'https://i.imgur.com/wTYqtSV.jpeg',
'https://i.imgur.com/tWx7OqJ.jpeg',
'https://i.imgur.com/EgkJFvo.jpeg',
'https://i.imgur.com/dHqTbO4.jpeg',
];
let g_elm = null;
const del = () => {
if(g_elm) {
g_elm.remove();
g_elm = null;
return true;
}
else return false;
};
const addInput = async value => {
if(del()) return;
g_elm = await GM.addElement(document.body, 'div', {});
Object.assign(g_elm.style, {
'position': 'fixed',
'width': '50vw',
'height': '30vh',
'display': 'flex',
'flex-wrap': 'wrap',
'justify-content': 'center',
'align-items': 'center',
'z-index': Infinity
});
const input = await GM.addElement(g_elm, 'textarea', {
textContent: value
});
Object.assign(input.style, {
'width': '10vw',
'height': '30vh',
'font-size': '30px',
'font-weight': 'bold'
});
const btnSave = await GM.addElement(g_elm, 'button', {
textContent: 'save'
});
await GM.addElement(g_elm, 'div', {
textContent: ' '
});
const btnCancel = await GM.addElement(g_elm, 'button', {
textContent: 'cancel'
});
for(const v of [btnSave, btnCancel]) Object.assign(v.style, {
'color': 'white',
'backgroundColor': 'red'
});
return new Promise((resolve, reject) => {
btnSave.addEventListener('click', () => del() && resolve(input.value));
btnCancel.addEventListener('click', () => del() && reject());
});
};
const key1 = 'intervalTime';
GM.registerMenuCommand('config interval time', async () => {
const res = await addInput(await GM.getValue(key1, intervalTime));
if(!res) return;
const m = res.match(/[0-9]+/);
if(!m) return;
const n = Number(m[0]);
intervalTime = n;
GM.setValue(key1, n);
});
intervalTime = await GM.getValue(key1, intervalTime);
const key2 = 'URL';
GM.registerMenuCommand('config URL', async () => {
const res = await addInput(await GM.getValue(key2, urls.join('\n')));
if(!res) return;
const a = findURL(res);
if(!a.length) return;
urls = a;
GM.setValue(key2, a.join('\n'));
});
urls = (await GM.getValue(key2, urls.join('\n'))).split('\n');
const findURL = str => {
};
const memo = new Map;
const get = async url => {
if(memo.has(url)) return memo.get(url);
const res = await GM.xmlHttpRequest({
method: 'GET',
url: url,
withCredentials: true,
responseType: 'arraybuffer',
});
const _url = URL.createObjectURL(new Blob([res.response], {type: 'application/octet-binary'}));
memo.set(url, _url);
return _url;
};
let g_url = await get(urls[0]);
const wait = resolve => {
if(document.querySelector('[class^="chatContent"]')) return resolve();
setTimeout(() => wait(resolve), 500);
};
await new Promise(resolve => wait(resolve));
const setURL = () => {
Object.assign(document.body.children[0].style, {
'background-image': 'url("' + g_url + '")',
'background-attachment': 'fixed',
'background-position': 'center center',
'background-size': 'cover',
'background-repeat': 'no-repeat',
'transition-duration': '1.5s'
});
};
setURL();
const setOther = () => {
for(const v of document.querySelectorAll('*')) v.style.backgroundColor = 'rgba(0, 0, 0, 0)';
document.body.children[0].children[3].style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
};
setOther();
let _url = location.href,
_time = 0,
index = 0;
const update = async () => {
const time = performance.now();
if(time - _time > intervalTime) {
g_url = await get(urls[(++index) % urls.length]);
_time = performance.now();
setURL();
}
else {
const url = location.href;
if(url !== _url) {
_url = url;
setOther();
}
}
requestAnimationFrame(update);
};
update();
})(window.unsafeWindow || window);
/**
* Delete all messages in a Discord channel or DM
* @param {string} authToken Your authorization token
* @param {string} authorId Author of the messages you want to delete
* @param {string} guildId Server were the messages are located
* @param {string} channelId Channel were the messages are located
* @param {string} minId Only delete messages after this, leave blank do delete all
* @param {string} maxId Only delete messages before this, leave blank do delete all
* @param {string} content Filter messages that contains this text content
* @param {boolean} hasLink Filter messages that contains link
* @param {boolean} hasFile Filter messages that contains file
* @param {boolean} includeNsfw Search in NSFW channels
* @param {function(string, Array)} extLogger Function for logging
* @param {function} stopHndl stopHndl used for stopping
* @author Victornpb <https://www.github.com/victornpb>
* @see https://github.com/victornpb/deleteDiscordMessages
*/
async function deleteMessages(authToken, authorId, guildId, channelId, minId, maxId, content, hasLink, hasFile, includeNsfw, includePinned, searchDelay, deleteDelay, extLogger, stopHndl, onProgress) {
const start = new Date();
let delCount = 0;
let failCount = 0;
let avgPing;
let lastPing;
let grandTotal;
let throttledCount = 0;
let throttledTotalTime = 0;
let offset = 0;
let iterations = -1;
const wait = async ms => new Promise(done => setTimeout(done, ms));
const msToHMS = s => `${s / 3.6e6 | 0}h ${(s % 3.6e6) / 6e4 | 0}m ${(s % 6e4) / 1000 | 0}s`;
const escapeHTML = html => html.replace(/[&<"']/g, m => ({ '&': '&', '<': '<', '"': '"', '\'': ''' })[m]);
const redact = str => `<span class="priv">${escapeHTML(str)}</span><span class="mask">REDACTED</span>`;
const queryString = params => params.filter(p => p[1] !== undefined).map(p => p[0] + '=' + encodeURIComponent(p[1])).join('&');
const ask = async msg => new Promise(resolve => setTimeout(() => resolve(window.confirm(msg)), 10));
const printDelayStats = () => log.verb(`Delete delay: ${deleteDelay}ms, Search delay: ${searchDelay}ms`, `Last Ping: ${lastPing}ms, Average Ping: ${avgPing | 0}ms`);
const toSnowflake = (date) => /:/.test(date) ? ((new Date(date).getTime() - 1420070400000) * Math.pow(2, 22)) : date;
const log = {
debug() { extLogger ? extLogger('debug', arguments) : console.debug.apply(console, arguments); },
info() { extLogger ? extLogger('info', arguments) : console.info.apply(console, arguments); },
verb() { extLogger ? extLogger('verb', arguments) : console.log.apply(console, arguments); },
warn() { extLogger ? extLogger('warn', arguments) : console.warn.apply(console, arguments); },
error() { extLogger ? extLogger('error', arguments) : console.error.apply(console, arguments); },
success() { extLogger ? extLogger('success', arguments) : console.info.apply(console, arguments); },
};
async function recurse() {
let API_SEARCH_URL;
if (guildId === '@me') {
API_SEARCH_URL = `https://discord.com/api/v6/channels/${channelId}/messages/`; // DMs
}
else {
API_SEARCH_URL = `https://discord.com/api/v6/guilds/${guildId}/messages/`; // Server
}
const headers = {
'Authorization': authToken
};
let resp;
try {
const s = Date.now();
resp = await fetch(API_SEARCH_URL + 'search?' + queryString([
['author_id', authorId || undefined],
['channel_id', (guildId !== '@me' ? channelId : undefined) || undefined],
['min_id', minId ? toSnowflake(minId) : undefined],
['max_id', maxId ? toSnowflake(maxId) : undefined],
['sort_by', 'timestamp'],
['sort_order', 'desc'],
['offset', offset],
['has', hasLink ? 'link' : undefined],
['has', hasFile ? 'file' : undefined],
['content', content || undefined],
['include_nsfw', includeNsfw ? true : undefined],
]), { headers });
lastPing = (Date.now() - s);
avgPing = avgPing > 0 ? (avgPing * 0.9) + (lastPing * 0.1) : lastPing;
} catch (err) {
return log.error('Search request threw an error:', err);
}
// not indexed yet
if (resp.status === 202) {
const w = (await resp.json()).retry_after;
throttledCount++;
throttledTotalTime += w;
log.warn(`This channel wasn't indexed, waiting ${w}ms for discord to index it...`);
await wait(w);
return await recurse();
}
if (!resp.ok) {
// searching messages too fast
if (resp.status === 429) {
const w = (await resp.json()).retry_after;
throttledCount++;
throttledTotalTime += w;
searchDelay += w; // increase delay
log.warn(`Being rate limited by the API for ${w}ms! Increasing search delay...`);
printDelayStats();
log.verb(`Cooling down for ${w * 2}ms before retrying...`);
await wait(w * 2);
return await recurse();
} else {
return log.error(`Error searching messages, API responded with status ${resp.status}!\n`, await resp.json());
}
}
const data = await resp.json();
const total = data.total_results;
if (!grandTotal) grandTotal = total;
const discoveredMessages = data.messages.map(convo => convo.find(message => message.hit === true));
const messagesToDelete = discoveredMessages.filter(msg => {
return msg.type === 0 || msg.type === 6 || (msg.pinned && includePinned);
});
const skippedMessages = discoveredMessages.filter(msg => !messagesToDelete.find(m => m.id === msg.id));
const end = () => {
log.success(`Ended at ${new Date().toLocaleString()}! Total time: ${msToHMS(Date.now() - start.getTime())}`);
printDelayStats();
log.verb(`Rate Limited: ${throttledCount} times. Total time throttled: ${msToHMS(throttledTotalTime)}.`);
log.debug(`Deleted ${delCount} messages, ${failCount} failed.\n`);
}
const etr = msToHMS((searchDelay * Math.round(total / 25)) + ((deleteDelay + avgPing) * total));
log.info(`Total messages found: ${data.total_results}`, `(Messages in current page: ${data.messages.length}, To be deleted: ${messagesToDelete.length}, System: ${skippedMessages.length})`, `offset: ${offset}`);
printDelayStats();
log.verb(`Estimated time remaining: ${etr}`)
if (messagesToDelete.length > 0) {
if (++iterations < 1) {
log.verb(`Waiting for your confirmation...`);
if (!await ask(`Do you want to delete ~${total} messages?\nEstimated time: ${etr}\n\n---- Preview ----\n` +
messagesToDelete.map(m => `${m.author.username}#${m.author.discriminator}: ${m.attachments.length ? '[ATTACHMENTS]' : m.content}`).join('\n')))
return end(log.error('Aborted by you!'));
log.verb(`OK`);
}
for (let i = 0; i < messagesToDelete.length; i++) {
const message = messagesToDelete[i];
if (stopHndl && stopHndl() === false) return end(log.error('Stopped by you!'));
log.debug(`${((delCount + 1) / grandTotal * 100).toFixed(2)}% (${delCount + 1}/${grandTotal})`,
`Deleting ID:${redact(message.id)} <b>${redact(message.author.username + '#' + message.author.discriminator)} <small>(${redact(new Date(message.timestamp).toLocaleString())})</small>:</b> <i>${redact(message.content).replace(/\n/g, '↵')}</i>`,
message.attachments.length ? redact(JSON.stringify(message.attachments)) : '');
if (onProgress) onProgress(delCount + 1, grandTotal);
let resp;
try {
const s = Date.now();
const API_DELETE_URL = `https://discord.com/api/v6/channels/${message.channel_id}/messages/${message.id}`;
resp = await fetch(API_DELETE_URL, {
headers,
method: 'DELETE'
});
lastPing = (Date.now() - s);
avgPing = (avgPing * 0.9) + (lastPing * 0.1);
delCount++;
} catch (err) {
log.error('Delete request throwed an error:', err);
log.verb('Related object:', redact(JSON.stringify(message)));
failCount++;
}
if (!resp.ok) {
// deleting messages too fast
if (resp.status === 429) {
const w = (await resp.json()).retry_after;
throttledCount++;
throttledTotalTime += w;
deleteDelay = w; // increase delay
log.warn(`Being rate limited by the API for ${w}ms! Adjusted delete delay to ${deleteDelay}ms.`);
printDelayStats();
log.verb(`Cooling down for ${w * 2}ms before retrying...`);
await wait(w * 2);
i--; // retry
} else {
log.error(`Error deleting message, API responded with status ${resp.status}!`, await resp.json());
log.verb('Related object:', redact(JSON.stringify(message)));
failCount++;
}
}
await wait(deleteDelay);
}
if (skippedMessages.length > 0) {
grandTotal -= skippedMessages.length;
offset += skippedMessages.length;
log.verb(`Found ${skippedMessages.length} system messages! Decreasing grandTotal to ${grandTotal} and increasing offset to ${offset}.`);
}
log.verb(`Searching next messages in ${searchDelay}ms...`, (offset ? `(offset: ${offset})` : ''));
await wait(searchDelay);
if (stopHndl && stopHndl() === false) return end(log.error('Stopped by you!'));
return await recurse();
} else {
if (total - offset > 0) log.warn('Ended because API returned an empty page.');
return end();
}
}
log.success(`\nStarted at ${start.toLocaleString()}`);
log.debug(`authorId="${redact(authorId)}" guildId="${redact(guildId)}" channelId="${redact(channelId)}" minId="${redact(minId)}" maxId="${redact(maxId)}" hasLink=${!!hasLink} hasFile=${!!hasFile}`);
if (onProgress) onProgress(null, 1);
return await recurse();
}
//---- User interface ----//
let popover;
let btn;
let stop;
function initUI() {
const insertCss = (css) => {
const style = document.createElement('style');
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
return style;
}
const createElm = (html) => {
const temp = document.createElement('div');
temp.innerHTML = html;
return temp.removeChild(temp.firstElementChild);
}
insertCss(`
#undicord-btn{position: relative; height: 24px;width: auto;-webkit-box-flex: 0;-ms-flex: 0 0 auto;flex: 0 0 auto;margin: 0 8px;cursor:pointer; color: var(--interactive-normal);}
#undiscord{position:fixed;top:100px;right:10px;bottom:10px;width:780px;z-index:99;color:var(--text-normal);background-color:var(--background-secondary);box-shadow:var(--elevation-stroke),var(--elevation-high);border-radius:4px;display:flex;flex-direction:column}
#undiscord a{color:#00b0f4}
#undiscord.redact .priv{display:none!important}
#undiscord:not(.redact) .mask{display:none!important}
#undiscord.redact [priv]{-webkit-text-security:disc!important}
#undiscord .toolbar span{margin-right:8px}
#undiscord button,#undiscord .btn{color:#fff;background:#7289da;border:0;border-radius:4px;font-size:14px}
#undiscord button:disabled{display:none}
#undiscord input[type="text"],#undiscord input[type="search"],#undiscord input[type="password"],#undiscord input[type="datetime-local"],#undiscord input[type="number"]{background-color:#202225;color:#b9bbbe;border-radius:4px;border:0;padding:0 .5em;height:24px;width:144px;margin:2px}
#undiscord input#file{display:none}
#undiscord hr{border-color:rgba(255,255,255,0.1)}
#undiscord .header{padding:12px 16px;background-color:var(--background-tertiary);color:var(--text-muted)}
#undiscord .form{padding:8px;background:var(--background-secondary);box-shadow:0 1px 0 rgba(0,0,0,.2),0 1.5px 0 rgba(0,0,0,.05),0 2px 0 rgba(0,0,0,.05)}
#undiscord .logarea{overflow:auto;font-size:.75rem;font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;flex-grow:1;padding:10px}
`);
popover = createElm(`
<div id="undiscord" style="display:none;">
<div class="header">
Undiscord - Bulk delete messages
</div>
<div class="form">
<div style="display:flex;flex-wrap:wrap;">
<span>Authorization <a
href="https://github.com/victornpb/deleteDiscordMessages/blob/master/help/authToken.md" title="Help"
target="_blank">?</a> <button id="getToken">get</button><br>
<input type="password" id="authToken" placeholder="Auth Token" autofocus>*<br>
<span>Author <a href="https://github.com/victornpb/deleteDiscordMessages/blob/master/help/authorId.md"
title="Help" target="_blank">?</a> <button id="getAuthor">get</button></span>
<br><input id="authorId" type="text" placeholder="Author ID" priv></span>
<span>Guild/Channel <a
href="https://github.com/victornpb/deleteDiscordMessages/blob/master/help/channelId.md" title="Help"
target="_blank">?</a>
<button id="getGuildAndChannel">get</button><br>
<input id="guildId" type="text" placeholder="Guild ID" priv><br>
<input id="channelId" type="text" placeholder="Channel ID" priv><br>
<label><input id="includeNsfw" type="checkbox">NSFW Channel</label><br><br>
<label for="file" title="Import list of channels from messages/index.json file"> Import: <span
class="btn">...</span> <input id="file" type="file" accept="application/json,.json"></label>
</span><br>
<span>Range <a href="https://github.com/victornpb/deleteDiscordMessages/blob/master/help/messageId.md"
title="Help" target="_blank">?</a><br>
<input id="minDate" type="datetime-local" title="After" style="width:auto;"><br>
<input id="maxDate" type="datetime-local" title="Before" style="width:auto;"><br>
<input id="minId" type="text" placeholder="After message with Id" priv><br>
<input id="maxId" type="text" placeholder="Before message with Id" priv><br>
</span>
<span>Search messages <a
href="https://github.com/victornpb/deleteDiscordMessages/blob/master/help/filters.md" title="Help"
target="_blank">?</a><br>
<input id="content" type="text" placeholder="Containing text" priv><br>
<label><input id="hasLink" type="checkbox">has: link</label><br>
<label><input id="hasFile" type="checkbox">has: file</label><br>
<label><input id="includePinned" type="checkbox">Include pinned</label>
</span><br>
<span>Search Delay <a
href="https://github.com/victornpb/deleteDiscordMessages/blob/master/help/delay.md" title="Help"
target="_blank">?</a><br>
<input id="searchDelay" type="number" value="100" step="100"><br>
</span>
<span>Delete Delay <a
href="https://github.com/victornpb/deleteDiscordMessages/blob/master/help/delay.md" title="Help"
target="_blank">?</a><br>
<input id="deleteDelay" type="number" value="1000" step="100">
</span>
</div>
<hr>
<button id="start" style="background:#43b581;width:80px;">Start</button>
<button id="stop" style="background:#f04747;width:80px;" disabled>Stop</button>
<button id="clear" style="width:80px;">Clear log</button>
<label><input id="autoScroll" type="checkbox" checked>Auto scroll</label>
<label title="Hide sensitive information for taking screenshots"><input id="redact" type="checkbox">Screenshot
mode</label>
<progress id="progress" style="display:none;"></progress> <span class="percent"></span>
</div>
<pre class="logarea">
<center>Star this project on <a href="https://github.com/victornpb/deleteDiscordMessages" target="_blank">github.com/victornpb/deleteDiscordMessages</a>!\n\n
<a href="https://github.com/victornpb/deleteDiscordMessages/issues" target="_blank">Issues or help</a>
</center>
</pre>
</div>
`);
document.body.appendChild(popover);
btn = createElm(`<div id="undicord-btn" tabindex="0" role="button" aria-label="Delete Messages" title="Delete Messages">
<svg aria-hidden="false" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M15 3.999V2H9V3.999H3V5.999H21V3.999H15Z"></path>
<path fill="currentColor" d="M5 6.99902V18.999C5 20.101 5.897 20.999 7 20.999H17C18.103 20.999 19 20.101 19 18.999V6.99902H5ZM11 17H9V11H11V17ZM15 17H13V11H15V17Z"></path>
</svg>
<br><progress style="display:none; width:24px;"></progress>
</div>`);
btn.onclick = function togglePopover() {
if (popover.style.display !== 'none') {
popover.style.display = 'none';
btn.style.color = 'var(--interactive-normal)';
}
else {
popover.style.display = '';
btn.style.color = '#f04747';
}
};
function mountBtn() {
const toolbar = document.querySelector('[class^=toolbar]');
if (toolbar) toolbar.appendChild(btn);
}
const observer = new MutationObserver(function (_mutationsList, _observer) {
if (!document.body.contains(btn)) mountBtn(); // re-mount the button to the toolbar
});
observer.observe(document.body, { attributes: false, childList: true, subtree: true });
mountBtn();
const $ = s => popover.querySelector(s);
const logArea = $('pre');
const startBtn = $('button#start');
const stopBtn = $('button#stop');
const autoScroll = $('#autoScroll');
startBtn.onclick = async e => {
const authToken = $('input#authToken').value.trim();
const authorId = $('input#authorId').value.trim();
const guildId = $('input#guildId').value.trim();
const channelIds = $('input#channelId').value.trim().split(/\s*,\s*/);
const minId = $('input#minId').value.trim();
const maxId = $('input#maxId').value.trim();
const minDate = $('input#minDate').value.trim();
const maxDate = $('input#maxDate').value.trim();
const content = $('input#content').value.trim();
const hasLink = $('input#hasLink').checked;
const hasFile = $('input#hasFile').checked;
const includeNsfw = $('input#includeNsfw').checked;
const includePinned = $('input#includePinned').checked;
const searchDelay = parseInt($('input#searchDelay').value.trim());
const deleteDelay = parseInt($('input#deleteDelay').value.trim());
const progress = $('#progress');
const progress2 = btn.querySelector('progress');
const percent = $('.percent');
const fileSelection = $("input#file");
fileSelection.addEventListener("change", () => {
const files = fileSelection.files;
const channelIdField = $('input#channelId');
if (files.length > 0) {
const file = files[0];
file.text().then(text => {
let json = JSON.parse(text);
let channels = Object.keys(json);
channelIdField.value = channels.join(",");
});
}
}, false);
const stopHndl = () => !(stop === true);
const onProg = (value, max) => {
if (value && max && value > max) max = value;
progress.setAttribute('max', max);
progress.value = value;
progress.style.display = max ? '' : 'none';
progress2.setAttribute('max', max);
progress2.value = value;
progress2.style.display = max ? '' : 'none';
percent.innerHTML = value && max ? Math.round(value / max * 100) + '%' : '';
};
stop = stopBtn.disabled = !(startBtn.disabled = true);
for (let i = 0; i < channelIds.length; i++) {
await deleteMessages(authToken, authorId, guildId, channelIds[i], minId || minDate, maxId || maxDate, content, hasLink, hasFile, includeNsfw, includePinned, searchDelay, deleteDelay, logger, stopHndl, onProg);
stop = stopBtn.disabled = !(startBtn.disabled = false);
}
};
stopBtn.onclick = e => stop = stopBtn.disabled = !(startBtn.disabled = false);
$('button#clear').onclick = e => { logArea.innerHTML = ''; };
$('button#getToken').onclick = e => {
window.dispatchEvent(new Event('beforeunload'));
const ls = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
$('input#authToken').value = JSON.parse(localStorage.token);
};
$('button#getAuthor').onclick = e => {
$('input#authorId').value = JSON.parse(localStorage.user_id_cache);
};
$('button#getGuildAndChannel').onclick = e => {
const m = location.href.match(/channels\/([\w@]+)\/(\d+)/);
$('input#guildId').value = m[1];
$('input#channelId').value = m[2];
};
$('#redact').onchange = e => {
popover.classList.toggle('redact') &&
window.alert('This will attempt to hide personal information, but make sure to double check before sharing screenshots.');
};
const logger = (type = '', args) => {
const style = { '': '', info: 'color:#ff0000;', verb: 'color:#72767d;', warn: 'color:#faa61a;', error: 'color:#f04747;', success: 'color:#43b581;' }[type];
logArea.insertAdjacentHTML('beforeend', `<div style="${style}">${Array.from(args).map(o => typeof o === 'object' ? JSON.stringify(o, o instanceof Error && Object.getOwnPropertyNames(o)) : o).join('\t')}</div>`);
if (autoScroll.checked) logArea.querySelector('div:last-child').scrollIntoView(false);
};
// fixLocalStorage
window.localStorage = document.body.appendChild(document.createElement('iframe')).contentWindow.localStorage;
}
initUI();
//END
(async function() {
'use strict';
let last_ver = '';
let v1 = await GM.getValue('version', '1');
let s1 = v1.split('.');
function comp_ver (v2) {
last_ver = v2;
let s2 = v2.split('.');
for (let i = 0; i < Math.min(s1.length, s2.length); i += 1) {
let a = parseInt(s1[i]);
let b = parseInt(s2[i]);
if (a !== b) {
return a < b;
}
}
return s1.length < s2.length;
}
let urls = await GM.getValue('urls', {});
if (comp_ver('1.4.10')) {
for (let [key, entry] of Object.entries(urls)) {
urls[key] = [entry];
}
await GM.setValue('urls', urls);
}
if (comp_ver('1.4.16')) {
for (let [key, entry] of Object.entries(urls)) {
if (key.lastIndexOf('.') < key.lastIndexOf('/')) {
delete urls[key];
}
}
await GM.setValue('urls', urls);
}
GM.setValue('version', last_ver);
function sleep (time) {
return new Promise((ok) => {setTimeout(ok, time);});
}
function createElementFromHTML(htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div.firstChild;
}
function get_token () {
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
let storage = iframe.contentWindow.localStorage;
let token = storage.token.replace(/"/g, '');
iframe.remove();
return token;
}
function send (message) {
let token = get_token();
let channel_id = document.location.pathname.split('/').pop();
let data = {
"content": message,
"tts": "false"
}
$.ajax({
type: 'POST',
url: 'https://discord.com/api/v6/channels/' + channel_id + '/messages',
data: JSON.stringify(data),
headers: {
'%3Aauthority': 'discord.com',
'%3Amethod': 'POST',
'%3Apath': '/api/v6/channels/' + channel_id + '/messages',
'%3Ascheme': 'https',
'Authorization': token,
'Content-Type': 'application/json'
}
});
}
const probability = 35;
document.addEventListener('mousedown', function (e) {
let link = undefined;
function set_link (img, size) {
if (img !== null) {
link = new URL(img.src);
let title = $('[class*="titleSecondary-"] > strong')[0];
let server = link.searchParams.get('server');
if (server === null && title) {
server = title.textContent;
}
link.search = '?' + (right_click ? '' : `size=${size}`);
if (server) {
link.search += `&server=${server}`;
}
}
}
let right_click = (e.button === 2);
if (e.target.classList.value.includes('emojiItem-')
&& (e.target.classList.value.includes('emojiItemDisabled')
|| right_click)) {
let img = e.target.querySelector('img');
set_link(img, 48);
} else
if (e.target.classList.value.includes('stickerAsset-')
&& (e.target.parentNode.parentNode.classList.value.includes('stickerUnsendable')
|| right_click)) {
let img = e.target;
set_link(img, 160);
} else
if (e.target.parentNode.id === 'cheatemoji') {
let img = e.target;
right_click = right_click || img.srcset;
set_link(img, 48);
}
if (link !== undefined) {
if (Math.random() * 1000 < probability) {
add_gayness(); // gayness for all!
}
send(link.href);
}
});
urls = await GM.getValue('urls', {});
let hover_url = '';
let hover_server = '';
const button_size = 28;
let button = createElementFromHTML(`<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="24" height="24"
viewBox="0 0 48 48"
><path d="M 24 4 C 12.972066 4 4 12.972074 4 24 C 4 35.027926 12.972066 44 24 44 C 35.027934 44 44 35.027926 44 24 C 44 12.972074 35.027934 4 24 4 z M 24 7 C 33.406615 7 41 14.593391 41 24 C 41 33.406609 33.406615 41 24 41 C 14.593385 41 7 33.406609 7 24 C 7 14.593391 14.593385 7 24 7 z M 23.976562 13.978516 A 1.50015 1.50015 0 0 0 22.5 15.5 L 22.5 22.5 L 15.5 22.5 A 1.50015 1.50015 0 1 0 15.5 25.5 L 22.5 25.5 L 22.5 32.5 A 1.50015 1.50015 0 1 0 25.5 32.5 L 25.5 25.5 L 32.5 25.5 A 1.50015 1.50015 0 1 0 32.5 22.5 L 25.5 22.5 L 25.5 15.5 A 1.50015 1.50015 0 0 0 23.976562 13.978516 z"></path>
<circle style="pointer-events: visible;" id="svg_circle" cx="24" cy="24" r="29" fill="transparent"></circle></svg>`);
let svg;
setTimeout(() => {svg = $('#svg_circle')[0];}, 5);
button.id = 'plus_svg';
document.body.append(button);
button.style['z-index'] = 10000;
button.style.position = 'absolute';
button.style.width = button_size + 'px';
button.style.height = button_size + 'px';
button.style['border-radius'] = '15px';
hide_button();
button.addEventListener('click', async () => {style_button(await toggle_url(hover_url));});
function style_button (checked) {
if (checked) {
button.classList.add('checked');
} else {
button.classList.remove('checked');
}
}
async function toggle_url (url) {
if (!urls[url]) {
urls[url] = [hover_server, deep];
add_image(url, [hover_server, deep]);
} else {
delete urls[url];
images[url].remove();
}
await GM.setValue('urls', urls);
return urls[url];
}
let hide_timeout;
let current = true;
let test_img = document.createElement('img');
let deep;
async function hover (e) {
let {target} = e;
let parent = target.parentNode;
//let hide = !button.contains(target);
let hide = target !== svg;
if (target.tagName === 'IMG') {
let emoji = false;
let url;
if (parent.tagName === 'A') {
emoji = true;
url = new URL(parent.href);
let server = url.searchParams.get('server');
target.title = (server !== null ? `from ${server}` : '');
}
let temp = target;
for (let i of [1,2,3]) {
temp = temp.parentNode;
if (temp.classList.value.includes('messageContent')) {
emoji = true;
}
}
if (!emoji || parent.getAttribute('href') === '') {
hide_button();
return;
}
deep = undefined;
test_img.src = url;
test_img.onerror = () => {
deep = new URL(target.src)
};
await sleep(50);
test_img.onerror = undefined;
let preview = url && deep;
url = url || deep;
if (url.pathname.slice(-4) === '.svg') {
hide_button();
return;
}
hover_url = url.origin + url.pathname;
let chat_rect = $('[class^="chatContent"]')[0].getBoundingClientRect();
let rect = target.getBoundingClientRect();
button.style.left = rect.right + 3 + 'px';
button.style.top = Math.max(rect.top + (48 - button_size)/2, chat_rect.top + 3) + 'px';
style_button(urls[hover_url]);
hover_server = url.searchParams.get('server');
if (urls[hover_url]) {
let image = images[hover_url];
if (hover_server) {
urls[hover_url][0] = hover_server;
let temp = new URL(image.src);
temp.search = `?size=48&server=${hover_server}`;
image.src = temp.href;
}
if (preview) {
image.srcset = urls[hover_url][1] = deep.href;
}
GM.setValue('urls', urls);
}
hide = false;
}
/*if (hide !== current) {
clearTimeout(hide_timeout);
hide_timeout = setTimeout(() => {button.style.display = hide ? 'none' : 'block';}, 300);
current = hide;
}*/
if (hide) {
hide_button();
} else {
button.style.visibility = button.style.opacity = '';
}
}
function hide_button () {
button.style.visibility = 'hidden';
button.style.opacity = '0';
}
document.addEventListener('mousemove', hover);
document.body.addEventListener('wheel', hide_button);
document.addEventListener('click', (e) => {
if (!button.contains(e.target)) {
hide_button();
}
});
let images = {};
let div = document.createElement('div');
div.id = 'cheatemoji';
div.addEventListener('contextmenu', (e) => {e.preventDefault();}, true);
for (let [url, entry] of Object.entries(urls)) {
add_image(url, entry);
}
function add_image (url, entry) {
let [server, preview] = entry;
let img = document.createElement('img');
let obj = new URL(url);
obj.search = `?size=48`;
if (server) {
obj.search += `&server=${server}`;
}
img.src = obj.href;
if (preview) {
img.srcset = preview;
}
div.append(img);
images[url] = img;
}
let iframe2 = document.createElement('div');
let fresh = true;
let max_height = 0;
setInterval(async () => {
let list;
let elem = $('[class*="bodyWrapper-"]')[0];
if (elem !== undefined && !document.contains(iframe2)) {
if (Math.random()*1000 < probability && await GM.getValue('active', true)) {
let width = 400;
let height = Math.trunc(width * 3/8)*2;
iframe2 = createElementFromHTML(`<iframe width="${width}" height="${height}" src="https://www.youtube.com/embed/DLzxrzFCyOs?autoplay=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`);
elem.appendChild(iframe2);
iframe2.style.position = 'absolute';
iframe2.style['z-index'] = 10000;
iframe2.style.left = 0;
iframe2.style.top = 0;
iframe2.style.width = '100%';
iframe2.style.height = '100%';
//iframe2.style['border-radius'] = '1000px';
let div = document.createElement('div');
elem.appendChild(div);
div.style.position = 'absolute';
div.style['z-index'] = 10001;
div.style.left = 0;
div.style.top = 0;
div.style.width = `${width}px`;
div.style.height = `${height}px`;
} else {
iframe2 = document.createElement('div');
elem.appendChild(iframe2);
}
}
list = elem && elem.querySelector('[class^="listItems"]');
let list_h = list && elem.querySelector('[class^="listHeight"]');
if (list && fresh && div.children.length !== 0) {
list.prepend(div);
div.style.height = '';
max_height = div.getBoundingClientRect().height - list_h.getBoundingClientRect().top;
}
if (list) {
let cur_height = div.offsetHeight;
let new_height = Math.max(max_height + list_h.getBoundingClientRect().top, 0);
//if (new_height < cur_height || new_height >= cur_height + 40) {
div.style.height = new_height + 'px';
//}
}
fresh = (elem === undefined);
}, 300);
let last = await GM.getValue('last', 0);
if (!isFinite(last)) {
last = 0;
}
async function add_gayness () {
if (!await GM.getValue('active', true)) {
return;
}
let time = Date.now();
if (time - last < 3600 * 1000) {
return;
}
last = time;
GM.setValue('last', last);
function createElementFromHTML(htmlString) {
var div = document.createElement('div');
div.innerHTML = htmlString.trim();
return div.firstChild;
}
//let iframe = createElementFromHTML(`<iframe width="400" height="300" src="https://www.youtube.com/embed/DLzxrzFCyOs?autoplay=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`);
//document.body.appendChild(iframe);
//iframe.style.position = 'fixed';
//iframe.style['z-index'] = 10000;
//iframe.style.left = 0;
//iframe.style.top = 0;
//iframe.style.width = '100%';
// iframe.style.height = '100%';#
//setTimeout(() => send(text), 1500);
setTimeout(() => iframe.remove(), 15000);
}
let style = document.createElement('style');
style.textContent =
`
.theme-dark {
--script-color: white;
}
.theme-light {
--script-color: purple;
}
` +
`.theme-dark [class*="emojiItemDisabled"] {\n`+
` filter: drop-shadow(0px 0px 1px white) !important;\n`+
`}\n`+
`.theme-light [class*="emojiItemDisabled"] {\n`+
` filter: drop-shadow(0px 0px 2px purple) !important;\n`+
`}\n`+
`.theme-dark [class*="stickerUnsendable"] {\n`+
` filter: drop-shadow(0px 0px 1px white) !important;\n`+
`}\n`+
`.theme-light [class*="stickerUnsendable"] {\n`+
` filter: drop-shadow(0px 0px 2px purple) !important;\n`+
`}\n`+
`[class*="premiumPromo-"], [class*="upsellWrapper-"] {\n`+
` display: none !important;\n`+
`}\n`+
`#plus_svg {
transition: 0.3s ease-in-out;
transition-property: transform, visibility, opacity;
fill: white;
cursor: pointer;
opacity: 0.7;
pointer-events: none;
overflow: visible;
}
#plus_svg.checked {
transform: rotate(45deg);
}
#plus_svg:hover {
opacity: 1;
}
.theme-light #plus_svg {
fill: purple;
}
#cheatemoji {
display: flex;
flex-wrap: wrap-reverse;
margin-top: 5px;
padding-bottom: 5px;
overflow-y: clip;
}
#cheatemoji img {
height: 32px;
width: 32px;
object-fit: contain;
border-radius: 4px;
}
#cheatemoji img:hover {
filter: drop-shadow(0px 0px 2px var(--script-color)) !important;
}
`
;
document.head.append(style);
})();
;(function () {
'use strict'
const DEBUG = true
const createLogger = (console, tag) =>
Object.keys(console)
.map(k => [k, (...args) => (DEBUG ? console[k](tag + ': ' + args[0], ...args.slice(1)) : void 0)])
.reduce((acc, [k, fn]) => ((acc[k] = fn), acc), {})
const logger = createLogger(console, 'YTDL')
const sleep = ms => new Promise(res => setTimeout(res, ms))
const LANG_FALLBACK = 'en'
const LOCALE = {
en: {
togglelinks: 'Show/Hide Links',
stream: 'Stream',
adaptive: 'Adaptive (No Sound)',
videoid: 'Video ID: ',
inbrowser_adaptive_merger: 'Online Adaptive Video & Audio Merger (FFmpeg)',
dlmp4: 'Download high-resolution mp4 in one click',
get_video_failed: 'Failed to get video infomation for unknown reason, refresh the page may work.',
live_stream_disabled_message: 'Local YouTube Downloader is not available for live stream'
}
}
for (const [lang, data] of Object.entries(LOCALE)) {
if (lang === LANG_FALLBACK) continue
for (const key of Object.keys(LOCALE[LANG_FALLBACK])) {
if (!(key in data)) {
data[key] = LOCALE[LANG_FALLBACK][key]
}
}
}
const findLang = l => {
l = l.replace('-Hant', '') // special case for zh-Hant-TW
// language resolution logic: zh-tw --(if not exists)--> zh --(if not exists)--> LANG_FALLBACK(en)
l = l.toLowerCase().replace('_', '-')
if (l in LOCALE) return l
else if (l.length > 2) return findLang(l.split('-')[0])
else return LANG_FALLBACK
}
const getLangCode = () => {
const html = document.querySelector('html')
if (html) {
return html.lang
} else {
return navigator.language
}
}
const $ = (s, x = document) => x.querySelector(s)
const $el = (tag, opts) => {
const el = document.createElement(tag)
Object.assign(el, opts)
return el
}
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const parseDecsig = data => {
try {
if (data.startsWith('var script')) {
// they inject the script via script tag
const obj = {}
const document = {
createElement: () => obj,
head: { appendChild: () => {} }
}
eval(data)
data = obj.innerHTML
}
const fnnameresult = /=([a-zA-Z0-9\$]+?)\(decodeURIComponent/.exec(data)
const fnname = fnnameresult[1]
const _argnamefnbodyresult = new RegExp(escapeRegExp(fnname) + '=function\\((.+?)\\){((.+)=\\2.+?)}').exec(
data
)
const [_, argname, fnbody] = _argnamefnbodyresult
const helpernameresult = /;(.+?)\..+?\(/.exec(fnbody)
const helpername = helpernameresult[1]
const helperresult = new RegExp('var ' + escapeRegExp(helpername) + '={[\\s\\S]+?};').exec(data)
const helper = helperresult[0]
logger.log(`parsedecsig result: %s=>{%s\n%s}`, argname, helper, fnbody)
return new Function([argname], helper + '\n' + fnbody)
} catch (e) {
logger.error('parsedecsig error: %o', e)
logger.info('script content: %s', data)
logger.info(
'If you encounter this error, please copy the full "script content" to https://pastebin.com/ for me.'
)
}
}
const parseQuery = s => [...new URLSearchParams(s).entries()].reduce((acc, [k, v]) => ((acc[k] = v), acc), {})
const parseResponse = (id, playerResponse, decsig) => {
logger.log(`video %s playerResponse: %o`, id, playerResponse)
let stream = []
if (playerResponse.streamingData.formats) {
stream = playerResponse.streamingData.formats.map(x =>
Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
)
logger.log(`video %s stream: %o`, id, stream)
for (const obj of stream) {
if (obj.s) {
obj.s = decsig(obj.s)
obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`
}
}
}
let adaptive = []
if (playerResponse.streamingData.adaptiveFormats) {
adaptive = playerResponse.streamingData.adaptiveFormats.map(x =>
Object.assign({}, x, parseQuery(x.cipher || x.signatureCipher))
)
logger.log(`video %s adaptive: %o`, id, adaptive)
for (const obj of adaptive) {
if (obj.s) {
obj.s = decsig(obj.s)
obj.url += `&${obj.sp}=${encodeURIComponent(obj.s)}`
}
}
}
logger.log(`video %s result: %o`, id, { stream, adaptive })
return { stream, adaptive, details: playerResponse.videoDetails, playerResponse }
}
const determineChunksNum = size => {
const n = Math.ceil(size / (1024 * 1024 * 3)) // 3 MB
return n
}
// video downloader
const xhrDownloadUint8Array = async ({ url, contentLength }, progressCb) => {
if (typeof contentLength === 'string') contentLength = parseInt(contentLength)
progressCb({
loaded: 0,
total: contentLength,
speed: 0
})
const chunkSize = Math.floor(contentLength / determineChunksNum(contentLength))
const getBuffer = (start, end) =>
new Promise((res, rej) => {
const xhr = {}
xhr.responseType = 'arraybuffer'
xhr.method = 'GET'
xhr.url = url
xhr.headers = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Edg/94.0.992.50',
Range: `bytes=${start}-${end ? end - 1 : ''}`,
Accept: '*/*',
'Accept-Encoding': 'identity',
'Accept-Language': 'en-us,en;q=0.5',
Origin: 'https://www.youtube.com',
Referer: 'https://www.youtube.com/',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site'
}
xhr.onload = obj => {
if (obj.status >= 200 && obj.status < 300) {
res(obj.response)
} else {
rej(obj)
}
}
GM_xmlhttpRequest(xhr)
})
const data = new Uint8Array(contentLength)
let downloaded = 0
const queue = new pQueue.default({ concurrency: 5 })
const startTime = Date.now()
const ps = []
for (let start = 0; start < contentLength; start += chunkSize) {
const exceeded = start + chunkSize > contentLength
const curChunkSize = exceeded ? contentLength - start : chunkSize
const end = exceeded ? null : start + chunkSize
const p = queue.add(() =>
getBuffer(start, end)
.then(buf => {
downloaded += curChunkSize
data.set(new Uint8Array(buf), start)
const ds = (Date.now() - startTime + 1) / 1000
progressCb({
loaded: downloaded,
total: contentLength,
speed: downloaded / ds
})
})
.catch(err => {
queue.clear()
alert('Download error')
})
)
ps.push(p)
}
await Promise.all(ps)
return data
}
const ffWorker = FFmpeg.createWorker({
logger: DEBUG ? m => logger.log(m.message) : () => {}
})
let ffWorkerLoaded = false
const mergeVideo = async (video, audio) => {
if (!ffWorkerLoaded) await ffWorker.load()
await ffWorker.write('video.mp4', video)
await ffWorker.write('audio.mp4', audio)
await ffWorker.run('-i video.mp4 -i audio.mp4 -c copy output.mp4', {
input: ['video.mp4', 'audio.mp4'],
output: 'output.mp4'
})
const { data } = await ffWorker.read('output.mp4')
await ffWorker.remove('output.mp4')
return data
}
const triggerDownload = (url, filename) => {
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
a.remove()
}
const dlModalTemplate = `
<div style="width: 100%; height: 100%;">
<div v-if="merging" style="height: 100%; width: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px;">Merging video, please wait...</div>
<div v-else style="height: 100%; width: 100%; display: flex; flex-direction: column;">
<div style="flex: 1; margin: 10px;">
<p style="font-size: 24px;">Video</p>
<progress style="width: 100%;" :value="video.progress" min="0" max="100"></progress>
<div style="display: flex; justify-content: space-between;">
<span>{{video.speed}} kB/s</span>
<span>{{video.loaded}}/{{video.total}} MB</span>
</div>
</div>
<div style="flex: 1; margin: 10px;">
<p style="font-size: 24px;">Audio</p>
<progress style="width: 100%;" :value="audio.progress" min="0" max="100"></progress>
<div style="display: flex; justify-content: space-between;">
<span>{{audio.speed}} kB/s</span>
<span>{{audio.loaded}}/{{audio.total}} MB</span>
</div>
</div>
</div>
</div>
`
function openDownloadModel(adaptive, title) {
const win = open(
'',
'Video Download',
`toolbar=no,height=${screen.height / 2},width=${screen.width / 2},left=${screenLeft},top=${screenTop}`
)
const div = win.document.createElement('div')
win.document.body.appendChild(div)
win.document.title = `Downloading "${title}"`
const dlModalApp = new Vue({
template: dlModalTemplate,
data() {
return {
video: {
progress: 0,
total: 0,
loaded: 0,
speed: 0
},
audio: {
progress: 0,
total: 0,
loaded: 0,
speed: 0
},
merging: false
}
},
methods: {
async start(adaptive, title) {
win.onbeforeunload = () => true
// YouTube's default order is descending by video quality
const videoObj = adaptive
.filter(x => x.mimeType.includes('video/mp4') || x.mimeType.includes('video/webm'))
.map(v => {
const [_, quality, fps] = /(\d+)p(\d*)/.exec(v.qualityLabel)
v.qualityNum = parseInt(quality)
v.fps = fps ? parseInt(fps) : 30
return v
})
.sort((a, b) => {
if (a.qualityNum === b.qualityNum) return b.fps - a.fps // ex: 30-60=-30, then a will be put before b
return b.qualityNum - a.qualityNum
})[0]
const audioObj = adaptive.find(x => x.mimeType.includes('audio/mp4'))
const vPromise = xhrDownloadUint8Array(videoObj, e => {
this.video.progress = (e.loaded / e.total) * 100
this.video.loaded = (e.loaded / 1024 / 1024).toFixed(2)
this.video.total = (e.total / 1024 / 1024).toFixed(2)
this.video.speed = (e.speed / 1024).toFixed(2)
})
const aPromise = xhrDownloadUint8Array(audioObj, e => {
this.audio.progress = (e.loaded / e.total) * 100
this.audio.loaded = (e.loaded / 1024 / 1024).toFixed(2)
this.audio.total = (e.total / 1024 / 1024).toFixed(2)
this.audio.speed = (e.speed / 1024).toFixed(2)
})
const [varr, aarr] = await Promise.all([vPromise, aPromise])
this.merging = true
win.onunload = () => {
// trigger download when user close it
const bvurl = URL.createObjectURL(new Blob([varr]))
const baurl = URL.createObjectURL(new Blob([aarr]))
triggerDownload(bvurl, title + '-videoonly.mp4')
triggerDownload(baurl, title + '-audioonly.mp4')
}
const result = await Promise.race([mergeVideo(varr, aarr), sleep(1000 * 25).then(() => null)])
if (!result) {
alert('An error has occurred when merging video')
const bvurl = URL.createObjectURL(new Blob([varr]))
const baurl = URL.createObjectURL(new Blob([aarr]))
triggerDownload(bvurl, title + '-videoonly.mp4')
triggerDownload(baurl, title + '-audioonly.mp4')
return this.close()
}
this.merging = false
const url = URL.createObjectURL(new Blob([result]))
triggerDownload(url, title + '.mp4')
win.onbeforeunload = null
win.onunload = null
win.close()
}
}
}).$mount(div)
dlModalApp.start(adaptive, title)
}
const template = `
<div class="box" :class="{'dark':dark}">
<template v-if="!isLiveStream">
<div v-if="adaptive.length" class="of-h t-center c-pointer lh-20">
<a class="fs-14px" @click="dlmp4" v-text="strings.dlmp4"></a>
</div>
<div @click="hide=!hide" class="box-toggle div-a t-center fs-14px c-pointer lh-20" v-text="strings.togglelinks"></div>
<div :class="{'hide':hide}">
<div class="t-center fs-14px" v-text="strings.videoid+id"></div>
<div class="d-flex">
<div class="f-1 of-h">
<div class="t-center fs-14px" v-text="strings.stream"></div>
<a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in stream" :href="vid.url" :title="vid.type" v-text="formatStreamText(vid)"></a>
</div>
<div class="f-1 of-h">
<div class="t-center fs-14px" v-text="strings.adaptive"></div>
<a class="ytdl-link-btn fs-14px" target="_blank" v-for="vid in adaptive" :href="vid.url" :title="vid.type" v-text="formatAdaptiveText(vid)"></a>
</div>
</div>
<div class="of-h t-center">
<a class="fs-14px" href="https://maple3142.github.io/mergemp4/" target="_blank" v-text="strings.inbrowser_adaptive_merger"></a>
</div>
</div>
</template>
<template v-else>
<div class="t-center fs-14px lh-20" v-text="strings.live_stream_disabled_message"></div>
</template>
</div>
`.slice(1)
const app = new Vue({
data() {
return {
hide: true,
id: '',
isLiveStream: false,
stream: [],
adaptive: [],
details: null,
dark: false,
lang: findLang(getLangCode())
}
},
computed: {
strings() {
return LOCALE[this.lang.toLowerCase()]
}
},
methods: {
dlmp4() {
openDownloadModel(this.adaptive, this.details.title)
},
formatStreamText(vid) {
return [vid.qualityLabel, vid.quality].filter(x => x).join(': ')
},
formatAdaptiveText(vid) {
let str = [vid.qualityLabel, vid.mimeType].filter(x => x).join(': ')
if (vid.mimeType.includes('audio')) {
str += ` ${Math.round(vid.bitrate / 1000)}kbps`
}
return str
}
},
template
})
logger.log(`default language: %s`, app.lang)
// attach element
const shadowHost = $el('div')
const shadow = shadowHost.attachShadow ? shadowHost.attachShadow({ mode: 'closed' }) : shadowHost // no shadow dom
logger.log('shadowHost: %o', shadowHost)
const container = $el('div')
shadow.appendChild(container)
app.$mount(container)
if (DEBUG && typeof unsafeWindow !== 'undefined') {
// expose some functions for debugging
unsafeWindow.$app = app
unsafeWindow.parseQuery = parseQuery
unsafeWindow.parseDecsig = parseDecsig
unsafeWindow.parseResponse = parseResponse
}
const load = async playerResponse => {
try {
const basejs =
(typeof ytplayer !== 'undefined' && 'config' in ytplayer && ytplayer.config.assets
? 'https://' + location.host + ytplayer.config.assets.js
: 'web_player_context_config' in ytplayer
? 'https://' + location.host + ytplayer.web_player_context_config.jsUrl
: null) || $('script[src$="base.js"]').src
const decsig = await xf.get(basejs).text(parseDecsig)
const id = parseQuery(location.search).v
const data = parseResponse(id, playerResponse, decsig)
logger.log('video loaded: %s', id)
app.isLiveStream = data.playerResponse.playabilityStatus.liveStreamability != null
app.id = id
app.stream = data.stream
app.adaptive = data.adaptive
app.details = data.details
const actLang = getLangCode()
if (actLang != null) {
const lang = findLang(actLang)
logger.log('youtube ui lang: %s', actLang)
logger.log('ytdl lang:', lang)
app.lang = lang
}
} catch (err) {
alert(app.strings.get_video_failed)
logger.error('load', err)
}
}
// hook fetch response
const ff = fetch
unsafeWindow.fetch = (...args) => {
if (args[0] instanceof Request) {
return ff(...args).then(resp => {
if (resp.url.includes('player')) {
resp.clone().json().then(load)
}
return resp
})
}
return ff(...args)
}
// attach element
setInterval(() => {
const el =
$('#info-contents') ||
$('#watch-header') ||
$('.page-container:not([hidden]) ytm-item-section-renderer>lazy-list')
if (el && !el.contains(shadowHost)) {
el.appendChild(shadowHost)
}
}, 100)
// init
unsafeWindow.addEventListener('load', () => {
const firstResp = unsafeWindow?.ytplayer?.config?.args?.raw_player_response
if (firstResp) {
load(firstResp)
}
})
// listen to dark mode toggle
const $html = $('html')
new MutationObserver(() => {
app.dark = $html.getAttribute('dark') === 'true'
}).observe($html, { attributes: true })
app.dark = $html.getAttribute('dark') === 'true'
const css = `
.hide{
display: none;
}
.t-center{
text-align: center;
}
.d-flex{
display: flex;
}
.f-1{
flex: 1;
}
.fs-14px{
font-size: 14px;
}
.of-h{
overflow: hidden;
}
.box{
padding-top: .5em;
padding-bottom: .5em;
border-bottom: 1px solid var(--yt-border-color);
font-family: Arial;
}
.box-toggle{
margin: 3px;
user-select: none;
-moz-user-select: -moz-none;
}
.ytdl-link-btn{
display: block;
border: 1px solid !important;
border-radius: 3px;
text-decoration: none !important;
outline: 0;
text-align: center;
padding: 2px;
margin: 5px;
color: black;
}
a, .div-a{
text-decoration: none;
color: var(--yt-button-color, inherit);
}
a:hover, .div-a:hover{
color: var(--yt-spec-call-to-action, blue);
}
.box.dark{
color: var(--yt-endpoint-color, var(--yt-spec-text-primary));
}
.box.dark .ytdl-link-btn{
color: var(--yt-endpoint-color, var(--yt-spec-text-primary));
}
.box.dark .ytdl-link-btn:hover{
color: rgba(200, 200, 255, 0.8);
}
.box.dark .box-toggle:hover{
color: rgba(200, 200, 255, 0.8);
}
.c-pointer{
cursor: pointer;
}
.lh-20{
line-height: 20px;
}
`
shadow.appendChild($el('style', { textContent: css }))
const css2 = `
/* https://gf.zukizuki.org/zh-TW/scripts/369400-local-youtube-downloader/discussions/95744 */
#meta-contents,
#info-contents{
display: contents !important;
}
ytd-watch-metadata.style-scope {
display: none !important;
}
`
document.body.appendChild($el('style', { textContent: css2 }))
})()