Télécharge nhentai, x-manga, ortegascans et hdporncomics en .cbz
// ==UserScript==
// @name CBZ Downloader (Multi-site)
// @namespace local.cbz.downloader
// @version 0.5.13
// @description Télécharge nhentai, x-manga, ortegascans et hdporncomics en .cbz
// @description:fr Télécharge nhentai, x-manga, ortegascans et hdporncomics en .cbz
// @license MIT
// @author You
// @match https://nhentai.net/*
// @match https://x-manga.org/*
// @match https://x-manga.net/*
// @match https://ortegascans.fr/*
// @match https://*.ortegascans.fr/*
// @match https://hdporncomics.com/*
// @match https://*.hdporncomics.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @connect *
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
"use strict";
if (window.__cbzLoaded) return;
window.__cbzLoaded = true;
const IS_NHENTAI = /nhentai\./.test(location.hostname);
const IS_XMANGA = /x-manga\.(org|net)/.test(location.hostname);
const IS_ORTEGA = /ortega/i.test(location.hostname);
const IS_HDPC = /hdporncomics\./.test(location.hostname);
const IS_LIVE = IS_XMANGA || IS_HDPC;
const CHAP_RE = /\/(chapitre|chapter|capitulo|episode|ep|part)[\/-]?\d+/i;
const HOSTS = ["i.nhentai.net","i1.nhentai.net","i2.nhentai.net","i3.nhentai.net","i4.nhentai.net","i5.nhentai.net","i6.nhentai.net","i7.nhentai.net"];
const EXT = { j:"jpg", p:"png", g:"gif", w:"webp" };
const sleep = ms => new Promise(r => setTimeout(r, ms));
const sanitize = s => s.replace(/[/\\:*?"<>|]/g,"").replace(/\s+/g," ").trim();
const normUrl = u => (u||"").replace(/\/+$/,"").toLowerCase();
const log = (...a) => console.log("[CBZ]", ...a);
GM_addStyle(`
.cbz-dl-btn{margin:4px;padding:6px 14px;border:0;border-radius:4px;background:#ed2553;color:#fff;font-weight:700;cursor:pointer;font-size:13px;white-space:nowrap;display:inline-block}
.cbz-dl-btn:disabled{opacity:.75;cursor:wait}
.cbz-dl-btn.cbz-ok{background:#2e7d32}
.cbz-dl-btn.cbz-err{background:#c62828}
.cbz-dl-btn.cbz-small{position:absolute;top:2px;right:2px;z-index:5;margin:0;padding:3px 7px;font-size:11px;border-radius:3px;white-space:nowrap}
#cbz-bulk-btn{margin:6px 10px;vertical-align:middle;flex-shrink:0;white-space:nowrap;width:auto;min-width:150px}
#cbz-bulk-btn.cbz-float,#cbz-manage-btn,#cbz-stop,#cbz-chap-btn.cbz-float{position:fixed;z-index:2147483646;cursor:move;box-shadow:0 4px 12px rgba(0,0,0,.5)}
#cbz-bulk-btn.cbz-float{bottom:90px;right:20px;margin:0}
#cbz-chap-btn.cbz-float{bottom:140px;right:20px;margin:0}
#cbz-manage-btn{bottom:190px;right:20px;margin:0;background:#37474f}
#cbz-stop{bottom:20px;left:20px;padding:10px 18px;background:#c62828;color:#fff;border:0;border-radius:6px;font-weight:700;font-size:13px}
#cbz-panel{position:fixed;top:20vh;right:0;z-index:2147483647;width:300px;background:rgba(0,0,0,.92);color:#fff;font-size:11px;border-radius:6px 0 0 6px;padding:10px 12px;font-family:sans-serif}
#cbz-panel .cbz-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:6px;font-weight:700;font-size:12px}
#cbz-panel .cbz-row{display:flex;align-items:center;gap:6px;margin:5px 0}
#cbz-panel .cbz-lbl{width:78px;font-size:10px;opacity:.9}
#cbz-panel .cbz-bar{flex:1;height:8px;background:#333;border-radius:4px;overflow:hidden}
#cbz-panel .cbz-bar-fill{height:100%;width:0%;background:#ed2553;transition:width .15s}
#cbz-panel .cbz-bar-fill.zip{background:#66bb6a}
#cbz-panel .cbz-bar-fill.cbz{background:#26c6da}
#cbz-panel .cbz-val{width:52px;text-align:right;font-size:10px}
#cbz-toasts{position:fixed;right:16px;bottom:16px;z-index:2147483647;display:flex;flex-direction:column;gap:8px;max-width:340px}
.cbz-toast{padding:10px 14px;border-radius:6px;color:#fff;font-size:13px;font-family:sans-serif;box-shadow:0 2px 10px rgba(0,0,0,.5)}
.cbz-toast--success{background:#2e7d32}.cbz-toast--error{background:#c62828}.cbz-toast--info{background:#37474f}.cbz-toast--warning{background:#f59e0b}
#cbz-lib-dialog{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:sans-serif}
#cbz-lib-dialog .cbz-lib-box{background:#1c1c1c;color:#fff;border-radius:8px;padding:16px 20px;max-width:520px;width:calc(100% - 40px);max-height:80vh;display:flex;flex-direction:column}
#cbz-lib-dialog .cbz-lib-list{overflow:auto;background:#111;border-radius:4px;padding:8px;flex:1;min-height:120px}
#cbz-lib-dialog .cbz-lib-row{display:flex;align-items:center;gap:8px;padding:4px 2px;border-bottom:1px solid #222;font-size:12px}
#cbz-lib-dialog .cbz-lib-name{flex:1;word-break:break-all}
#cbz-lib-dialog .cbz-lib-del{background:#c62828;border:0;color:#fff;border-radius:3px;padding:2px 8px;cursor:pointer;font-size:11px}
#cbz-lib-dialog .cbz-lib-actions{display:flex;gap:8px;margin-top:10px}
#cbz-lib-dialog .cbz-lib-actions button{border:0;border-radius:4px;padding:8px 12px;font-weight:700;cursor:pointer;color:#fff}
#cbz-lib-clear{background:#c62828}#cbz-lib-close{background:#555}
`);
const LIB_KEY = "cbz_library";
let library = {};
try { library = JSON.parse(GM_getValue(LIB_KEY,"{}")) || {}; } catch(e) {}
const saveLibrary = () => GM_setValue(LIB_KEY, JSON.stringify(library));
const libGet = n => library[n];
const libSet = (n,e) => { library[n] = e; saveLibrary(); updateManageCount(); };
function updateManageCount(){ const b=document.getElementById("cbz-manage-btn"); if(b) b.textContent="🗑 CBZ ("+Object.keys(library).length+")"; }
let toastBox=null;
function showToast(type,text,timeout){
if(!toastBox){toastBox=document.createElement("div");toastBox.id="cbz-toasts";document.body.appendChild(toastBox);}
const t=document.createElement("div");t.className="cbz-toast cbz-toast--"+type;t.textContent=text;toastBox.appendChild(t);
setTimeout(()=>t.remove(),timeout||5000);
}
let panel=null;
function showPanel(title){
hidePanel(0);
panel=document.createElement("div");panel.id="cbz-panel";
panel.innerHTML='<div class="cbz-title"></div>'
+row("img","Image en cours")+row("files","Fichiers")+row("zip","Compression")+row("cbz","Conversion .cbz");
panel.querySelector(".cbz-title").textContent=title;
document.body.appendChild(panel);
function row(id,lbl){return '<div class="cbz-row"><span class="cbz-lbl">'+lbl+'</span><div class="cbz-bar"><div class="cbz-bar-fill '+(id==="zip"?"zip":id==="cbz"?"cbz":"")+'" id="bar-'+id+'"></div></div><span class="cbz-val" id="val-'+id+'"></span></div>';}
}
function setBar(id,pct,val){
const b=document.getElementById("bar-"+id),v=document.getElementById("val-"+id);
if(b)b.style.width=Math.max(0,Math.min(100,pct))+"%";
if(v)v.textContent=val||"";
}
function hidePanel(d=0){ if(!panel)return; const p=panel;panel=null; if(d<=0)p.remove();else setTimeout(()=>p.remove(),d); }
function makeDraggable(el,id){
let sx,sy,ox,oy,drag=false,moved=false;
const saved=localStorage.getItem("cbz_pos_"+id);
if(saved){try{const p=JSON.parse(saved);el.style.left=p.x+"px";el.style.top=p.y+"px";el.style.right="auto";el.style.bottom="auto";}catch(e){}}
el.addEventListener("pointerdown",e=>{drag=true;moved=false;sx=e.clientX;sy=e.clientY;const r=el.getBoundingClientRect();ox=r.left;oy=r.top;});
el.addEventListener("pointermove",e=>{
if(!drag)return;
const dx=e.clientX-sx,dy=e.clientY-sy;
if(Math.abs(dx)>4||Math.abs(dy)>4)moved=true;
if(moved){el.style.left=(ox+dx)+"px";el.style.top=(oy+dy)+"px";el.style.right="auto";el.style.bottom="auto";}
});
el.addEventListener("pointerup",()=>{drag=false;if(moved){el._dragged=true;const r=el.getBoundingClientRect();localStorage.setItem("cbz_pos_"+id,JSON.stringify({x:r.left,y:r.top}));}});
}
const swallowIfDragged=(el,fn)=>e=>{ if(el._dragged){el._dragged=false;e.preventDefault();e.stopPropagation();return;} fn(e); };
function req(url,type="text",headers={}){
return new Promise((res,rej)=>{GM_xmlhttpRequest({method:"GET",url,responseType:type,timeout:30000,headers,
onload:r=>{(r.status>=200&&r.status<300)?res(r.response):rej(new Error("HTTP "+r.status+" : "+url));},
onerror:()=>rej(new Error("Erreur réseau")),ontimeout:()=>rej(new Error("Timeout"))});});
}
function reqProg(url,onprog,headers={}){
return new Promise((res,rej)=>{GM_xmlhttpRequest({method:"GET",url,responseType:"arraybuffer",timeout:30000,headers,
onprogress:e=>{if(onprog&&e.lengthComputable&&e.total)onprog(Math.round(e.loaded/e.total*100));},
onload:r=>{(r.status>=200&&r.status<300)?res(r.response):rej(new Error("HTTP "+r.status));},
onerror:()=>rej(new Error("net")),ontimeout:()=>rej(new Error("timeout"))});});
}
const getJSON=u=>req(u).then(t=>JSON.parse(t));
let CRC_TABLE=null;
function getCrcTable(){if(CRC_TABLE)return CRC_TABLE;const T=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?(0xEDB88320^(c>>>1)):(c>>>1);T[n]=c;}CRC_TABLE=T;return T;}
function crc32(u8){const T=getCrcTable();let c=0xFFFFFFFF;for(let i=0;i<u8.length;i++)c=T[(c^u8[i])&255]^(c>>>8);return(c^0xFFFFFFFF)>>>0;}
const nameBytes=n=>new TextEncoder().encode(n);
async function compress(files,onZip){
const parts=[],central=[];let offset=0;
for(let i=0;i<files.length;i++){
const f=files[i],u8=new Uint8Array(f.data),nb=nameBytes(f.name),crc=crc32(u8);
const lh=new DataView(new ArrayBuffer(30));
lh.setUint32(0,0x04034b50,true);lh.setUint16(4,20,true);lh.setUint16(8,0,true);lh.setUint32(14,crc,true);lh.setUint32(18,u8.length,true);lh.setUint32(22,u8.length,true);lh.setUint16(26,nb.length,true);
parts.push(new Uint8Array(lh.buffer),nb,u8);
const ch=new DataView(new ArrayBuffer(46));
ch.setUint32(0,0x02014b50,true);ch.setUint16(4,20,true);ch.setUint16(6,20,true);ch.setUint32(16,crc,true);ch.setUint32(20,u8.length,true);ch.setUint32(24,u8.length,true);ch.setUint16(28,nb.length,true);ch.setUint32(42,offset,true);
central.push(new Uint8Array(ch.buffer),nb);
offset+=30+nb.length+u8.length;
if(onZip)onZip(Math.round((i/files.length)*100));
if(i%3===0)await sleep(0);
}
let cs=0;for(const c of central)cs+=c.length;
const eo=new DataView(new ArrayBuffer(22));
eo.setUint32(0,0x06054b50,true);eo.setUint16(8,files.length,true);eo.setUint16(10,files.length,true);eo.setUint32(12,cs,true);eo.setUint32(16,offset,true);
parts.push(...central,new Uint8Array(eo.buffer));
if(onZip)onZip(100);
return new Blob(parts,{type:"application/zip"});
}
function saveBlob(name,blob){const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();setTimeout(()=>{URL.revokeObjectURL(a.href);a.remove();},5000);}
function makeBtn(txt){const b=document.createElement("button");b.className="cbz-dl-btn";b.textContent=txt;return b;}
async function downloadAndSave(urls,filename){
setBar("img",0,"");setBar("files",0,"0/"+urls.length);setBar("zip",0,"");setBar("cbz",0,"");
const results=[];
for(let i=0;i<urls.length;i++){
const u=urls[i];let data=null;
try{data=await reqProg(u,p=>setBar("img",p,p+"%"));}catch(e){}
if(!data){try{data=await reqProg(u,p=>setBar("img",p,p+"%"),{Referer:location.origin});}catch(e){}}
if(!data)throw new Error("Échec image "+(i+1));
const ext=(u.match(/\.(png|jpe?g|webp|gif)$/i)||[,"jpg"])[1].replace("jpeg","jpg");
results.push({name:String(i+1).padStart(3,"0")+"."+ext,data});
setBar("files",Math.round(((i+1)/urls.length)*100),(i+1)+"/"+urls.length);
setBar("img",0,"");
await sleep(120);
}
const blob=await compress(results,p=>setBar("zip",p,p+"%"));
setBar("cbz",40,"…");
saveBlob(filename,blob);
libSet(filename,{p:urls.length,s:blob.size,g:location.href});
setBar("cbz",100,"✓");
return blob;
}
function waitForLiveImages(grab,timeout=20000){
return new Promise(res=>{
const init=grab();if(init.length)return res(init);
const start=Date.now();let sc=0;
const check=()=>{const u=grab();if(u.length)return res(u);if(Date.now()-start>timeout||sc>12)return res(u);window.scrollBy(0,Math.max(600,window.innerHeight*0.8));sc++;setTimeout(check,700);};
setTimeout(check,700);
});
}
const chapFile=(t,l)=>{const a=sanitize(t||""),b=sanitize(l||"");return (a? a+" - ":"")+b+".cbz";};
const chapNum=l=>parseInt((l.match(/(\d+)/)||[])[1]||"0",10);
const labelFromUrl=u=>{const m=u.match(/\/(chapitre|chapter|capitulo|episode|ep|part)[\/-]?(\d+)/i);return m?"Chapitre "+m[2]:u;};
function stopAuto(){GM_setValue("auto_mode","");GM_setValue("auto_queue","[]");GM_setValue("auto_last",0);const s=document.getElementById("cbz-stop");if(s)s.remove();}
function liveIsChapterPage(){ return CHAP_RE.test(location.href) || /\/\d+\/?$/.test(location.pathname); }
function liveLabel(){
let m=location.href.match(/\/(?:chapitre|chapter|capitulo|episode|ep|part)[\/-]?(\d+)/i);
if(m) return "Chapitre "+m[1];
m=location.pathname.match(/\/(\d+)\/?$/);
if(m) return "Chapitre "+m[1];
return labelFromUrl(location.href);
}
const CAT_WORDS=/^(home|accueil|manhwa|manga|comic|comics|series|séries|pornhwa|webtoon)$/i;
function stripSite(t){return (t||"").replace(/\s*[-–|]\s*(read manhwa|hd[^-–|]*comics|.*porn\s*comics|.*\bcomics\b|.*\.com).*$/i,"").trim();}
function cleanChapter(t){return (t||"").replace(/[-–|]?\s*(chapter|chapitre|episode|ep|part)\s*\d+.*$/i,"").trim();}
function prettifySlug(s){return s.replace(/[-_]/g," ").replace(/\b\w/g,c=>c.toUpperCase()).trim();}
function slugTitle(){
const base=location.pathname.replace(/\/(chapter|chapitre|episode|ep|part)[\/-]?\d+\/?$/i,"").replace(/\/$/,"");
const m=base.match(/\/([^\/]+)\/?$/);
return m?prettifySlug(m[1]):"";
}
function bcTitle(){
const bc=document.querySelector('[class*="breadcrumb"], .td-breadcrumb, nav[aria-label*="readcrumb"], #breadcrumb');
if(!bc)return null;
const items=[...bc.querySelectorAll("a,span,li")].map(x=>x.textContent.trim()).filter(t=>t&&!/^(home|accueil)$/i.test(t));
for(let i=items.length-1;i>=0;i--){
if(/^\d+$/.test(items[i]) && items[i-1] && !CAT_WORDS.test(items[i-1])) return items[i-1];
}
return null;
}
async function getSmartTitle(){
const b=bcTitle(); if(b){const t=stripSite(b); if(t){log("titre via breadcrumb:",t);return t;}}
const t=slugTitle()||"Manhwa";
log("titre via slug:",t);
return t;
}
function liveTitle(){
const h=document.querySelector("h1")?.textContent;
if(h && !/chapter|chapitre/i.test(h)) return h.trim();
return cleanChapter(document.title)||document.title.split(/[|–-]/)[0].trim();
}
async function hdpcImagesXHR(url){
const html=await req(url,"text");
const re=/https?:\/\/[^"'\s<>\\]+?\.(?:jpe?g|png|webp|gif)(?:\?[^"'\s<>\\]*)?/gi;
const seen=new Set();const urls=[];let m;
while((m=re.exec(html))!==null){
let s=m[0].replace(/&/g,"&");
if(/logo|icon|avatar|gravatar|emoji|favicon|banner|\/ads?\/|sprite|placeholder|blank|\.svg/i.test(s))continue;
if(!seen.has(s)){seen.add(s);urls.push(s);}
}
log("hdpc images trouvées:",urls.length);
return urls;
}
function xmangaGrab(){return[...new Set([...document.querySelectorAll("img")].map(i=>i.dataset.src||i.src).filter(u=>/reader\.hentai\.gifts\//.test(u)))];}
async function getLiveChapterImages(){
if(IS_HDPC) return hdpcImagesXHR(location.href);
return waitForLiveImages(xmangaGrab);
}
function openLibraryManager(){
const old=document.getElementById("cbz-lib-dialog");if(old)old.remove();
const box=document.createElement("div");box.id="cbz-lib-dialog";
box.innerHTML='<div class="cbz-lib-box"><div style="font-weight:700;margin-bottom:8px">Bibliothèque CBZ (<span class="cbz-lib-count"></span>)</div><div class="cbz-lib-list"></div><div class="cbz-lib-actions"><button id="cbz-lib-clear">Tout effacer</button><button id="cbz-lib-close">Fermer</button></div></div>';
document.body.appendChild(box);
const list=box.querySelector(".cbz-lib-list"),count=box.querySelector(".cbz-lib-count");
function render(){const names=Object.keys(library).sort();count.textContent=names.length;list.innerHTML="";if(!names.length){list.innerHTML='<div style="opacity:.7;font-size:12px">Aucune entrée.</div>';return;}names.forEach(n=>{const row=document.createElement("div");row.className="cbz-lib-row";const nm=document.createElement("div");nm.className="cbz-lib-name";nm.textContent=n;const del=document.createElement("button");del.className="cbz-lib-del";del.textContent="✕";del.onclick=()=>{delete library[n];saveLibrary();render();updateManageCount();};row.appendChild(nm);row.appendChild(del);list.appendChild(row);});}
render();
box.querySelector("#cbz-lib-clear").onclick=()=>{library={};saveLibrary();render();updateManageCount();showToast("info","Bibliothèque effacée");};
box.querySelector("#cbz-lib-close").onclick=()=>box.remove();
box.addEventListener("click",e=>{if(e.target===box)box.remove();});
}
function initManageButton(){
if(document.getElementById("cbz-manage-btn"))return;
const b=makeBtn("🗑 CBZ");b.id="cbz-manage-btn";
b.onclick=swallowIfDragged(b,()=>openLibraryManager());
document.body.appendChild(b);makeDraggable(b,"manage");updateManageCount();
}
/* ================= NHENTAI ================= */
const getLangFromTags=t=>{const x=(t||[]).find(y=>y.type==="language"&&y.name!=="translated");return x?x.name:undefined;};
async function getGallery(gid){
try{const d=await getJSON("https://nhentai.net/api/v2/galleries/"+gid);if(d&&d.media_id&&Array.isArray(d.pages))return{mid:String(d.media_id),title:(d.title&&(d.title.english||d.title.japanese||d.title.pretty))||("gallery-"+gid),lang:getLangFromTags(d.tags),pages:d.pages.map((p,i)=>({i:i+1,ext:(String(p.path).match(/\.(\w+)$/)||[])[1]||"jpg"}))};}catch(e){}
try{const d=await getJSON("https://nhentai.net/api/gallery/"+gid);if(d&&d.media_id&&d.images&&d.images.pages)return{mid:String(d.media_id),title:(d.title&&(d.title.english||d.title.japanese))||("gallery-"+gid),lang:getLangFromTags(d.tags),pages:d.images.pages.map((p,i)=>({i:i+1,ext:EXT[p.t]||"jpg"}))};}catch(e){}
throw new Error("Infos introuvables : "+gid);
}
async function fetchPageImage(mid,page,onImg){
for(const h of HOSTS){try{return{data:await reqProg("https://"+h+"/galleries/"+mid+"/"+page.i+"."+page.ext,onImg),ext:page.ext};}catch(e){}}
throw new Error("Échec page "+page.i);
}
async function runNhentai(gid,btn){
if(btn.dataset.busy)return;btn.dataset.busy="1";btn.disabled=true;
try{
const g=await getGallery(gid);showPanel(g.title);
setBar("img",0,"");setBar("files",0,"0/"+g.pages.length);setBar("zip",0,"");setBar("cbz",0,"");
const results=new Array(g.pages.length),queue=[...g.pages];let done=0;
const worker=async()=>{while(queue.length){const p=queue.shift();const img=await fetchPageImage(g.mid,p,pc=>setBar("img",pc,pc+"%"));results[p.i-1]={name:String(p.i).padStart(3,"0")+"."+img.ext,data:img.data};done++;setBar("files",Math.round(done/g.pages.length*100),done+"/"+g.pages.length);setBar("img",0,"");}};
await Promise.all(Array.from({length:4},worker));
const blob=await compress(results.filter(Boolean),p=>setBar("zip",p,p+"%"));
const filename=(sanitize(g.title)||("gallery-"+gid))+".cbz";
setBar("cbz",40,"…");saveBlob(filename,blob);libSet(filename,{p:g.pages.length,s:blob.size,g:gid});setBar("cbz",100,"✓");
btn.textContent="✓ Terminé";btn.classList.add("cbz-ok");showToast("success","✓ "+filename);hidePanel(3000);
}catch(e){btn.textContent="✗ "+e.message;btn.classList.add("cbz-err");showToast("error","✗ "+e.message);hidePanel(4000);}
finally{btn.disabled=false;delete btn.dataset.busy;}
}
function initNhentai(){
const gid=(location.pathname.match(/\/g\/(\d+)/)||[])[1];
const box=document.querySelector("#info .buttons")||document.querySelector("#info");
if(gid&&box&&!box.querySelector("[data-cbz-detail]")){const b=makeBtn("Download .cbz");b.dataset.cbzDetail="1";box.appendChild(b);b.onclick=()=>runNhentai(gid,b);}
document.querySelectorAll(".gallery").forEach(g=>{if(g.dataset.cbzInit)return;const a=g.querySelector("a.cover")||g.querySelector("a");if(!a)return;const id=((a.getAttribute("href")||"").match(/\/g\/(\d+)/)||[])[1];if(!id)return;g.dataset.cbzInit="1";const b=makeBtn("⬇ CBZ");b.classList.add("cbz-small");g.appendChild(b);b.onclick=e=>{e.preventDefault();e.stopPropagation();runNhentai(id,b);};});
let t=null;new MutationObserver(()=>{clearTimeout(t);t=setTimeout(initNhentai,200);}).observe(document.body,{childList:true,subtree:true});
}
/* ================= ORTEGA ================= */
let ortegaBulk=false;
const makeOrtegaFilename=(t,n)=>chapFile(t,"Chapitre "+n);
function ortegaTitle(){const bc=document.querySelector('nav[aria-label*="readcrumb"], .breadcrumb, [class*="breadcrumb"]');if(bc){const it=[...bc.querySelectorAll("a,span,li")].map(x=>x.textContent.trim()).filter(Boolean);if(it.length>=2)return it[it.length-2]||it[it.length-1];}const h=document.querySelector("h1");if(h&&!/chapitre/i.test(h.textContent))return h.textContent.trim();return document.title.split(/[|–-]/)[0].trim();}
function parseOrtegaRSC(html){const re=/self\.__next_f\.push\(\[\d+,"([\s\S]*?)"\]\)/g;let m;while((m=re.exec(html))!==null){let d;try{d=m[1].replace(/\\n/g,"\n").replace(/\\"/g,'"').replace(/\\\\/g,"\\");}catch(e){continue;}if(d.includes('"chapters"')&&d.includes('"slug"')){const cm=d.match(/"chapters":\s*\[([\s\S]*?)\]/);if(cm){try{return JSON.parse("["+cm[1]+"]");}catch(e){}}}}return[];}
async function ortegaImages(url){const html=await req(url,"text");const re=/<img[^>]*src="([^"]*\/api\/chapters\/[^"]*\/image\/[^"]*)"[^>]*>/gi;const u=[];let m;while((m=re.exec(html))!==null){let s=m[1];if(!s.startsWith("http"))s=location.origin+s;u.push(s);}return u;}
async function bulkOrtega(){
if(ortegaBulk)return;ortegaBulk=true;let stop=false;
showPanel("Téléchargement groupé");setBar("files",0,"…");
const title=ortegaTitle(),slug=location.pathname.match(/\/serie\/([^\/\?]+)/)?.[1];
const chaps=parseOrtegaRSC(document.documentElement.outerHTML).map(c=>({number:c.number,url:location.origin+"/serie/"+slug+"/chapter/"+c.number,filename:makeOrtegaFilename(title,c.number)})).sort((a,b)=>a.number-b.number);
const rem=chaps.filter(c=>!libGet(c.filename));
if(!rem.length){showToast("info","Déjà tout téléchargé (🗑 pour effacer)");ortegaBulk=false;hidePanel(2000);return;}
const sb=document.createElement("button");sb.id="cbz-stop";sb.textContent="⏹ STOP";sb.onclick=()=>{stop=true;};document.body.appendChild(sb);makeDraggable(sb,"stop");
let done=0,failed=0;
for(let i=0;i<rem.length;i++){if(stop)break;const c=rem[i];
try{const imgs=await ortegaImages(c.url);if(!imgs.length){failed++;continue;}await downloadAndSave(imgs,c.filename);done++;showToast("success","✓ Chapitre "+c.number,1200);}catch(e){failed++;}
await sleep(400);}
sb.remove();ortegaBulk=false;showToast("success","Terminé : "+done+" ✓"+(failed?" , "+failed+" ✗":""),8000);hidePanel(3000);
}
async function singleOrtega(){
const m=location.pathname.match(/\/serie\/([^\/]+)\/chapter\/(\d+)/);if(!m)return;
const num=parseInt(m[2],10),btn=document.getElementById("cbz-chap-btn");if(btn){btn.disabled=true;btn.textContent="…";}
const filename=makeOrtegaFilename(ortegaTitle(),num);
showPanel(ortegaTitle());setBar("files",0,"…");
try{const imgs=await ortegaImages(location.href);if(!imgs.length)throw new Error("Aucune image");await downloadAndSave(imgs,filename);showToast("success","✓ "+filename);if(btn){btn.disabled=false;btn.textContent="✓ Téléchargé";btn.classList.add("cbz-ok");}hidePanel(3000);}
catch(e){showToast("error","Échec : "+e.message);if(btn){btn.disabled=false;btn.textContent="✗ Erreur";btn.classList.add("cbz-err");}hidePanel(3000);}
}
function initOrtega(){
setTimeout(()=>{try{
if(/\/serie\/[^\/]+\/?$/.test(location.pathname)&&!document.getElementById("cbz-bulk-btn")){const b=makeBtn("⬇ Tous les chapitres");b.id="cbz-bulk-btn";b.classList.add("cbz-float");document.body.appendChild(b);makeDraggable(b,"bulk");b.onclick=swallowIfDragged(b,()=>bulkOrtega());}
if(/\/serie\/[^\/]+\/chapter\/\d+/.test(location.pathname)&&!document.getElementById("cbz-chap-btn")){const b=makeBtn("⬇ Ce chapitre en CBZ");b.id="cbz-chap-btn";b.classList.add("cbz-float");document.body.appendChild(b);makeDraggable(b,"chap");b.onclick=swallowIfDragged(b,()=>singleOrtega());}
}catch(e){}},1500);
}
/* ================= SITES LIVE ================= */
async function getLiveSeries(){
const title = IS_XMANGA ? liveTitle() : await getSmartTitle();
const seen=new Map();
if (IS_HDPC) {
document.querySelectorAll("a").forEach(a=>{
const href=a.getAttribute("href")||"";let abs;try{abs=new URL(href,location.origin).href;}catch(e){return;}
if(CHAP_RE.test(abs)&&abs!==location.href&&!seen.has(abs))seen.set(abs,{url:abs,label:labelFromUrl(abs)});
});
} else {
document.querySelectorAll("a").forEach(a=>{if(CHAP_RE.test(a.href)&&!seen.has(a.href))seen.set(a.href,{url:a.href,label:a.textContent.trim()||labelFromUrl(a.href)});});
}
return{title,chapters:[...seen.values()]};
}
function liveStartAuto(info){
const ch=[...info.chapters].sort((a,b)=>chapNum(a.label)-chapNum(b.label));
const rem=ch.filter(c=>!libGet(chapFile(info.title,c.label)));
if(!rem.length){showToast("info","Déjà tout téléchargé (🗑 pour effacer)");return;}
GM_setValue("auto_mode","live");GM_setValue("auto_title",info.title);GM_setValue("auto_queue",JSON.stringify(rem.map(c=>({url:c.url,label:c.label}))));GM_setValue("auto_last",Date.now());
showToast("info","Démarrage auto ("+rem.length+")");location.href=rem[0].url;
}
async function liveAutoStep(){
GM_setValue("auto_last",Date.now());
let q=JSON.parse(GM_getValue("auto_queue","[]"));const title=GM_getValue("auto_title","série");
if(!q.length){stopAuto();showToast("success","Lot terminé");return;}
if(!document.getElementById("cbz-stop")){const s=document.createElement("button");s.id="cbz-stop";s.textContent="⏹ STOP auto";s.onclick=()=>{stopAuto();hidePanel();};document.body.appendChild(s);makeDraggable(s,"stop");}
const cur=normUrl(location.href);let idx=q.findIndex(x=>normUrl(x.url)===cur);const cur0=idx>=0?q[idx]:q[0];
showPanel("Auto : "+title);
try{const urls=await getLiveChapterImages();if(!urls.length)throw new Error("Aucune image");await downloadAndSave(urls,chapFile(title,cur0.label));showToast("success","✓ "+cur0.label,1500);}catch(e){showToast("error","✗ "+cur0.label,2000);}
if(idx>=0)q.splice(idx,1);else q.shift();
GM_setValue("auto_queue",JSON.stringify(q));
if(q.length){await sleep(1500);location.href=q[0].url;}else{stopAuto();hidePanel();showToast("success","Lot terminé",8000);}
}
async function addLiveButtons(){
const isChap=liveIsChapterPage();
if(isChap){
if(document.getElementById("cbz-chap-btn"))return;
const label=liveLabel();
const b=makeBtn("⬇ Ce chapitre en CBZ");b.id="cbz-chap-btn";b.classList.add("cbz-float");
document.body.appendChild(b);makeDraggable(b,"chap");
b.onclick=swallowIfDragged(b,async()=>{
b.disabled=true;b.textContent="…";
let title = IS_XMANGA ? liveTitle() : await getSmartTitle();
if(!title) title = slugTitle() || "Manhwa";
log("titre utilisé:",title);
showPanel(title);
try{const urls=await getLiveChapterImages();if(!urls.length)throw new Error("Aucune image");await downloadAndSave(urls,chapFile(title,label));showToast("success","✓ "+chapFile(title,label));b.disabled=false;b.textContent="✓ Téléchargé";b.classList.add("cbz-ok");hidePanel(3000);}
catch(e){b.disabled=false;b.textContent="✗ Erreur";showToast("error",e.message);hidePanel(3000);}
});
} else {
if(document.getElementById("cbz-bulk-btn"))return;
const b=makeBtn("⬇ Tous les chapitres (auto)");b.id="cbz-bulk-btn";b.classList.add("cbz-float");
document.body.appendChild(b);makeDraggable(b,"bulk");
b.onclick=swallowIfDragged(b,async()=>liveStartAuto(await getLiveSeries()));
}
}
function initLive(){
if(GM_getValue("auto_mode")==="live"&&liveIsChapterPage()){
const last=GM_getValue("auto_last",0);
if(Date.now()-last<120000){ if(!window.__cbzAutoStarted){window.__cbzAutoStarted=true;liveAutoStep();} }
else stopAuto();
return;
}
addLiveButtons();
let t=null;
new MutationObserver(()=>{clearTimeout(t);t=setTimeout(addLiveButtons,400);}).observe(document.body,{childList:true,subtree:true});
}
log("Script v0.5.13 chargé sur:",location.href);
initManageButton();
if(IS_NHENTAI)initNhentai();
else if(IS_ORTEGA)initOrtega();
else if(IS_LIVE)initLive();
})();