Right-click a folder in Google Drive → "Get Direct Download Links" → save a JSON with appid + ddl for every ZIP/RAR inside
// ==UserScript==
// @name Google Drive — Folder DDL Extractor
// @namespace https://drive.google.com/
// @version 3.0.0
// @description Right-click a folder in Google Drive → "Get Direct Download Links" → save a JSON with appid + ddl for every ZIP/RAR inside
// @author Arose and Claude
// @match https://drive.google.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const MENU_LABEL = '📦 Get Direct Download Links';
const TARGET_EXTS = ['zip', 'rar'];
const MENU_ID = 'gdrive-ddl-menu-item';
const FOLDER_MIME = 'application/vnd.google-apps.folder';
/* ─── TOAST ─────────────────────────────────────────────────── */
function showToast(msg, color = '#1a73e8', duration = 3500) {
const old = document.getElementById('gdrive-ddl-toast');
if (old) old.remove();
const t = document.createElement('div');
t.id = 'gdrive-ddl-toast';
Object.assign(t.style, {
position: 'fixed', bottom: '28px', right: '28px', zIndex: 99999,
background: color, color: '#fff', padding: '12px 20px',
borderRadius: '8px', fontSize: '14px',
fontFamily: 'Google Sans, Roboto, sans-serif',
boxShadow: '0 4px 18px rgba(0,0,0,0.22)', maxWidth: '400px',
lineHeight: '1.5', transition: 'opacity 0.4s',
});
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, duration);
}
/* ─── SAVE DIALOG (browse + custom filename) ─────────────────── */
async function saveWithDialog(jsonStr, suggestedName) {
// Use the modern File System Access API if available (Chrome 86+)
if (window.showSaveFilePicker) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: suggestedName,
types: [{ description: 'JSON File', accept: { 'application/json': ['.json'] } }],
});
const writable = await handle.createWritable();
await writable.write(jsonStr);
await writable.close();
showToast('✅ File saved!', '#34a853');
return;
} catch (e) {
if (e.name === 'AbortError') return; // user cancelled — do nothing
}
}
// Fallback: prompt for name then trigger download
const name = prompt('Save as:', suggestedName);
if (!name) return; // cancelled
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = name.endsWith('.json') ? name : name + '.json';
a.click();
setTimeout(() => URL.revokeObjectURL(url), 2000);
showToast('✅ Download started!', '#34a853');
}
/* ─── MODAL ─────────────────────────────────────────────────── */
function showModal(jsonStr, folderName) {
const backdrop = document.createElement('div');
Object.assign(backdrop.style, {
position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.55)',
zIndex: 99998, display: 'flex', alignItems: 'center', justifyContent: 'center',
});
const box = document.createElement('div');
Object.assign(box.style, {
background: '#fff', borderRadius: '12px', padding: '28px',
width: 'min(680px, 90vw)', maxHeight: '80vh', display: 'flex',
flexDirection: 'column', gap: '16px',
boxShadow: '0 8px 40px rgba(0,0,0,0.3)',
fontFamily: 'Google Sans, Roboto, sans-serif',
});
const title = document.createElement('h2');
title.textContent = '📦 Direct Download Links JSON';
Object.assign(title.style, { margin: '0', fontSize: '18px', color: '#202124' });
const pre = document.createElement('textarea');
pre.value = jsonStr;
Object.assign(pre.style, {
flex: '1', minHeight: '240px', border: '1px solid #dadce0',
borderRadius: '8px', padding: '12px', fontFamily: 'monospace',
fontSize: '13px', resize: 'vertical', color: '#3c4043',
background: '#f8f9fa', outline: 'none',
});
// Filename row
const fileRow = document.createElement('div');
Object.assign(fileRow.style, { display: 'flex', alignItems: 'center', gap: '8px' });
const fileLabel = document.createElement('label');
fileLabel.textContent = 'Filename:';
Object.assign(fileLabel.style, { fontSize: '14px', color: '#5f6368', whiteSpace: 'nowrap' });
const fileInput = document.createElement('input');
fileInput.type = 'text';
fileInput.value = (folderName || 'drive_ddl_links').replace(/[^a-z0-9_\-]/gi, '_') + '.json';
Object.assign(fileInput.style, {
flex: '1', padding: '8px 12px', border: '1px solid #dadce0',
borderRadius: '6px', fontSize: '14px', fontFamily: 'inherit', outline: 'none',
});
fileRow.append(fileLabel, fileInput);
const btnRow = document.createElement('div');
Object.assign(btnRow.style, { display: 'flex', gap: '10px', justifyContent: 'flex-end' });
const mkBtn = (label, bg, fg) => {
const b = document.createElement('button');
b.textContent = label;
Object.assign(b.style, {
padding: '9px 20px', borderRadius: '6px', border: 'none',
background: bg, color: fg, fontWeight: '600', cursor: 'pointer',
fontSize: '14px', fontFamily: 'inherit',
});
return b;
};
const copyBtn = mkBtn('Copy JSON', '#1a73e8', '#fff');
const saveBtn = mkBtn('💾 Save File…', '#34a853', '#fff');
const closeBtn = mkBtn('Close', '#f1f3f4', '#3c4043');
copyBtn.onclick = () => navigator.clipboard.writeText(jsonStr).then(() => showToast('✅ Copied!'));
saveBtn.onclick = () => {
const name = fileInput.value.trim() || 'drive_ddl_links.json';
saveWithDialog(jsonStr, name.endsWith('.json') ? name : name + '.json');
};
closeBtn.onclick = () => backdrop.remove();
backdrop.onclick = (e) => { if (e.target === backdrop) backdrop.remove(); };
btnRow.append(copyBtn, saveBtn, closeBtn);
box.append(title, pre, fileRow, btnRow);
backdrop.appendChild(box);
document.body.appendChild(backdrop);
}
/* ─── FILE LISTING ───────────────────────────────────────────── */
function getTokenFromPage() {
for (const s of document.querySelectorAll('script')) {
const m = s.textContent.match(/"token"\s*:\s*"(ya29\.[^"]{20,})"/);
if (m) return m[1];
}
try {
const str = JSON.stringify(window.WIZ_global_data || {});
const m = str.match(/ya29\.[A-Za-z0-9_\-]{20,}/);
if (m) return m[0];
} catch (_) {}
return null;
}
async function fetchViaAPI(query, token) {
let files = [], pageToken = '';
do {
const params = new URLSearchParams({
q: query, fields: 'nextPageToken,files(id,name)',
pageSize: '1000', supportsAllDrives: 'true',
includeItemsFromAllDrives: 'true',
});
if (pageToken) params.set('pageToken', pageToken);
const headers = { 'X-Goog-AuthUser': '0' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`https://www.googleapis.com/drive/v3/files?${params}`, { credentials: 'include', headers });
if (!res.ok) return null;
const data = await res.json();
if (data.error) return null;
files = files.concat(data.files || []);
pageToken = data.nextPageToken || '';
} while (pageToken);
return files;
}
async function scrapeFolder(folderId) {
showToast('⏳ Loading folder page to read files…', '#1a73e8', 15000);
const res = await fetch(`https://drive.google.com/drive/folders/${folderId}`, { credentials: 'include' });
const html = await res.text();
const files = [];
const seen = new Set();
const extRx = TARGET_EXTS.join('|');
const re = new RegExp(`"([^"\\\\]*\\.(?:${extRx}))"`, 'gi');
let m;
while ((m = re.exec(html)) !== null) {
const name = m[1];
const after = html.slice(m.index, m.index + 400);
const idM = after.match(/"([A-Za-z0-9_-]{28,44})"/);
if (idM && !seen.has(idM[1])) {
seen.add(idM[1]);
files.push({ name, id: idM[1] });
}
}
if (!files.length) throw new Error('No ZIP/RAR files found in this folder.');
return files;
}
async function listFilesInFolder(folderId) {
const extFilter = TARGET_EXTS.map(e => `name contains '.${e}'`).join(' or ');
const query = `'${folderId}' in parents and trashed = false and (${extFilter})`;
const token = getTokenFromPage();
if (token) {
const r = await fetchViaAPI(query, token).catch(() => null);
if (r) return r;
}
const r2 = await fetchViaAPI(query, null).catch(() => null);
if (r2) return r2;
return scrapeFolder(folderId);
}
const toDDL = (id) => `https://drive.google.com/uc?export=download&id=${id}`;
const toAppId = (name) => name.replace(/\.(zip|rar)$/i, '');
/* ─── MAIN HANDLER ───────────────────────────────────────────── */
async function handleExtract(folderId, folderName) {
showToast('⏳ Fetching file list…', '#1a73e8', 60000);
try {
const files = await listFilesInFolder(folderId);
if (!files.length) { showToast('⚠️ No ZIP / RAR files found in this folder.', '#f29900'); return; }
const entries = files.map(f => ({ appid: toAppId(f.name), ddl: toDDL(f.id) }));
showToast(`✅ Found ${files.length} file(s)!`, '#34a853');
showModal(JSON.stringify(entries, null, 2), folderName);
} catch (err) {
console.error('[GDrive DDL]', err);
showToast(`❌ ${err.message}`, '#d93025', 8000);
}
}
/* ─── FOLDER DETECTION ───────────────────────────────────────── */
// Returns true if the element (or its ancestors) represents a Drive FOLDER
function isFolder(el) {
if (!el) return false;
// Drive marks the row's drag-drop target with the mime type
if (el.closest('[data-mime-type="' + FOLDER_MIME + '"]')) return true;
// In list view the icon has an aria-label containing "folder"
if (el.closest('[data-id]')) {
const row = el.closest('[data-id]');
// Check aria-labels on the row or its children
const labels = [
row.getAttribute('aria-label') || '',
...([...row.querySelectorAll('[aria-label]')].map(n => n.getAttribute('aria-label') || '')),
];
if (labels.some(l => /\bfolder\b/i.test(l))) return true;
// Drive's list view uses a specific icon class for folders (drive-folder icon)
if (row.querySelector('[data-icon-type="folder"], [aria-label="Folder"]')) return true;
// Grid view: folder cards have a div with role="img" and aria-label containing "Folder"
if (row.querySelector('[role="img"][aria-label*="Folder" i]')) return true;
}
return false;
}
/* ─── CONTEXT MENU INJECTION ─────────────────────────────────── */
// Store info about the right-clicked element
window.__gddlTarget = { id: null, name: null, isFolder: false };
document.addEventListener('contextmenu', (e) => {
const el = e.target.closest('[data-id]');
if (!el) return;
window.__gddlTarget = {
id: el.getAttribute('data-id'),
name: el.getAttribute('aria-label') || el.querySelector('[data-tooltip]')?.getAttribute('data-tooltip') || 'folder',
isFolder: isFolder(e.target),
};
}, true);
function findMenuContainer() {
for (const el of document.querySelectorAll('ul, [role="menu"]')) {
if (el.querySelector(`#${MENU_ID}`)) continue;
const style = getComputedStyle(el);
if (style.position !== 'absolute' && style.position !== 'fixed') continue;
if (style.display === 'none' || style.visibility === 'hidden') continue;
if (el.querySelectorAll('li, [role="menuitem"]').length < 2) continue;
return el;
}
return null;
}
function injectMenuItem(container) {
if (container.querySelector(`#${MENU_ID}`)) return;
// Only inject when a folder was right-clicked
if (!window.__gddlTarget.isFolder) return;
const usesLi = !!container.querySelector('li');
const item = document.createElement(usesLi ? 'li' : 'div');
item.id = MENU_ID;
const sibling = container.querySelector(usesLi ? 'li' : '[role="menuitem"]');
if (sibling) item.className = sibling.className;
item.setAttribute('role', 'menuitem');
item.setAttribute('tabindex', '0');
Object.assign(item.style, {
display: 'flex', alignItems: 'center', gap: '16px',
padding: '8px 24px', cursor: 'pointer', userSelect: 'none',
fontSize: '14px', fontFamily: 'Google Sans, Roboto, sans-serif',
color: '#202124', listStyle: 'none', whiteSpace: 'nowrap', boxSizing: 'border-box',
});
item.innerHTML = `
<span style="display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;font-size:16px;flex-shrink:0">📦</span>
<span style="flex:1">${MENU_LABEL}</span>
`;
item.addEventListener('mouseenter', () => { item.style.background = '#f1f3f4'; });
item.addEventListener('mouseleave', () => { item.style.background = ''; });
item.addEventListener('click', (e) => {
e.stopPropagation(); e.preventDefault();
const wrapper = container.closest('[jsname],[data-ved],[jscontroller]') || container;
wrapper.remove();
const { id, name } = window.__gddlTarget;
if (!id) { showToast('⚠️ Could not detect folder ID — right-click directly on the folder row.', '#f29900'); return; }
handleExtract(id, name);
});
const hr = document.createElement('div');
Object.assign(hr.style, { height: '1px', background: '#e0e0e0', margin: '4px 0' });
container.appendChild(hr);
container.appendChild(item);
}
const menuObserver = new MutationObserver(() => {
const c = findMenuContainer();
if (c) injectMenuItem(c);
});
menuObserver.observe(document.body, { childList: true, subtree: true });
/* ─── FLOATING BUTTON (inside a folder via URL) ──────────────── */
function injectFAB() {
if (document.getElementById('gdrive-ddl-fab')) return;
const fab = document.createElement('button');
fab.id = 'gdrive-ddl-fab';
fab.title = 'Extract DDL Links from current folder';
fab.innerHTML = '📦';
Object.assign(fab.style, {
position: 'fixed', bottom: '24px', left: '24px', zIndex: 9999,
width: '52px', height: '52px', borderRadius: '50%', border: 'none',
background: '#1a73e8', color: '#fff', fontSize: '22px', cursor: 'pointer',
boxShadow: '0 3px 14px rgba(0,0,0,0.25)',
transition: 'transform 0.15s, box-shadow 0.15s',
display: 'flex', alignItems: 'center', justifyContent: 'center',
});
fab.addEventListener('mouseenter', () => { fab.style.transform = 'scale(1.1)'; fab.style.boxShadow = '0 6px 20px rgba(0,0,0,0.3)'; });
fab.addEventListener('mouseleave', () => { fab.style.transform = 'scale(1)'; fab.style.boxShadow = '0 3px 14px rgba(0,0,0,0.25)'; });
fab.addEventListener('click', () => {
const match = location.pathname.match(/\/folders\/([^/?#]+)/);
if (match) handleExtract(match[1], document.title.replace(' - Google Drive', '').trim());
else showToast('⚠️ Navigate into a folder first, then click this button.', '#f29900');
});
document.body.appendChild(fab);
}
const fabInterval = setInterval(() => {
if (document.querySelector('[role="main"]')) { injectFAB(); clearInterval(fabInterval); }
}, 800);
})();