您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键下载帖子内所有图片,并保存为ZIP文件。
- // ==UserScript==
- // @name SaveAsZip for Discord
- // @name:ja SaveAsZip for Discord
- // @name::zh-cn SaveAsZip for Discord
- // @name::zh-tw SaveAsZip for Discord
- // @description Download post images and save as a ZIP file.
- // @description:ja 投稿の画像をZIPファイルとして保存する。
- // @description:zh-cn 一键下载帖子内所有图片,并保存为ZIP文件。
- // @description:zh-tw 一鍵下載帖子内所有圖片,並保存為ZIP文件。
- // @version 0.13
- // @namespace none
- // @match https://discord.com/*
- // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js
- // @grant none
- // @license MIT
- // @run-at document-body
- // ==/UserScript==
- /* jshint esversion: 8 */
- const preset_zip_name = '{username}_{datetime_local:YYYYMMDD-hhmmss}_{channel_id}_{message_id}_images.zip';
- const token = getToken();
- const JSZip = window.JSZip;
- addStyle();
- addButton();
- function addButton() {
- let observer = new MutationObserver(() => findContainer());
- observer.observe(document.body, {childList: true, subtree: true});
- }
- function findContainer() {
- let containers = document.querySelectorAll('li div[class^="mediaAttachmentsContainer_"]:not(.zip-btn-added)');
- containers.forEach(container => addButtonTo(container));
- }
- function addButtonTo(container) {
- container.classList.add('zip-btn-added');
- let btn = document.createElement('span');
- btn.classList.add('saveaszip');
- if (isGroupStart(container)) btn.classList.add('group-start');
- btn.innerHTML = '<label class="down-btn"><span class="btn-text">ZIP</span></label><label class="down-speed">0KB/S</label>';
- btn.onclick = () => SaveAsZip(btn, container);
- container.appendChild(btn);
- }
- function isGroupStart(container) {
- let target_li = container.closest('li');
- while (true) {
- let is_group_start = target_li.querySelector(':scope > div[class*="groupStart"]');
- if (is_group_start) return true;
- target_li = target_li.previousElementSibling;
- if (!target_li) break;
- let has_media = target_li.querySelector('div[class^="mediaAttachmentsContainer_"]');
- if (has_media) break;
- }
- return false;
- }
- async function SaveAsZip(btn, container) {
- if (btn.classList.contains('down')) return;
- else btn.classList.add('down');
- let btn_text = btn.querySelector('.btn-text');
- let btn_speed = btn.querySelector('.down-speed');
- const status = text => (btn_text.innerText = text);
- const speeds = text => (btn_speed.innerHTML = text);
- let invalid_chars = {'\\': '\', '\/': '/', '\|': '|', '<': '<', '>': '>', ':': ':', '*': '*', '?': '?', '"': '"', '\u200b': '', '\u200c': '', '\u200d': '', '\u2060': '', '\ufeff': '', '🔞': ''};
- let datetime_pattern = preset_zip_name.match(/{datetime(-local)?:[^{}]+}/) ? preset_zip_name.match(/{datetime(?:-local)?:([^{}]+)}/)[1].replace(/[\\/|<>*?:"]/g, v => invalid_chars[v]) : 'YYYYMMDD-hhmmss';
- //get channel_id and message_id
- let anchor_li = container.closest('li');
- let anchor_li_is_flash = anchor_li.parentNode.classList.value.indexOf('backgroundFlash') >=0;
- let [channel_id, message_id] = anchor_li.id.split('-').slice(-2);
- //get datetime in first message
- let datetime_utc = formatDate(anchor_li.querySelector('time').getAttribute('datetime'), datetime_pattern);
- let datetime_local = formatDate(anchor_li.querySelector('time').getAttribute('datetime'), datetime_pattern, true);
- //get messages group
- let messages_group = [message_id];
- let anchor = anchor_li_is_flash ? anchor_li.parentNode : anchor_li;
- while (true) {
- let current = anchor.nextElementSibling;
- let current_is_flash = current.tagName == 'DIV' && current.classList.value.indexOf('backgroundFlash') >=0;
- if (current_is_flash) current = current.firstChild;
- if (current.tagName == 'LI' && !current.querySelector('h3')) {
- messages_group.push(current.id.split('-').pop());
- anchor = current_is_flash ? current.parentNode : current;
- } else break;
- }
- //get post json
- let url = `https://discord.com/api/v9/channels/${channel_id}/messages?limit=50&around=${message_id}`;
- let json = await (await fetch(url, {headers: {'Authorization': token}})).json();
- if (!Array.isArray(json)) return console.error('error: get json failed');
- let message = json.find(message => message.id == message_id);
- let author_id = message.author.id;
- let author_name = message.author.global_name || message.author.username;
- //extract post info
- let info = {
- channel_id: channel_id,
- message_id: message_id,
- datetime: datetime_utc,
- datetime_local: datetime_local,
- user_id: author_id,
- username: author_name
- };
- //create zip and set filename
- let zip = new JSZip();
- let zip_name = preset_zip_name.replace(/{([^{}:]+)(:[^{}]+)?}/g, (match, name) => info[name]);
- //find images
- let images = [];
- let images_size = 0, images_size_2;
- messages_group.forEach(message_id => {
- let message = json.find(message => message.id == message_id);
- if (message && message.author.id == author_id && message.attachments.length) {
- message.attachments.forEach(file => {
- if (file.content_type.indexOf('image') == 0) {
- file.message_id = message_id;
- file.timestamp = formatDate(message.timestamp, datetime_pattern);
- file.timestamp_local = formatDate(message.timestamp, datetime_pattern, true);
- images.push(file);
- images_size += file.size;
- }
- });
- }
- });
- images_size_2 = images_size < 1024000 ? Math.round(images_size / 1024) + 'KB' : (images_size / 1048576).toFixed(2) + 'MB';
- //show download speed if images size over 10MB
- let received = 0, received_2, traffic = 0, traffic_buffer = [], traffic_update;
- if (images_size >= 10485760) {
- btn.classList.add('speed');
- traffic_update = setInterval(() => {
- traffic_buffer.push(traffic);
- let speed = traffic_buffer.reduce((a, b) => a + b, 0) / traffic_buffer.length / 1024;
- received_2 = received < 1024000 ? Math.round(received / 1024) + 'KB' : (received / 1048576).toFixed(2) + 'MB';
- speeds(`${received_2} of ${images_size_2} | ${speed < 1000 ? Math.round(speed) + ' KB/s' : (speed / 1024).toFixed(2) + ' MB/s'}`);
- if (traffic_buffer.length >= 5) traffic_buffer.shift();
- traffic = 0;
- }, 1000);
- }
- //download image and add to zip
- for (let i = 0; i < images.length; i++) {
- status(`${i + 1}/${images.length}`);
- let image = images[i];
- let response = await fetch(image.url);
- let content_type = response.headers.get('content-type');
- const reader = response.body.getReader();
- let chunks = [];
- while (true) {
- const {done, value} = await reader.read();
- if (value) {
- chunks.push(value);
- received += value.length;
- traffic += value.length;
- }
- if (done) break;
- }
- let image_blob = new Blob(chunks, {type: content_type});
- zip.file(`${image.message_id}_${image.id}_${image.filename}`, image_blob);
- }
- //download completed
- speeds('');
- btn.classList.remove('speed');
- clearInterval(traffic_update);
- //save
- status('Save');
- let zip_blob = await zip.generateAsync({type: 'blob'});
- let zip_url = URL.createObjectURL(zip_blob);
- let link = document.createElement('a');
- link.href = zip_url;
- link.download = zip_name;
- link.dispatchEvent(new MouseEvent('click'));
- setTimeout(() => URL.revokeObjectURL(zip_url), 100);
- //done
- btn.classList.remove('down');
- btn.classList.add('done');
- status('Done');
- }
- function getToken() {
- const iframe = document.createElement('iframe');
- iframe.style.display = 'none';
- const token = JSON.parse(document.body.appendChild(iframe).contentWindow.localStorage.token);
- iframe.remove();
- return token;
- }
- function formatDate(i, o, tz) {
- let d = new Date(i);
- if (tz) d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
- let v = {YYYY: d.getUTCFullYear(), YY: d.getUTCFullYear() % 100, MM: d.getUTCMonth() + 1, DD: d.getUTCDate(), hh: d.getUTCHours(), mm: d.getUTCMinutes(), ss: d.getUTCSeconds()};
- return o.replace(/(YYYY|MM|DD|hh|mm|ss)/g, n => ('0' + v[n]).substr(-n.length));
- }
- function addStyle() {
- let css = `
- .saveaszip {position: absolute; color: white; padding: 6px 4px; z-index: 99; left: 0; top: 0;}
- .saveaszip {display: flex; gap: 4px; align-items: center;}
- .saveaszip:not(.group-start) {display: none;}
- .saveaszip label {background: #0008; border: 1px solid #8888; border-radius: 6px; padding: 4px 12px;}
- .saveaszip:hover label.down-btn {background: #000a; border-color: #fff3;}
- .saveaszip.down label.down-btn {background: #000a; border-color: #fff3;}
- .saveaszip.done label.down-btn {background: #060a; border-color: #fff3;}
- .saveaszip label.down-speed {font-family: monospace; font-size: 14px; padding: 3px 6px;}
- .saveaszip:not(.speed) label.down-speed {display: none;}
- /* progress bar animation */
- .saveaszip.down .down-speed {background-image: linear-gradient(-45deg, #fff2 0%, #fff2 25%, #0000 25%, #0000 50%, #fff2 50%, #fff2 75%, #0000 75%, #0000 100%); background-size: 32px 32px; animation: progress 2s linear infinite;}
- @keyframes progress {0% {background-position:0 0} 100% {background-position:32px 32px}}
- `;
- document.head.insertAdjacentHTML('beforeend', `<style>${css}</style>`);
- }