Last active
January 19, 2025 15:10
-
-
Save Zxce3/dfc57fa38a226c89dbe6190c7187d53c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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