├── Downloader Script.js ├── LICENSE └── README.md /Downloader Script.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Multi-Site Media Downloader 3 | // @namespace http://tampermonkey.net/ 4 | // @version 2.2 5 | // @description Download images and videos from tags on Rule34, Gelbooru, and Danbooru 6 | // @author shadybrady 7 | // @match https://rule34.xxx/* 8 | // @match https://danbooru.donmai.us/* 9 | // @match https://gelbooru.com/* 10 | // @downloadURL https://raw.githubusercontent.com/shadybrady101/R34-Danbooru-media-downloader/main/Downloader%20Script.js 11 | // @updateURL https://raw.githubusercontent.com/shadybrady101/R34-Danbooru-media-downloader/main/Downloader%20Script.js 12 | // @grant GM_xmlhttpRequest 13 | // @grant GM_download 14 | // @license MIT 15 | // ==/UserScript== 16 | 17 | (function () { 18 | 'use strict'; 19 | 20 | console.log('Script activated on:', window.location.hostname); 21 | 22 | const MAX_PER_PAGE = 100; 23 | const BATCH_SIZE = 500; // Number of entries to save in a single batch 24 | const LOCAL_STORAGE_KEY = 'downloadedMedia'; 25 | const MAX_API_RETRIES = 3; 26 | 27 | let totalMedia = 0; 28 | let downloadedMedia = 0; 29 | let failedDownloads = 0; 30 | let skippedMedia = 0; 31 | let progressContainer; 32 | let stopRequested = false; 33 | 34 | const downloadedMediaSet = new Set( 35 | JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '[]') 36 | ); 37 | const inProgressDownloads = new Set(); 38 | const newlySkippedSet = new Set(); 39 | 40 | function createUIContainer() { 41 | const container = document.createElement('div'); 42 | container.style.cssText = ` 43 | position: fixed; 44 | top: 10px; 45 | right: 10px; 46 | z-index: 1000; 47 | padding: 8px; 48 | background-color: #1e1e1e; 49 | color: #f0f0f0; 50 | border: 1px solid #555; 51 | border-radius: 6px; 52 | width: 260px; 53 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); 54 | font-family: Arial, sans-serif; 55 | font-size: 13px; 56 | `; 57 | 58 | const buttonContainer = document.createElement('div'); 59 | buttonContainer.style.cssText = 'display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap;'; 60 | 61 | const downloadButton = document.createElement('button'); 62 | downloadButton.innerText = 'Download'; 63 | downloadButton.style.cssText = getButtonStyles('#4CAF50'); 64 | downloadButton.addEventListener('click', () => { 65 | const tags = prompt('Enter tags for mass download (separated by spaces):'); 66 | const scoreThreshold = parseInt(prompt('Enter the minimum score for downloads:'), 10); 67 | if (tags && !isNaN(scoreThreshold)) { 68 | stopRequested = false; 69 | startMassDownload(tags.trim(), scoreThreshold); 70 | } 71 | }); 72 | 73 | const stopButton = document.createElement('button'); 74 | stopButton.innerText = 'Stop'; 75 | stopButton.style.cssText = getButtonStyles('#F44336'); 76 | stopButton.addEventListener('click', () => { 77 | if (confirm('Stop all processes?')) { 78 | stopRequested = true; 79 | alert('Stopping downloads. This may take a moment.'); 80 | } 81 | }); 82 | 83 | const resetButton = document.createElement('button'); 84 | resetButton.innerText = 'Reset Skipped'; 85 | resetButton.style.cssText = getButtonStyles('#FFC107'); 86 | resetButton.addEventListener('click', () => { 87 | if (confirm('Are you sure you want to reset skipped tracking?')) { 88 | downloadedMediaSet.clear(); 89 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify([...downloadedMediaSet])); 90 | alert('Skipped tracking has been reset.'); 91 | } 92 | }); 93 | 94 | buttonContainer.appendChild(downloadButton); 95 | buttonContainer.appendChild(stopButton); 96 | buttonContainer.appendChild(resetButton); 97 | 98 | progressContainer = document.createElement('div'); 99 | progressContainer.style.cssText = ` 100 | background-color: #2e2e2e; 101 | padding: 8px; 102 | border-radius: 4px; 103 | font-size: 12px; 104 | text-align: left; 105 | `; 106 | progressContainer.innerHTML = ` 107 | Progress: 0%
108 | Downloaded: 0
109 | Failed: 0
110 | Skipped: 0 111 | `; 112 | 113 | container.appendChild(buttonContainer); 114 | container.appendChild(progressContainer); 115 | document.body.appendChild(container); 116 | } 117 | 118 | function getButtonStyles(color) { 119 | return ` 120 | flex: 1; 121 | padding: 6px 10px; 122 | background-color: ${color}; 123 | color: white; 124 | border: none; 125 | border-radius: 4px; 126 | cursor: pointer; 127 | font-family: Arial, sans-serif; 128 | font-size: 13px; 129 | `; 130 | } 131 | 132 | function updateProgress() { 133 | const percentage = totalMedia > 0 ? Math.floor(((downloadedMedia + failedDownloads + skippedMedia) / totalMedia) * 100) : 0; 134 | progressContainer.innerHTML = ` 135 | Progress: ${percentage}%
136 | Downloaded: ${downloadedMedia}
137 | Failed: ${failedDownloads}
138 | Skipped: ${skippedMedia} 139 | `; 140 | } 141 | 142 | async function startMassDownload(tags, scoreThreshold) { 143 | totalMedia = 0; 144 | downloadedMedia = 0; 145 | failedDownloads = 0; 146 | skippedMedia = 0; 147 | 148 | let page = 0; 149 | let continueFetching = true; 150 | 151 | while (continueFetching && !stopRequested) { 152 | const url = generateSearchUrl(tags, page); 153 | console.log(`Fetching: ${url}`); 154 | 155 | const response = await fetchWithRetry(url); 156 | console.log(response); 157 | 158 | if (!response || response.length === 0 || stopRequested) { 159 | console.warn('No more posts found or stopped.'); 160 | break; 161 | } 162 | 163 | const posts = parsePosts(response); 164 | 165 | const filteredPosts = posts.filter(post => (post.score || 0) >= scoreThreshold); 166 | 167 | totalMedia += filteredPosts.length; 168 | 169 | for (const post of filteredPosts) { 170 | if (stopRequested) break; 171 | 172 | if (post.file_url) { 173 | if (!downloadedMediaSet.has(post.file_url) && !inProgressDownloads.has(post.file_url)) { 174 | const fileName = `post_${post.id}`; 175 | downloadMedia(post.file_url, fileName); 176 | } else { 177 | skippedMedia++; 178 | newlySkippedSet.add(post.file_url); 179 | updateProgress(); 180 | } 181 | } else { 182 | console.warn(`Post ${post.id} has no file_url`); 183 | failedDownloads++; 184 | updateProgress(); 185 | } 186 | } 187 | 188 | continueFetching = posts.length === MAX_PER_PAGE; 189 | page++; 190 | 191 | if (newlySkippedSet.size >= BATCH_SIZE) { 192 | saveSkippedMedia(); 193 | } 194 | } 195 | 196 | if (stopRequested) { 197 | alert('Mass download stopped by user.'); 198 | } else { 199 | checkCompletion(); 200 | } 201 | } 202 | 203 | function saveSkippedMedia() { 204 | newlySkippedSet.forEach(url => downloadedMediaSet.add(url)); 205 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify([...downloadedMediaSet])); 206 | newlySkippedSet.clear(); 207 | console.log('Saved skipped media to localStorage.'); 208 | } 209 | 210 | function generateSearchUrl(tags, page) { 211 | if (window.location.hostname.includes('rule34.xxx')) { 212 | return `https://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&tags=${encodeURIComponent(tags)}&limit=${MAX_PER_PAGE}&pid=${page}`; 213 | } else if (window.location.hostname.includes('danbooru.donmai.us')) { 214 | return `https://danbooru.donmai.us/posts.json?tags=${encodeURIComponent(tags)}&limit=${MAX_PER_PAGE}&page=${page + 1}`; 215 | } else if (window.location.hostname.includes('gelbooru.com')) { 216 | return `https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&tags=${encodeURIComponent(tags)}&limit=${MAX_PER_PAGE}&pid=${page}`; 217 | } 218 | throw new Error('Unsupported site'); 219 | } 220 | 221 | function parsePosts(response) { 222 | if (Array.isArray(response)) { 223 | return response.map(post => ({ 224 | id: post.id, 225 | file_url: post.file_url || post.large_file_url || post.preview_file_url, 226 | score: post.score || 0, 227 | })); 228 | } else if (response.post) { 229 | return Array.isArray(response.post) ? response.post : [response.post]; 230 | } 231 | return []; 232 | } 233 | 234 | async function fetchWithRetry(url, retries = MAX_API_RETRIES) { 235 | return new Promise((resolve, reject) => { 236 | GM_xmlhttpRequest({ 237 | method: 'GET', 238 | url: url, 239 | onload: function (response) { 240 | if (response.status === 200) { 241 | resolve(JSON.parse(response.responseText)); 242 | } else { 243 | handleRetry(retries, reject, url); 244 | } 245 | }, 246 | onerror: function () { 247 | handleRetry(retries, reject, url); 248 | } 249 | }); 250 | }); 251 | } 252 | 253 | function handleRetry(retries, reject, url) { 254 | if (retries > 0 && !stopRequested) { 255 | console.warn(`Retrying... (${retries} attempts left)`); 256 | fetchWithRetry(url, retries - 1).then(resolve).catch(reject); 257 | } else { 258 | console.error('Failed to fetch:', url); 259 | reject('Failed after retrying'); 260 | } 261 | } 262 | 263 | function downloadMedia(url, baseFileName) { 264 | let fileExt = url.split('.').pop().split('?')[0]; 265 | if (!fileExt.match(/^(jpg|jpeg|png|gif|webm|mp4)$/i)) { 266 | fileExt = 'jpg'; // Default to 'jpg' if the extension is not recognized 267 | } 268 | 269 | const fileName = `${baseFileName}.${fileExt}`; 270 | inProgressDownloads.add(url); 271 | 272 | GM_download({ 273 | url: url, 274 | name: fileName, 275 | onload: () => { 276 | console.log(`Downloaded: ${fileName}`); 277 | downloadedMedia++; 278 | downloadedMediaSet.add(url); 279 | inProgressDownloads.delete(url); 280 | updateProgress(); 281 | }, 282 | onerror: (err) => { 283 | console.error(`Failed to download: ${url}`, err); 284 | failedDownloads++; 285 | inProgressDownloads.delete(url); 286 | updateProgress(); 287 | }, 288 | }); 289 | } 290 | 291 | function checkCompletion() { 292 | saveSkippedMedia(); 293 | const interval = setInterval(() => { 294 | if (downloadedMedia + failedDownloads + skippedMedia === totalMedia) { 295 | clearInterval(interval); 296 | progressContainer.innerHTML += '
Download Complete!'; 297 | alert( 298 | `Mass download complete!\nSuccessful: ${downloadedMedia}\nFailed: ${failedDownloads}\nSkipped: ${skippedMedia}\nTotal: ${totalMedia}` 299 | ); 300 | } 301 | }, 500); 302 | } 303 | 304 | window.addEventListener('load', () => { 305 | createUIContainer(); 306 | }); 307 | 308 | console.log('Multi-Site Media Downloader is active.'); 309 | })(); 310 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 shadybrady101 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Media Downloader 2 | 3 | ## Overview 4 | The **Multi-Site Media Downloader** is a Tampermonkey userscript that allows users to download images and videos from **Rule34**, **Gelbooru**, **Danbooru** using specific tags. 5 | 6 | ## Features 7 | - Supports downloading media from Rule34, Gelbooru, and Danbooru. 8 | - Downloading by tags and by score. 9 | - Progress bar to track download progress. 10 | - Saves locally and will never repeat downloads unless you reset it. 11 | 12 | ## Disclaimer 13 | The user is solely responsible for complying with the terms of service of the websites they interact with and ensuring they have permission to download any content. 14 | 15 | ## Installation 16 | 1. Install [Tampermonkey](https://www.tampermonkey.net/) or a similar browser extension. 17 | 2. Add this script to Tampermonkey: 18 | - Click the "Add New Script" button in Tampermonkey. 19 | - Paste the script into the editor. 20 | - Save the script. 21 | 22 | ## Usage 23 | 1. Visit Rule34, Gelbooru, Danbooru. 24 | 2. Use the **"Download"** button located at the top-right of the page. 25 | 3. Enter tags to search for specific media (e.g., `cute_girl`). 26 | 4. The script will fetch and download media files matching your tags. 27 | 5. A progress bar will display the download progress, and a completion alert will show the total number of successful and failed downloads. 28 | 29 | ## Limitations 30 | - Downloading over 1000 I highly reccomend just closing out your browser. 31 | - Some websites may throttle or block automated downloads if abused, use responsibly. 32 | - Chrome downloads(does not save in a made folder), I recommend using Firefox. 33 | - Tag limits, Rule34(15), Gelbooru(10), Danbooru(2 unless premium user), 34 | 35 | ## Contributing 36 | Contributions are welcome! Feel free to submit issues or pull requests on the GitHub repository. 37 | 38 | ## License 39 | This project is licensed under the [MIT License](LICENSE). 40 | --------------------------------------------------------------------------------