├── 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 |
--------------------------------------------------------------------------------