Skip to content

Instantly share code, notes, and snippets.

@Zxce3
Last active January 19, 2025 15:10
Show Gist options
  • Save Zxce3/dfc57fa38a226c89dbe6190c7187d53c to your computer and use it in GitHub Desktop.
Save Zxce3/dfc57fa38a226c89dbe6190c7187d53c to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Discord Media Downloader
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Adds checkboxes to media items and a download button to download selected media on Discord Web concurrently, handling images as downloads and videos by opening them in new tabs.
// @author Zxce3
// @match https://discord.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Create and inject CSS styles
const styles = `
.media-checkbox {
position: absolute !important;
top: 10px !important;
left: 10px !important;
z-index: 1000 !important;
width: 18px !important;
height: 18px !important;
border-radius: 4px !important;
background-color: #36393f !important;
border: 2px solid #7289da !important;
outline: none !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
}
.media-checkbox:checked {
background-color: #7289da !important;
border-color: #7289da !important;
}
.media-checkbox:hover {
border-color: #5b6eae !important;
}
.discord-button {
padding: 12px 24px !important;
background-color: #7289da !important;
color: #fff !important;
border: none !important;
border-radius: 5px !important;
cursor: pointer !important;
font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
font-size: 14px !important;
font-weight: 500 !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important;
transition: all 0.2s ease !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
}
.discord-button:hover {
background-color: #5b6eae !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
}
.discord-button:active {
transform: translateY(0) !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important;
}
.media-counter {
padding: 12px 20px !important;
background-color: #2f3136 !important;
color: #fff !important;
border-radius: 5px !important;
font-family: 'gg sans', 'Noto Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif !important;
font-size: 14px !important;
font-weight: 500 !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important;
z-index: 1000 !important;
}
.draggable-card {
position: fixed !important;
bottom: 20px !important;
right: 20px !important;
width: 200px !important;
height: auto !important;
max-height: 7em !important;
overflow-y: auto !important;
background-color: #2f3136 !important;
color: #fff !important;
border-radius: 5px !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) !important;
z-index: 1000 !important;
cursor: move !important;
padding: 10px !important;
}
.card-header {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
cursor: pointer !important;
}
.card-content {
display: none !important;
flex-direction: column !important;
gap: 10px !important;
}
.card-content.show {
display: flex !important;
}
.loading-indicator {
display: none !important;
margin-left: 8px !important;
border: 2px solid #f3f3f3 !important;
border-top: 2px solid #7289da !important;
border-radius: 50% !important;
width: 18px !important;
height: 18px !important;
animation: spin 0.8s linear infinite !important;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
const styleSheet = document.createElement("style");
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
// Function to dynamically identify all media elements
function getMediaElements() {
const mediaElements = [];
// Find all message containers
const messages = document.querySelectorAll('li[class*="messageListItem"]');
messages.forEach(message => {
// Get message timestamp
const timestamp = message.querySelector('time')?.dateTime;
if (timestamp && new Date(timestamp) < new Date('2025-01-18 22:11:59')) {
return;
}
// Handle grid layouts
const grids = message.querySelectorAll('[class*="oneByTwoGrid"], [class*="twoByTwoGrid"], [class*="threeByThreeGrid"]');
if (grids.length > 0) {
grids.forEach(grid => {
const gridItems = grid.querySelectorAll('[class*="mosaicItem"]');
gridItems.forEach(item => {
if (!item.querySelector('.media-checkbox')) {
mediaElements.push(item);
}
});
});
} else {
// Handle single media items
const mediaContainers = message.querySelectorAll('[class*="imageWrapper"], [class*="videoWrapper"], [class*="embedVideo"]');
mediaContainers.forEach(container => {
if (!container.querySelector('.media-checkbox')) {
mediaElements.push(container);
}
});
}
// Handle attachments
const attachments = message.querySelectorAll('[class*="attachment"]');
attachments.forEach(attachment => {
if (!attachment.querySelector('.media-checkbox')) {
mediaElements.push(attachment);
}
});
});
return mediaElements;
}
// Function to update the counter
function updateCounter() {
const counter = document.querySelector('.media-counter');
const selectedCount = document.querySelectorAll('.media-checkbox:checked').length;
const totalCount = document.querySelectorAll('.media-checkbox').length;
if (counter) {
counter.textContent = `Selected: ${selectedCount} / ${totalCount}`;
counter.style.display = totalCount > 0 ? 'block' : 'none';
}
}
// Function to add checkboxes to media items
function addCheckboxes() {
const mediaElements = getMediaElements();
mediaElements.forEach(mediaElement => {
if (!mediaElement.querySelector('.media-checkbox')) {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'media-checkbox';
if (getComputedStyle(mediaElement).position === 'static') {
mediaElement.style.position = 'relative';
}
checkbox.addEventListener('change', updateCounter);
mediaElement.appendChild(checkbox);
}
});
updateCounter();
}
// Function to get media URL from different types of containers
function getMediaUrl(mediaElement) {
// Check for direct media elements
const mediaTag = mediaElement.querySelector('img, video');
if (mediaTag) {
return mediaTag.src;
}
// Check for attachment links
const attachmentLink = mediaElement.querySelector('a[href*="/attachments/"]');
if (attachmentLink) {
return attachmentLink.href;
}
// Check for embed videos
const embedVideo = mediaElement.querySelector('[class*="embedVideo"]');
if (embedVideo) {
const videoUrl = embedVideo.getAttribute('src') ||
embedVideo.getAttribute('data-src') ||
embedVideo.querySelector('source')?.src;
if (videoUrl) return videoUrl;
}
// Check for background images
const style = window.getComputedStyle(mediaElement);
if (style.backgroundImage) {
const match = style.backgroundImage.match(/url\(['"]?(.*?)['"]?\)/);
if (match) return match[1];
}
return null;
}
// Function to determine media type
function getMediaType(url, element) {
if (!url) return null;
const videoExtensions = ['mp4', 'webm', 'mov'];
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const extension = url.split('.').pop().toLowerCase().split('?')[0];
if (videoExtensions.includes(extension) || element.querySelector('video')) {
return 'video';
} else if (imageExtensions.includes(extension) || element.querySelector('img')) {
return 'image';
}
return 'attachment';
}
// Function to generate filename
function generateFilename(url, type) {
const urlObj = new URL(url);
let filename = urlObj.pathname.split('/').pop();
filename = filename.split('?')[0];
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const extension = filename.split('.').pop();
const baseName = filename.substring(0, filename.length - extension.length - 1);
return `${baseName}_${timestamp}.${extension}`;
}
// Function to download an image
async function downloadImage(url, checkbox) {
try {
const response = await fetch(url);
const blob = await response.blob();
const filename = generateFilename(url, 'image');
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
a.click();
window.URL.revokeObjectURL(blobUrl);
console.log(`Downloaded image: ${filename}`);
checkbox.checked = false; // Uncheck the checkbox
return true;
} catch (error) {
console.error(`Failed to download image: ${url}`, error);
return false;
}
}
// Function to open video in a new tab
async function openVideo(url, checkbox) {
try {
window.open(url, '_blank');
console.log(`Opened video in new tab: ${url}`);
checkbox.checked = false; // Uncheck the checkbox
return true;
} catch (error) {
console.error(`Failed to open video: ${url}`, error);
return false;
}
}
// Function to handle media download
async function downloadMedia() {
const checkboxes = document.querySelectorAll('.media-checkbox:checked');
if (checkboxes.length === 0) {
alert('No media selected.');
return;
}
const mediaQueue = Array.from(checkboxes).map(checkbox => {
const mediaElement = checkbox.parentElement;
const url = getMediaUrl(mediaElement);
const mediaType = getMediaType(url, mediaElement);
return { url, mediaType, checkbox };
}).filter(media => media.url && media.mediaType);
const totalMedia = mediaQueue.length;
let completed = 0;
const downloadWorker = async () => {
while (mediaQueue.length > 0) {
const { url, mediaType, checkbox } = mediaQueue.shift();
if (mediaType === 'image') {
await downloadImage(url, checkbox);
} else if (mediaType === 'video') {
await openVideo(url, checkbox);
}
completed++;
console.log(`Downloaded: ${completed}/${totalMedia}`);
}
};
// Display loading indicator
const loadingIndicator = document.querySelector('.loading-indicator');
loadingIndicator.style.display = 'block';
const workers = [];
for (let i = 0; i < 5; i++) {
workers.push(downloadWorker());
}
await Promise.all(workers);
// Hide loading indicator
loadingIndicator.style.display = 'none';
alert(`Downloaded ${completed} out of ${totalMedia} media files.`);
}
// Function to make the card draggable
function makeDraggable(element) {
let isDragging = false;
let offsetX, offsetY;
element.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - element.getBoundingClientRect().left;
offsetY = e.clientY - element.getBoundingClientRect().top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
if (isDragging) {
element.style.left = `${e.clientX - offsetX}px`;
element.style.top = `${e.clientY - offsetY}px`;
}
}
function onMouseUp() {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
}
// Create the draggable card
const card = document.createElement('div');
card.className = 'draggable-card';
const cardHeader = document.createElement('div');
cardHeader.className = 'card-header';
cardHeader.innerHTML = '<span>Media Downloader</span><span>▼</span>';
const cardContent = document.createElement('div');
cardContent.className = 'card-content';
cardHeader.addEventListener('click', () => {
cardContent.classList.toggle('show');
});
card.appendChild(cardHeader);
card.appendChild(cardContent);
document.body.appendChild(card);
// Add download button to the card content
const downloadButton = document.createElement('button');
downloadButton.className = 'discord-button';
downloadButton.innerHTML = 'Download Media';
// Add loading indicator to the button
const loadingIndicator = document.createElement('div');
loadingIndicator.className = 'loading-indicator';
downloadButton.appendChild(loadingIndicator);
downloadButton.addEventListener('click', downloadMedia);
cardContent.appendChild(downloadButton);
// Add media counter to the card content
const mediaCounter = document.createElement('div');
mediaCounter.className = 'media-counter';
cardContent.appendChild(mediaCounter);
makeDraggable(card);
let lastExecution = 0;
const observer = new MutationObserver(() => {
const now = performance.now();
if (now - lastExecution > 100) {
lastExecution = now;
requestAnimationFrame(addCheckboxes);
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Initial call to add checkboxes
addCheckboxes();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment