ينزّل صور الفصل من utoon.net كملف ZIP مرتّب الترقيم (يستعمل اتصال متصفحك مباشرة، يتخطى حظر Cloudflare).
// ==UserScript==
// @name ARCANE — Utoon Chapter Downloader
// @name:ar ARCANE — تنزيل فصول Utoon
// @namespace https://arcane.app/utoon
// @version 1.0.0
// @description Download all images of a utoon.net chapter as a numbered ZIP using your own browser session (bypasses Cloudflare datacenter blocks).
// @description:ar ينزّل صور الفصل من utoon.net كملف ZIP مرتّب الترقيم (يستعمل اتصال متصفحك مباشرة، يتخطى حظر Cloudflare).
// @author ARCANE
// @license MIT
// @homepageURL https://greasyfork.org/en/scripts/arcane-utoon-chapter-downloader
// @supportURL https://greasyfork.org/en/scripts/arcane-utoon-chapter-downloader/feedback
// @match https://utoon.net/*
// @match https://*.utoon.net/*
// @icon https://utoon.net/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect utoon.net
// @connect *
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const IMAGE_SELECTORS = [
'div.page-break img',
'li.blocks-gallery-item img',
'div.reading-content img',
'div.text-left img',
];
const isChapterPage = () =>
/\/manga\/[^/]+\/(chapter|ep|episode)[-_ ]?\d/i.test(location.pathname);
function pickSrc(img) {
const v = (img.getAttribute('data-lazy-src') ||
img.getAttribute('data-src') ||
img.getAttribute('src') || '').trim();
if (!v || v.startsWith('data:')) return '';
try { return new URL(v, location.href).href; } catch { return ''; }
}
function collectImages() {
for (const sel of IMAGE_SELECTORS) {
const nodes = document.querySelectorAll(sel);
const urls = [];
const seen = new Set();
nodes.forEach(img => {
const u = pickSrc(img);
if (u && !seen.has(u)) { seen.add(u); urls.push(u); }
});
if (urls.length) return urls;
}
return [];
}
function detectMeta() {
const m = location.pathname.match(/\/manga\/([^/]+)\/([^/]+)/);
const seriesSlug = m ? m[1] : 'series';
const chapterSlug = m ? m[2] : 'chapter';
let seriesTitle = '';
const titleEl = document.querySelector('.entry-header h1, .breadcrumb a[href*="/manga/"]:last-of-type, h1.entry-title');
if (titleEl) seriesTitle = titleEl.textContent.trim();
return {
series: (seriesTitle || seriesSlug).replace(/[\\/:*?"<>|]/g, '_').slice(0, 80),
chapter: chapterSlug.replace(/[\\/:*?"<>|]/g, '_'),
};
}
// Force lazy images to load by scrolling to bottom then back to top
async function autoScroll() {
return new Promise(resolve => {
const total = document.body.scrollHeight;
let y = 0;
const step = Math.max(400, Math.floor(window.innerHeight * 0.9));
const timer = setInterval(() => {
window.scrollTo(0, y);
y += step;
if (y >= total + window.innerHeight) {
clearInterval(timer);
window.scrollTo(0, 0);
setTimeout(resolve, 500);
}
}, 120);
});
}
function fetchBlob(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'blob',
headers: { 'Referer': location.origin + '/' },
timeout: 60000,
onload: r => {
if (r.status >= 200 && r.status < 300 && r.response) resolve(r.response);
else reject(new Error(`HTTP ${r.status}`));
},
onerror: () => reject(new Error('network')),
ontimeout: () => reject(new Error('timeout')),
});
});
}
function extOf(url, blob) {
const m = url.split('?')[0].match(/\.([a-z0-9]{2,5})$/i);
if (m) return m[1].toLowerCase();
if (blob && blob.type) {
const t = blob.type.split('/')[1];
if (t) return t.split('+')[0];
}
return 'jpg';
}
function pad(i, n) {
return String(i).padStart(String(n).length, '0');
}
function setStatus(msg) {
const s = document.getElementById('arcane-utoon-status');
if (s) s.textContent = msg;
console.log('[ARCANE]', msg);
}
async function downloadChapter() {
try {
setStatus('بحمّل الصور المخفية… (سحب تلقائي)');
await autoScroll();
const urls = collectImages();
if (!urls.length) {
alert('ARCANE: ما لقيتش صور الفصل. تأكد إنك على صفحة فصل مفتوحة.');
setStatus('فشل: لا توجد صور');
return;
}
setStatus(`لقيت ${urls.length} صورة. بنزّل…`);
const zip = new JSZip();
let ok = 0, fail = 0;
const concurrency = 5;
let cursor = 0;
async function worker() {
while (cursor < urls.length) {
const i = cursor++;
const url = urls[i];
try {
const blob = await fetchBlob(url);
const ext = extOf(url, blob);
zip.file(`${pad(i + 1, urls.length)}.${ext}`, blob);
ok++;
} catch (e) {
console.warn('[ARCANE] failed', url, e);
fail++;
}
setStatus(`تنزيل ${ok + fail}/${urls.length} (نجاح ${ok}، فشل ${fail})`);
}
}
await Promise.all(Array.from({ length: concurrency }, worker));
if (ok === 0) {
alert('ARCANE: فشل تنزيل كل الصور.');
setStatus('فشل التنزيل');
return;
}
setStatus('بضغط الـ ZIP…');
const zipBlob = await zip.generateAsync({ type: 'blob' }, meta => {
if (meta.percent != null) setStatus(`ضغط ${meta.percent.toFixed(0)}%`);
});
const meta = detectMeta();
const fileName = `${meta.series}__${meta.chapter}.zip`;
const a = document.createElement('a');
a.href = URL.createObjectURL(zipBlob);
a.download = fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 2000);
setStatus(`✅ تم: ${ok}/${urls.length} → ${fileName}`);
} catch (e) {
console.error('[ARCANE]', e);
alert('ARCANE error: ' + e.message);
setStatus('خطأ: ' + e.message);
}
}
function injectUI() {
if (document.getElementById('arcane-utoon-btn')) return;
const wrap = document.createElement('div');
wrap.id = 'arcane-utoon-ui';
wrap.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 2147483647;
background: #1a1a2e; color: #fff; padding: 10px 14px; border-radius: 10px;
font-family: system-ui, -apple-system, sans-serif; font-size: 13px;
box-shadow: 0 4px 18px rgba(0,0,0,.5); direction: rtl;
border: 1px solid #6a4cff; max-width: 280px;
`;
const btn = document.createElement('button');
btn.id = 'arcane-utoon-btn';
btn.textContent = '📥 تنزيل الفصل (ZIP)';
btn.style.cssText = `
background: #6a4cff; color: #fff; border: 0; padding: 8px 14px;
border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 13px;
width: 100%;
`;
btn.onclick = () => { btn.disabled = true; btn.textContent = '...جاري التنزيل'; downloadChapter().finally(() => { btn.disabled = false; btn.textContent = '📥 تنزيل الفصل (ZIP)'; }); };
const status = document.createElement('div');
status.id = 'arcane-utoon-status';
status.style.cssText = 'margin-top: 6px; font-size: 11px; color: #b8b8d4; min-height: 14px;';
status.textContent = isChapterPage() ? 'جاهز' : 'افتح صفحة فصل أولاً';
wrap.appendChild(btn);
wrap.appendChild(status);
document.body.appendChild(wrap);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectUI);
} else {
injectUI();
}
})();