// ==UserScript==
// @name Twitter/X Media Batch Downloader
// @description Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.
// @icon https://raw.githubusercontent.com/afkarxyz/Twitter-X-Media-Batch-Downloader/refs/heads/main/Archived/icon.svg
// @version 2.6
// @author afkarxyz
// @namespace https://github.com/afkarxyz/misc-scripts/
// @supportURL https://github.com/afkarxyz/misc-scripts/issues
// @license MIT
// @match https://twitter.com/*
// @match https://x.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_download
// @connect gallerydl.vercel.app
// @connect pbs.twimg.com
// @connect video.twimg.com
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// ==/UserScript==
(function() {
'use strict';
const defaultSettings = {
authToken: '',
batchEnabled: true,
batchSize: 100,
timelineType: 'media',
mediaType: 'all',
concurrentDownloads: 25,
cacheDuration: 360
};
function getSettings() {
return {
authToken: GM_getValue('authToken', defaultSettings.authToken),
batchEnabled: GM_getValue('batchEnabled', defaultSettings.batchEnabled),
batchSize: GM_getValue('batchSize', defaultSettings.batchSize),
timelineType: GM_getValue('timelineType', defaultSettings.timelineType),
mediaType: GM_getValue('mediaType', defaultSettings.mediaType),
concurrentDownloads: GM_getValue('concurrentDownloads', defaultSettings.concurrentDownloads),
cacheDuration: GM_getValue('cacheDuration', defaultSettings.cacheDuration)
};
}
function saveSettings(settings) {
GM_setValue('authToken', settings.authToken);
GM_setValue('batchEnabled', settings.batchEnabled);
GM_setValue('batchSize', settings.batchSize);
GM_setValue('timelineType', settings.timelineType);
GM_setValue('mediaType', settings.mediaType);
GM_setValue('concurrentDownloads', settings.concurrentDownloads);
GM_setValue('cacheDuration', settings.cacheDuration);
}
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const cacheManager = {
set: function(key, data) {
const settings = getSettings();
const cacheItem = {
data: data,
timestamp: Date.now(),
expiry: Date.now() + (settings.cacheDuration * 60 * 1000)
};
localStorage.setItem(`twitter_dl_${key}`, JSON.stringify(cacheItem));
},
get: function(key) {
const cacheItem = localStorage.getItem(`twitter_dl_${key}`);
if (!cacheItem) return null;
try {
const parsed = JSON.parse(cacheItem);
if (Date.now() > parsed.expiry) {
localStorage.removeItem(`twitter_dl_${key}`);
return null;
}
return parsed.data;
} catch (e) {
localStorage.removeItem(`twitter_dl_${key}`);
return null;
}
},
clear: function() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('twitter_dl_')) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => localStorage.removeItem(key));
}
};
function createDownloadIcon() {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("viewBox", "0 0 512 512");
svg.setAttribute("width", "18");
svg.setAttribute("height", "18");
svg.style.verticalAlign = "middle";
svg.style.cursor = "pointer";
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
style.textContent = ".fa-secondary{opacity:.4}";
defs.appendChild(style);
svg.appendChild(defs);
const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
secondaryPath.setAttribute("class", "fa-secondary");
secondaryPath.setAttribute("fill", "currentColor");
secondaryPath.setAttribute(
"d",
"M0 256C0 397.4 114.6 512 256 512s256-114.6 256-256c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 106-86 192-192 192S64 362 64 256c0-17.7-14.3-32-32-32s-32 14.3-32 32z"
);
svg.appendChild(secondaryPath);
const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
primaryPath.setAttribute("class", "fa-primary");
primaryPath.setAttribute("fill", "currentColor");
primaryPath.setAttribute(
"d",
"M390.6 185.4c12.5 12.5 12.5 32.8 0 45.3l-112 112c-12.5 12.5-32.8 12.5-45.3 0l-112-112c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L224 242.7 224 32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 210.7 57.4-57.4c12.5-12.5 32.8-12.5 45.3 0z"
);
svg.appendChild(primaryPath);
return svg;
}
function createGithubIcon() {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", "24");
svg.setAttribute("height", "24");
svg.style.verticalAlign = "middle";
svg.style.marginRight = "8px";
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("fill", "currentColor");
path.setAttribute(
"d",
"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
);
svg.appendChild(path);
return svg;
}
function createConfirmDialog(message, onConfirm, onCancel) {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background-color: #15202b;
color: white;
border-radius: 16px;
width: 300px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
`;
const header = document.createElement('div');
header.style.cssText = `
padding: 16px;
border-bottom: 1px solid #334155;
font-weight: bold;
font-size: 16px;
text-align: center;
`;
header.textContent = 'Confirmation';
const content = document.createElement('div');
content.style.cssText = `
padding: 16px;
text-align: center;
`;
content.textContent = message;
const buttons = document.createElement('div');
buttons.style.cssText = `
display: flex;
padding: 16px;
border-top: 1px solid #334155;
`;
const cancelButton = document.createElement('button');
cancelButton.style.cssText = `
flex: 1;
background-color: #64748b;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
margin-right: 8px;
font-weight: bold;
cursor: pointer;
text-align: center;
transition: background-color 0.2s;
`;
cancelButton.textContent = 'Cancel';
cancelButton.addEventListener('mouseenter', () => {
cancelButton.style.backgroundColor = '#475569';
});
cancelButton.addEventListener('mouseleave', () => {
cancelButton.style.backgroundColor = '#64748b';
});
cancelButton.onclick = () => {
document.body.removeChild(overlay);
if (onCancel) onCancel();
};
const confirmButton = document.createElement('button');
confirmButton.style.cssText = `
flex: 1;
background-color: #ef4444;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
text-align: center;
transition: background-color 0.2s;
`;
confirmButton.textContent = 'Confirm';
confirmButton.addEventListener('mouseenter', () => {
confirmButton.style.backgroundColor = '#dc2626';
});
confirmButton.addEventListener('mouseleave', () => {
confirmButton.style.backgroundColor = '#ef4444';
});
confirmButton.onclick = () => {
document.body.removeChild(overlay);
if (onConfirm) onConfirm();
};
buttons.appendChild(cancelButton);
buttons.appendChild(confirmButton);
dialog.appendChild(header);
dialog.appendChild(content);
dialog.appendChild(buttons);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
}
function extractUsername() {
const pathParts = window.location.pathname.split('/').filter(part => part);
if (pathParts.length > 0) {
return pathParts[0];
}
return null;
}
function formatDate(dateString) {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
}
function getCurrentTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
}
function fetchData(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'json',
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
resolve(response.response);
} else {
reject(new Error(`Request failed with status ${response.status}`));
}
},
onerror: function() {
reject(new Error('Network error'));
}
});
});
}
function fetchBinary(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
resolve(response.response);
} else {
reject(new Error(`Request failed with status ${response.status}`));
}
},
onerror: function() {
reject(new Error('Network error'));
}
});
});
}
function getMediaTypeLabel(mediaType) {
switch(mediaType) {
case 'image': return 'Image';
case 'video': return 'Video';
case 'gif': return 'GIF';
default: return 'Media';
}
}
function createModal(username) {
const existingModal = document.getElementById('media-downloader-modal');
if (existingModal) {
existingModal.remove();
}
const settings = getSettings();
const modal = document.createElement('div');
modal.id = 'media-downloader-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background-color: #15202b;
color: white;
border-radius: 16px;
width: 500px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
`;
const header = document.createElement('div');
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #334155;
`;
const title = document.createElement('h2');
title.textContent = `Download Media: ${username}`;
title.style.cssText = `
margin: 0;
font-size: 18px;
font-weight: bold;
`;
const closeButton = document.createElement('button');
closeButton.innerHTML = '×';
closeButton.style.cssText = `
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
line-height: 1;
transition: color 0.2s;
`;
closeButton.addEventListener('mouseenter', () => {
closeButton.style.color = '#0ea5e9';
});
closeButton.addEventListener('mouseleave', () => {
closeButton.style.color = 'white';
});
closeButton.onclick = () => modal.remove();
header.appendChild(title);
header.appendChild(closeButton);
const tabs = document.createElement('div');
tabs.style.cssText = `
display: flex;
border-bottom: 1px solid #334155;
`;
const mainTab = document.createElement('div');
mainTab.textContent = 'Main';
mainTab.className = 'active-tab';
mainTab.style.cssText = `
padding: 12px 16px;
cursor: pointer;
flex: 1;
text-align: center;
border-bottom: 2px solid #0ea5e9;
`;
const settingsTab = document.createElement('div');
settingsTab.textContent = 'Settings';
settingsTab.style.cssText = `
padding: 12px 16px;
cursor: pointer;
flex: 1;
text-align: center;
color: #8899a6;
`;
tabs.appendChild(mainTab);
tabs.appendChild(settingsTab);
const mainContent = document.createElement('div');
mainContent.style.cssText = `
padding: 16px;
`;
const settingsContent = document.createElement('div');
settingsContent.style.cssText = `
padding: 16px;
display: none;
`;
const fetchButton = document.createElement('button');
fetchButton.textContent = 'Fetch Media';
fetchButton.style.cssText = `
background-color: #0ea5e9;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
margin-bottom: 16px;
width: 100%;
text-align: center;
transition: background-color 0.2s;
`;
fetchButton.addEventListener('mouseenter', () => {
fetchButton.style.backgroundColor = '#0284c7';
});
fetchButton.addEventListener('mouseleave', () => {
fetchButton.style.backgroundColor = '#0ea5e9';
});
const infoContainer = document.createElement('div');
infoContainer.style.cssText = `
background-color: #192734;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
display: none;
`;
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 8px;
margin-bottom: 16px;
`;
const downloadCurrentButton = document.createElement('button');
downloadCurrentButton.textContent = 'Download Current Batch';
downloadCurrentButton.style.cssText = `
background-color: #0ea5e9;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
flex: 1;
display: none;
text-align: center;
transition: background-color 0.2s;
`;
downloadCurrentButton.addEventListener('mouseenter', () => {
downloadCurrentButton.style.backgroundColor = '#0284c7';
});
downloadCurrentButton.addEventListener('mouseleave', () => {
downloadCurrentButton.style.backgroundColor = '#0ea5e9';
});
const downloadAllButton = document.createElement('button');
downloadAllButton.textContent = 'Download All Batches';
downloadAllButton.style.cssText = `
background-color: #22c55e;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
flex: 1;
display: none;
text-align: center;
transition: background-color 0.2s;
`;
downloadAllButton.addEventListener('mouseenter', () => {
downloadAllButton.style.backgroundColor = '#16a34a';
});
downloadAllButton.addEventListener('mouseleave', () => {
downloadAllButton.style.backgroundColor = '#22c55e';
});
buttonContainer.appendChild(downloadCurrentButton);
buttonContainer.appendChild(downloadAllButton);
const nextBatchButton = document.createElement('button');
nextBatchButton.textContent = 'Next Batch';
nextBatchButton.style.cssText = `
background-color: #0ea5e9;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
margin-bottom: 16px;
width: 100%;
display: none;
text-align: center;
transition: background-color 0.2s;
`;
nextBatchButton.addEventListener('mouseenter', () => {
nextBatchButton.style.backgroundColor = '#0284c7';
});
nextBatchButton.addEventListener('mouseleave', () => {
nextBatchButton.style.backgroundColor = '#0ea5e9';
});
const autoBatchButton = document.createElement('button');
autoBatchButton.textContent = 'Auto Batch';
autoBatchButton.style.cssText = `
background-color: #6366f1;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
margin-bottom: 16px;
width: 100%;
display: none;
text-align: center;
transition: background-color 0.2s;
`;
autoBatchButton.addEventListener('mouseenter', () => {
autoBatchButton.style.backgroundColor = '#4f46e5';
});
autoBatchButton.addEventListener('mouseleave', () => {
autoBatchButton.style.backgroundColor = '#6366f1';
});
const stopBatchButton = document.createElement('button');
stopBatchButton.textContent = 'Stop Batch';
stopBatchButton.style.cssText = `
background-color: #ef4444;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
margin-bottom: 16px;
width: 100%;
display: none;
text-align: center;
transition: background-color 0.2s;
`;
stopBatchButton.addEventListener('mouseenter', () => {
stopBatchButton.style.backgroundColor = '#dc2626';
});
stopBatchButton.addEventListener('mouseleave', () => {
stopBatchButton.style.backgroundColor = '#ef4444';
});
const progressContainer = document.createElement('div');
progressContainer.style.cssText = `
margin-top: 16px;
display: none;
`;
const progressText = document.createElement('div');
progressText.style.cssText = `
margin-bottom: 8px;
font-size: 14px;
text-align: center;
`;
progressText.textContent = 'Downloading...';
const progressBar = document.createElement('div');
progressBar.style.cssText = `
width: 100%;
height: 8px;
background-color: #192734;
border-radius: 4px;
overflow: hidden;
`;
const progressFill = document.createElement('div');
progressFill.style.cssText = `
height: 100%;
width: 0%;
background-color: #0ea5e9;
transition: width 0.3s;
`;
progressBar.appendChild(progressFill);
progressContainer.appendChild(progressText);
progressContainer.appendChild(progressBar);
mainContent.appendChild(fetchButton);
mainContent.appendChild(infoContainer);
mainContent.appendChild(buttonContainer);
mainContent.appendChild(nextBatchButton);
mainContent.appendChild(autoBatchButton);
mainContent.appendChild(stopBatchButton);
mainContent.appendChild(progressContainer);
const settingsForm = document.createElement('div');
settingsForm.style.cssText = `
display: flex;
flex-direction: column;
gap: 16px;
`;
const tokenGroup = document.createElement('div');
tokenGroup.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
const tokenLabel = document.createElement('label');
tokenLabel.textContent = 'Auth Token:';
tokenLabel.style.cssText = `
font-size: 14px;
font-weight: bold;
`;
const tokenInputContainer = document.createElement('div');
tokenInputContainer.style.cssText = `
position: relative;
display: flex;
align-items: center;
`;
const tokenInput = document.createElement('input');
tokenInput.type = 'text';
tokenInput.value = settings.authToken;
tokenInput.style.cssText = `
background-color: #192734;
border: 1px solid #334155;
border-radius: 4px;
padding: 8px 12px;
color: white;
width: 100%;
box-sizing: border-box;
`;
tokenInput.addEventListener('input', () => {
const newSettings = getSettings();
newSettings.authToken = tokenInput.value;
saveSettings(newSettings);
tokenClearButton.style.display = tokenInput.value ? 'block' : 'none';
});
const tokenClearButton = document.createElement('button');
tokenClearButton.innerHTML = '×';
tokenClearButton.style.cssText = `
position: absolute;
right: 8px;
background: none;
border: none;
color: #8899a6;
font-size: 18px;
cursor: pointer;
padding: 0;
display: ${settings.authToken ? 'block' : 'none'};
`;
tokenClearButton.addEventListener('click', () => {
tokenInput.value = '';
const newSettings = getSettings();
newSettings.authToken = '';
saveSettings(newSettings);
tokenClearButton.style.display = 'none';
});
tokenInputContainer.appendChild(tokenInput);
tokenInputContainer.appendChild(tokenClearButton);
tokenGroup.appendChild(tokenLabel);
tokenGroup.appendChild(tokenInputContainer);
const batchGroup = document.createElement('div');
batchGroup.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
`;
const batchLabel = document.createElement('label');
batchLabel.textContent = 'Batch:';
batchLabel.style.cssText = `
font-size: 14px;
font-weight: bold;
flex: 1;
`;
const batchToggle = document.createElement('div');
batchToggle.style.cssText = `
position: relative;
width: 50px;
height: 24px;
background-color: ${settings.batchEnabled ? '#0ea5e9' : '#334155'};
border-radius: 12px;
cursor: pointer;
transition: background-color 0.3s;
`;
const batchToggleHandle = document.createElement('div');
batchToggleHandle.style.cssText = `
position: absolute;
top: 2px;
left: ${settings.batchEnabled ? '28px' : '2px'};
width: 20px;
height: 20px;
background-color: white;
border-radius: 50%;
transition: left 0.3s;
`;
batchToggle.appendChild(batchToggleHandle);
batchToggle.addEventListener('click', () => {
const newSettings = getSettings();
newSettings.batchEnabled = !newSettings.batchEnabled;
saveSettings(newSettings);
batchToggle.style.backgroundColor = newSettings.batchEnabled ? '#0ea5e9' : '#334155';
batchToggleHandle.style.left = newSettings.batchEnabled ? '28px' : '2px';
batchSizeGroup.style.display = newSettings.batchEnabled ? 'flex' : 'none';
});
batchGroup.appendChild(batchLabel);
batchGroup.appendChild(batchToggle);
const batchSizeGroup = document.createElement('div');
batchSizeGroup.style.cssText = `
display: ${settings.batchEnabled ? 'flex' : 'none'};
flex-direction: column;
gap: 8px;
`;
const batchSizeLabel = document.createElement('label');
batchSizeLabel.textContent = 'Batch Size:';
batchSizeLabel.style.cssText = `
font-size: 14px;
font-weight: bold;
`;
const batchSizeSelect = document.createElement('select');
batchSizeSelect.style.cssText = `
background-color: #192734;
border: 1px solid #334155;
border-radius: 4px;
padding: 8px 12px;
color: white;
width: 100%;
box-sizing: border-box;
`;
const batchSizes = [50, 100, 150, 200];
batchSizes.forEach(size => {
const option = document.createElement('option');
option.value = size;
option.textContent = size;
option.selected = size === settings.batchSize;
batchSizeSelect.appendChild(option);
});
batchSizeSelect.addEventListener('change', () => {
const newSettings = getSettings();
newSettings.batchSize = parseInt(batchSizeSelect.value);
saveSettings(newSettings);
});
batchSizeGroup.appendChild(batchSizeLabel);
batchSizeGroup.appendChild(batchSizeSelect);
const timelineTypeGroup = document.createElement('div');
timelineTypeGroup.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
const timelineTypeLabel = document.createElement('label');
timelineTypeLabel.textContent = 'Timeline Type:';
timelineTypeLabel.style.cssText = `
font-size: 14px;
font-weight: bold;
`;
const timelineTypeSelect = document.createElement('select');
timelineTypeSelect.style.cssText = `
background-color: #192734;
border: 1px solid #334155;
border-radius: 4px;
padding: 8px 12px;
color: white;
width: 100%;
box-sizing: border-box;
`;
const timelineTypes = [
{ value: 'media', label: 'Media' },
{ value: 'timeline', label: 'Post' },
{ value: 'tweets', label: 'Tweets' },
{ value: 'with_replies', label: 'Replies' }
];
timelineTypes.forEach(type => {
const option = document.createElement('option');
option.value = type.value;
option.textContent = type.label;
option.selected = type.value === settings.timelineType;
timelineTypeSelect.appendChild(option);
});
timelineTypeSelect.addEventListener('change', () => {
const newSettings = getSettings();
newSettings.timelineType = timelineTypeSelect.value;
saveSettings(newSettings);
});
timelineTypeGroup.appendChild(timelineTypeLabel);
timelineTypeGroup.appendChild(timelineTypeSelect);
const mediaTypeGroup = document.createElement('div');
mediaTypeGroup.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
const mediaTypeLabel = document.createElement('label');
mediaTypeLabel.textContent = 'Media Type:';
mediaTypeLabel.style.cssText = `
font-size: 14px;
font-weight: bold;
`;
const mediaTypeSelect = document.createElement('select');
mediaTypeSelect.style.cssText = `
background-color: #192734;
border: 1px solid #334155;
border-radius: 4px;
padding: 8px 12px;
color: white;
width: 100%;
box-sizing: border-box;
`;
const mediaTypes = [
{ value: 'all', label: 'All' },
{ value: 'image', label: 'Image' },
{ value: 'video', label: 'Video' },
{ value: 'gif', label: 'GIF' }
];
mediaTypes.forEach(type => {
const option = document.createElement('option');
option.value = type.value;
option.textContent = type.label;
option.selected = type.value === settings.mediaType;
mediaTypeSelect.appendChild(option);
});
mediaTypeSelect.addEventListener('change', () => {
const newSettings = getSettings();
newSettings.mediaType = mediaTypeSelect.value;
saveSettings(newSettings);
});
mediaTypeGroup.appendChild(mediaTypeLabel);
mediaTypeGroup.appendChild(mediaTypeSelect);
const concurrentGroup = document.createElement('div');
concurrentGroup.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
const concurrentLabel = document.createElement('label');
concurrentLabel.textContent = 'Batch Download Items:';
concurrentLabel.style.cssText = `
font-size: 14px;
font-weight: bold;
`;
const concurrentSelect = document.createElement('select');
concurrentSelect.style.cssText = `
background-color: #192734;
border: 1px solid #334155;
border-radius: 4px;
padding: 8px 12px;
color: white;
width: 100%;
box-sizing: border-box;
`;
const concurrentSizes = [5, 10, 20, 25, 50];
concurrentSizes.forEach(size => {
const option = document.createElement('option');
option.value = size;
option.textContent = size;
option.selected = size === settings.concurrentDownloads;
concurrentSelect.appendChild(option);
});
concurrentSelect.addEventListener('change', () => {
const newSettings = getSettings();
newSettings.concurrentDownloads = parseInt(concurrentSelect.value);
saveSettings(newSettings);
});
concurrentGroup.appendChild(concurrentLabel);
concurrentGroup.appendChild(concurrentSelect);
const cacheDurationGroup = document.createElement('div');
cacheDurationGroup.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
const cacheDurationLabel = document.createElement('label');
cacheDurationLabel.textContent = 'Cache Duration (Hour):';
cacheDurationLabel.style.cssText = `
font-size: 14px;
font-weight: bold;
`;
const cacheDurationSelect = document.createElement('select');
cacheDurationSelect.style.cssText = `
background-color: #192734;
border: 1px solid #334155;
border-radius: 4px;
padding: 8px 12px;
color: white;
width: 100%;
box-sizing: border-box;
`;
for (let i = 1; i <= 24; i++) {
const option = document.createElement('option');
option.value = i * 60;
option.textContent = `${i} Hour${i > 1 ? 's' : ''}`;
option.selected = i === 6 || (settings.cacheDuration === i * 60);
cacheDurationSelect.appendChild(option);
}
cacheDurationSelect.addEventListener('change', () => {
const newSettings = getSettings();
newSettings.cacheDuration = parseInt(cacheDurationSelect.value);
saveSettings(newSettings);
});
cacheDurationGroup.appendChild(cacheDurationLabel);
cacheDurationGroup.appendChild(cacheDurationSelect);
const clearCacheButton = document.createElement('button');
clearCacheButton.textContent = 'Clear Cache';
clearCacheButton.style.cssText = `
background-color: #ef4444;
color: white;
border: none;
border-radius: 9999px;
padding: 8px 16px;
font-weight: bold;
cursor: pointer;
margin-top: 16px;
width: 100%;
text-align: center;
transition: background-color 0.2s;
`;
clearCacheButton.addEventListener('mouseenter', () => {
clearCacheButton.style.backgroundColor = '#dc2626';
});
clearCacheButton.addEventListener('mouseleave', () => {
clearCacheButton.style.backgroundColor = '#ef4444';
});
clearCacheButton.addEventListener('click', () => {
createConfirmDialog('Are you sure you want to clear the cache?', () => {
cacheManager.clear();
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #0ea5e9;
color: white;
padding: 12px 24px;
border-radius: 9999px;
font-weight: bold;
z-index: 10002;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
text-align: center;
`;
notification.textContent = 'Cache cleared successfully';
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 3000);
});
});
const githubLink = document.createElement('a');
githubLink.href = 'https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader';
githubLink.target = '_blank';
githubLink.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
color: #8899a6;
text-decoration: none;
margin-top: 16px;
padding: 8px;
border-radius: 8px;
transition: background-color 0.2s, color 0.2s;
`;
githubLink.innerHTML = createGithubIcon().outerHTML + 'GitHub Repository';
githubLink.addEventListener('mouseenter', () => {
githubLink.style.backgroundColor = '#192734';
githubLink.style.color = '#0ea5e9';
});
githubLink.addEventListener('mouseleave', () => {
githubLink.style.backgroundColor = 'transparent';
githubLink.style.color = '#8899a6';
});
settingsForm.appendChild(tokenGroup);
settingsForm.appendChild(batchGroup);
settingsForm.appendChild(batchSizeGroup);
settingsForm.appendChild(timelineTypeGroup);
settingsForm.appendChild(mediaTypeGroup);
settingsForm.appendChild(concurrentGroup);
settingsForm.appendChild(cacheDurationGroup);
settingsForm.appendChild(clearCacheButton);
settingsForm.appendChild(githubLink);
settingsContent.appendChild(settingsForm);
mainTab.addEventListener('click', () => {
mainTab.style.borderBottom = '2px solid #0ea5e9';
mainTab.style.color = 'white';
settingsTab.style.borderBottom = 'none';
settingsTab.style.color = '#8899a6';
mainContent.style.display = 'block';
settingsContent.style.display = 'none';
});
settingsTab.addEventListener('click', () => {
settingsTab.style.borderBottom = '2px solid #0ea5e9';
settingsTab.style.color = 'white';
mainTab.style.borderBottom = 'none';
mainTab.style.color = '#8899a6';
settingsContent.style.display = 'block';
mainContent.style.display = 'none';
});
modalContent.appendChild(header);
modalContent.appendChild(tabs);
modalContent.appendChild(mainContent);
modalContent.appendChild(settingsContent);
modal.appendChild(modalContent);
let mediaData = {
username: username,
currentPage: 0,
mediaItems: [],
allMediaItems: [],
hasMore: false,
downloading: false,
totalDownloaded: 0,
totalToDownload: 0,
totalItems: 0,
autoBatchRunning: false
};
fetchButton.addEventListener('click', async () => {
const settings = getSettings();
if (!settings.authToken) {
alert('Please enter your auth token in the Settings tab');
settingsTab.click();
return;
}
infoContainer.style.display = 'none';
buttonContainer.style.display = 'none';
downloadCurrentButton.style.display = 'none';
downloadAllButton.style.display = 'none';
nextBatchButton.style.display = 'none';
autoBatchButton.style.display = 'none';
stopBatchButton.style.display = 'none';
progressContainer.style.display = 'none';
fetchButton.disabled = true;
fetchButton.textContent = 'Fetching...';
try {
const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}`;
let data = cacheManager.get(cacheKey);
if (!data) {
let url;
if (settings.batchEnabled) {
url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}`;
} else {
url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}`;
}
data = await fetchData(url);
cacheManager.set(cacheKey, data);
}
if (data.timeline && data.timeline.length > 0) {
mediaData.mediaItems = data.timeline;
mediaData.hasMore = data.metadata.has_more;
mediaData.totalItems = data.total_urls;
if (mediaData.currentPage === 0) {
mediaData.allMediaItems = [...data.timeline];
} else {
mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline];
}
const mediaTypeLabel = getMediaTypeLabel(settings.mediaType);
if (settings.batchEnabled) {
infoContainer.innerHTML = `
<div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
<div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
<div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
<div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
`;
} else {
const currentPart = Math.floor(mediaData.allMediaItems.length / 500) + 1;
infoContainer.innerHTML = `
<div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
<div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
<div style="margin-top: 8px;"><strong>Part:</strong> ${currentPart}</div>
<div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
`;
}
infoContainer.style.display = 'block';
buttonContainer.style.display = 'flex';
downloadCurrentButton.style.display = 'block';
downloadAllButton.style.display = 'block';
if (settings.batchEnabled && mediaData.hasMore) {
nextBatchButton.style.display = 'block';
autoBatchButton.style.display = 'block';
}
downloadCurrentButton.onclick = () => downloadMedia(false);
downloadAllButton.onclick = () => downloadMedia(true);
fetchButton.disabled = false;
fetchButton.textContent = 'Fetch Media';
} else {
infoContainer.innerHTML = '<div style="color: #ef4444;">No media found or invalid token</div>';
infoContainer.style.display = 'block';
fetchButton.disabled = false;
fetchButton.textContent = 'Fetch Media';
}
} catch (error) {
console.error('Error fetching media:', error);
infoContainer.innerHTML = `<div style="color: #ef4444;">Error: ${error.message}</div>`;
infoContainer.style.display = 'block';
fetchButton.disabled = false;
fetchButton.textContent = 'Fetch Media';
}
});
nextBatchButton.addEventListener('click', () => {
mediaData.currentPage++;
fetchButton.click();
});
autoBatchButton.addEventListener('click', () => {
if (mediaData.autoBatchRunning) {
return;
}
mediaData.autoBatchRunning = true;
autoBatchButton.style.display = 'none';
stopBatchButton.style.display = 'block';
nextBatchButton.style.display = 'none';
startAutoBatch();
});
stopBatchButton.addEventListener('click', () => {
createConfirmDialog('Stop auto batch download?', () => {
mediaData.autoBatchRunning = false;
stopBatchButton.style.display = 'none';
autoBatchButton.style.display = 'block';
if (mediaData.hasMore) {
nextBatchButton.style.display = 'block';
}
});
});
async function startAutoBatch() {
while (mediaData.hasMore && mediaData.autoBatchRunning) {
mediaData.currentPage++;
downloadCurrentButton.disabled = true;
downloadAllButton.disabled = true;
await new Promise(resolve => {
const settings = getSettings();
const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}`;
let data = cacheManager.get(cacheKey);
if (data) {
processNextBatch(data);
resolve();
} else {
let url;
if (settings.batchEnabled) {
url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}`;
} else {
url = `https://gallerydl.vercel.app/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}`;
}
fetchData(url).then(data => {
cacheManager.set(cacheKey, data);
processNextBatch(data);
resolve();
}).catch(error => {
console.error('Error in auto batch:', error);
mediaData.autoBatchRunning = false;
stopBatchButton.style.display = 'none';
autoBatchButton.style.display = 'block';
downloadCurrentButton.disabled = false;
downloadAllButton.disabled = false;
if (mediaData.hasMore) {
nextBatchButton.style.display = 'block';
}
resolve();
});
}
});
await new Promise(resolve => setTimeout(resolve, 1000));
}
if (mediaData.autoBatchRunning) {
mediaData.autoBatchRunning = false;
stopBatchButton.style.display = 'none';
autoBatchButton.style.display = 'none';
}
downloadCurrentButton.disabled = false;
downloadAllButton.disabled = false;
}
function processNextBatch(data) {
if (data.timeline && data.timeline.length > 0) {
mediaData.mediaItems = data.timeline;
mediaData.hasMore = data.metadata.has_more;
mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline];
const settings = getSettings();
const mediaTypeLabel = getMediaTypeLabel(settings.mediaType);
infoContainer.innerHTML = `
<div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
<div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
<div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
<div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
`;
if (!mediaData.hasMore) {
nextBatchButton.style.display = 'none';
autoBatchButton.style.display = 'none';
stopBatchButton.style.display = 'none';
}
} else {
mediaData.hasMore = false;
nextBatchButton.style.display = 'none';
autoBatchButton.style.display = 'none';
stopBatchButton.style.display = 'none';
}
}
function chunkMediaItems(items) {
const chunks = [];
for (let i = 0; i < items.length; i += 500) {
chunks.push(items.slice(i, i + 500));
}
return chunks;
}
async function downloadMedia(downloadAll) {
if (mediaData.downloading) return;
mediaData.downloading = true;
const settings = getSettings();
const timestamp = getCurrentTimestamp();
let itemsToDownload;
if (downloadAll) {
itemsToDownload = mediaData.allMediaItems;
} else {
itemsToDownload = mediaData.mediaItems;
}
mediaData.totalToDownload = itemsToDownload.length;
mediaData.totalDownloaded = 0;
progressText.textContent = `Downloading 0/${formatNumber(mediaData.totalToDownload)}`;
progressFill.style.width = '0%';
progressContainer.style.display = 'block';
fetchButton.disabled = true;
downloadCurrentButton.disabled = true;
downloadAllButton.disabled = true;
nextBatchButton.disabled = true;
autoBatchButton.disabled = true;
stopBatchButton.disabled = true;
const chunks = chunkMediaItems(itemsToDownload);
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
const zip = new JSZip();
const hasImages = chunk.some(item => item.type === 'photo');
const hasVideos = chunk.some(item => item.type === 'video');
const hasGifs = chunk.some(item => item.type === 'gif');
let imageFolder, videoFolder, gifFolder;
if (settings.mediaType === 'all') {
if (hasImages) imageFolder = zip.folder('image');
if (hasVideos) videoFolder = zip.folder('video');
if (hasGifs) gifFolder = zip.folder('gif');
}
const filenameMap = {};
const concurrentBatches = [];
for (let i = 0; i < chunk.length; i += settings.concurrentDownloads) {
concurrentBatches.push(chunk.slice(i, i + settings.concurrentDownloads));
}
for (const batch of concurrentBatches) {
const downloadPromises = batch.map(async (item) => {
try {
const formattedDate = formatDate(item.date);
let baseFilename = `${username}_${formattedDate}_${item.tweet_id}`;
if (filenameMap[baseFilename] !== undefined) {
filenameMap[baseFilename]++;
baseFilename = `${baseFilename}_${String(filenameMap[baseFilename]).padStart(2, '0')}`;
} else {
filenameMap[baseFilename] = 0;
}
const fileExtension = item.type === 'photo' ? 'jpg' : 'mp4';
const filename = `${baseFilename}.${fileExtension}`;
const blob = await fetchBinary(item.url);
if (settings.mediaType === 'all') {
if (item.type === 'photo') {
imageFolder.file(filename, blob);
} else if (item.type === 'video') {
videoFolder.file(filename, blob);
} else if (item.type === 'gif') {
gifFolder.file(filename, blob);
}
} else {
zip.file(filename, blob);
}
return true;
} catch (error) {
console.error(`Error downloading ${item.url}:`, error);
return false;
}
});
await Promise.all(downloadPromises);
mediaData.totalDownloaded += batch.length;
progressText.textContent = `Downloading ${formatNumber(mediaData.totalDownloaded)}/${formatNumber(mediaData.totalToDownload)}`;
progressFill.style.width = `${(mediaData.totalDownloaded / mediaData.totalToDownload) * 100}%`;
}
progressText.textContent = `Creating ZIP file ${chunkIndex + 1}/${chunks.length}...`;
try {
const zipBlob = await zip.generateAsync({ type: 'blob' });
let zipFilename;
if (chunks.length === 1 && chunk.length < 500) {
zipFilename = `${username}_${timestamp}.zip`;
} else if (settings.batchEnabled && !downloadAll) {
zipFilename = `${username}_${timestamp}_part_${String(mediaData.currentPage + 1).padStart(2, '0')}.zip`;
} else {
zipFilename = `${username}_${timestamp}_part_${String(chunkIndex + 1).padStart(2, '0')}.zip`;
}
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(zipBlob);
downloadLink.download = zipFilename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
} catch (error) {
console.error('Error creating ZIP:', error);
progressText.textContent = `Error creating ZIP ${chunkIndex + 1}: ${error.message}`;
}
}
progressText.textContent = 'Download complete!';
progressFill.style.width = '100%';
setTimeout(() => {
fetchButton.disabled = false;
downloadCurrentButton.disabled = false;
downloadAllButton.disabled = false;
nextBatchButton.disabled = false;
autoBatchButton.disabled = false;
stopBatchButton.disabled = false;
mediaData.downloading = false;
}, 2000);
}
document.body.appendChild(modal);
}
function insertDownloadIcon() {
const usernameDivs = document.querySelectorAll('[data-testid="UserName"]');
usernameDivs.forEach((usernameDiv) => {
if (!usernameDiv.querySelector(".download-icon")) {
const username = extractUsername();
if (!username) return;
const verifiedButton = usernameDiv
.querySelector('[aria-label*="verified"], [aria-label*="Verified"]')
?.closest("button");
const targetElement = verifiedButton
? verifiedButton.parentElement
: usernameDiv.querySelector(".css-1jxf684")?.closest("span");
if (targetElement) {
const downloadIcon = createDownloadIcon();
const iconDiv = document.createElement("div");
iconDiv.className = "download-icon css-175oi2r r-1awozwy r-xoduu5";
iconDiv.style.cssText = `
display: inline-flex;
align-items: center;
margin-left: 6px;
margin-right: 6px;
gap: 6px;
padding: 0 3px;
transition: transform 0.2s, color 0.2s;
`;
iconDiv.appendChild(downloadIcon);
iconDiv.addEventListener("mouseenter", () => {
iconDiv.style.transform = "scale(1.1)";
iconDiv.style.color = "#0ea5e9";
});
iconDiv.addEventListener("mouseleave", () => {
iconDiv.style.transform = "scale(1)";
iconDiv.style.color = "";
});
iconDiv.addEventListener("click", (e) => {
e.stopPropagation();
createModal(username);
});
const wrapperDiv = document.createElement("div");
wrapperDiv.style.cssText = `
display: inline-flex;
align-items: center;
gap: 4px;
`;
wrapperDiv.appendChild(iconDiv);
targetElement.parentNode.insertBefore(wrapperDiv, targetElement.nextSibling);
}
}
});
}
insertDownloadIcon();
let lastUrl = location.href;
const observer = new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
setTimeout(insertDownloadIcon, 1000);
} else {
insertDownloadIcon();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
})();