CBZ Downloader (Multi-site)

Télécharge nhentai, x-manga, ortegascans et hdporncomics en .cbz

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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(/&amp;/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();
})();