// ==UserScript== // @name Mangadex API v5 reader // @namespace Violentmonkey Scripts // @match https://*.mangadex.org/* // @grant none // @run-at document-start // @version 1.12 // @author - // @description 5/8/2021, 10:46:50 AM // @downloadURL https://update.greasyfork.icu/scripts/426166/Mangadex%20API%20v5%20reader.user.js // @updateURL https://update.greasyfork.icu/scripts/426166/Mangadex%20API%20v5%20reader.meta.js // ==/UserScript== if(location.href.startsWith("https://api.mangadex.org")) window.addEventListener('beforescriptexecute', function(e) { e.stopPropagation(); e.preventDefault(); e.target.remove(); }, true) window.addEventListener("load",()=>{ window.sessionToken = undefined; const login = async(username,password)=>{ let response = await fetch("https://api.mangadex.org/auth/login",{ method:"POST", body:JSON.stringify({ username, password }), headers: { "Content-Type": "application/json" } }); let data = await response.json().catch(e=>alert(`/auth/login failed: ${response.status}`)); if(data.result==="ko") alert("wrong password!"); window.sessionToken = data.token.session; }; const sleep = (delay)=>new Promise((resolve)=>setTimeout(resolve,delay)); let last_request = performance.now(); let cache = new Map(); window.fetchJSON = async(url)=>{ if(cache.has(url)) return cache.get(url); let resolve; let promise = new Promise((r)=>{resolve = r;}) promise.resolve = resolve; cache.set(url,promise); // rate limit while(true) { let diff = performance.now()-last_request; let target = diff - 250; if(target>=0) break; await sleep(-target); } last_request = performance.now(); let headers = {}; if(sessionToken) headers.Authorization = sessionToken; let options = { mode: location.href.startsWith("https://api.mangadex.org")?"same-origin":"cors", headers }; let response = await fetch(url,options); try{ let data = await response.json(); promise.resolve(data); return data; } catch(e) { promise.resolve(null); return promise; } }; async function* listIterator(urlstring, params = {}, limit=100) { let i=0; params.limit = limit; while(true) { let url = new URL(urlstring); params.offset = i; url.search = new URLSearchParams(params); let result = await fetchJSON(url.toString()); if(result===null || result.results.length === 0 || i > result.total) return; for(let e of result.results) { yield e; } i+=limit; if(i > result.total) return; } } let getImageUrlsFromChapterID = async(chapterID)=>{ let chapter = await fetchJSON(`https://api.mangadex.org/chapter/${chapterID}`); let server = await fetchJSON(`https://api.mangadex.org/at-home/server/${chapterID}`); console.log(chapter) let urls = chapter.data.attributes.dataSaver.map(s=>`${server.baseUrl}/data-saver/${chapter.data.attributes.hash}/${s}`) return urls; }; let getChaptersFromMangaID = async(mangaID)=>{ let chapters = []; for await (let result of listIterator(`https://api.mangadex.org/manga/${mangaID}/feed`,{"translatedLanguage[]":["en"]})) { chapters.push(result) } let string = (c)=>(c.data.attributes.volume||"0")+"."+(c.data.attributes.chapter||"0"); return chapters.sort((a,b)=>string(b).localeCompare(string(a), undefined, {numeric: true})); }; let getGroupNamesFromChapter = async(chapter)=>{ let groupIDs = chapter.relationships.filter(e=>e.type==="scanlation_group").map(e=>e.id); let groups = await Promise.all(groupIDs.map(id=>fetchJSON(`https://api.mangadex.org/group/${id}`))); return groups.map(g=>g.data.attributes.name); }; let search = async(title)=>{ let matches = []; for await (let result of listIterator("https://api.mangadex.org/manga",{title})) { let resultTitle = result.data.attributes.title.en; if(resultTitle.toLowerCase().indexOf(title.toLowerCase())>=0) matches.push(result); } return matches; }; const displayChapter = async(chapter)=>{ searchResults.innerHTML = ""; let imageIDs = await getImageUrlsFromChapterID(chapter.data.id); let mangaID = chapter.relationships.filter(e=>e.type==="manga").map(e=>e.id)[0]; const links = ()=>{ let manga = document.createElement("div"); manga.innerText = "manga"; manga.addEventListener("click", ()=>{ displayManga(mangaID); }); searchResults.appendChild(manga); let chapterNumber = document.createElement("div"); chapterNumber.innerText = " chapter: "+chapter.data.attributes.chapter; searchResults.appendChild(chapterNumber); }; links(); for(let e of imageIDs) { let image = document.createElement("img"); image.src = e; searchResults.appendChild(image); } links(); }; const displayManga = async(id)=>{ searchResults.innerHTML = ""; let uploadMenu = document.createElement("div"); uploadMenu.innerHTML = ` group ids(seperated by " " or ","): files(images not archives/zip files!): volume: chapter: title: language: ` searchResults.appendChild(uploadMenu); uploadMenu.querySelector("#upload-submit").addEventListener("click",async()=>{ let username = document.querySelector("#username").value; let password = document.querySelector("#password").value; await login(username,password); if(!sessionToken) { alert("not logged in"); return; } let oldUpload = await fetchJSON("https://api.mangadex.org/upload",{ }); if(oldUpload.result !== "error") { console.log(oldUpload); if(!confirm("There is an old upload session.\n Do you want to continue (will cancel the old session)?")) return; await fetch(`https://api.mangadex.org/upload/${oldUpload.data.id}`,{ method: "DELETE", mode: location.href.startsWith("https://api.mangadex.org")?"same-origin":"cors", headers: { Authorization: sessionToken } }); } let headers = { "Content-Type": "application/json" }; headers.Authorization = sessionToken; let uploadResponse = await fetch("https://api.mangadex.org/upload/begin",{ method: "POST", body:JSON.stringify({ manga: id, groups: uploadMenu.querySelector("#upload-groups").value.split(/[\s,]+/) }), mode: location.href.startsWith("https://api.mangadex.org")?"same-origin":"cors", headers }); let upload = await uploadResponse.json(); console.log(upload); let files = [...uploadMenu.querySelector("#upload-files").files].sort((a,b)=>a.name.localeCompare(b.name,"en", {numeric: true})); console.log(files); //files.push(new File([Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,1,194,0,0,0,42,2,3,0,0,0,93,22,179,141,0,0,0,4,103,65,77,65,0,0,177,143,11,252,97,5,0,0,0,32,99,72,82,77,0,0,122,38,0,0,128,132,0,0,250,0,0,0,128,232,0,0,117,48,0,0,234,96,0,0,58,152,0,0,23,112,156,186,81,60,0,0,0,9,80,76,84,69,0,0,0,255,0,0,255,255,255,103,25,100,30,0,0,0,1,98,75,71,68,2,102,11,124,100,0,0,0,9,112,72,89,115,0,0,0,96,0,0,0,96,0,240,107,66,207,0,0,0,7,116,73,77,69,7,229,7,8,2,22,32,217,187,149,31,0,0,0,16,99,97,78,118,0,0,2,128,0,0,1,224,0,0,0,118,0,0,0,140,85,90,246,167,0,0,1,173,73,68,65,84,88,195,237,83,65,110,196,48,8,4,137,220,125,112,254,131,37,124,119,37,231,255,95,233,64,146,141,35,101,87,213,158,122,240,52,218,218,48,102,0,99,162,137,137,137,137,137,137,137,255,2,193,167,177,202,231,118,119,232,141,115,97,211,193,24,63,250,162,218,197,92,94,103,225,181,49,64,130,49,221,66,248,127,225,71,69,29,50,121,86,124,81,83,57,227,195,91,70,69,236,15,218,168,72,252,182,198,79,138,53,95,149,240,65,231,27,195,35,175,220,97,212,218,132,27,214,248,144,66,94,90,37,250,225,86,119,35,146,226,182,177,194,5,251,105,108,180,52,4,83,115,26,200,171,89,22,184,124,155,150,230,4,38,134,93,211,45,125,243,86,43,26,130,176,38,88,32,44,186,218,148,76,165,89,24,75,218,84,84,117,51,105,181,31,198,234,76,87,172,210,84,204,207,154,44,30,1,219,141,41,8,254,167,42,183,26,77,188,198,80,196,122,115,69,55,33,148,149,14,187,27,177,225,14,69,191,1,72,44,61,152,116,40,122,74,221,106,223,131,192,174,220,209,85,39,48,218,139,131,67,141,8,28,215,189,147,253,172,12,138,81,178,127,164,17,28,186,62,9,72,57,152,174,88,118,69,67,141,103,16,120,69,160,24,149,105,218,15,14,179,90,204,103,181,224,30,113,252,232,42,20,37,20,51,146,47,209,85,146,188,145,43,122,81,7,211,21,243,161,152,247,244,206,180,115,226,32,80,194,90,111,179,42,25,173,195,99,52,140,80,195,218,39,135,211,74,49,18,197,112,5,110,244,201,49,229,134,156,87,159,191,96,162,132,165,33,11,120,157,89,48,57,130,53,226,40,91,162,182,198,59,195,122,189,189,199,199,23,48,218,47,135,62,249,85,233,75,188,83,244,186,62,41,174,223,10,78,76,76,76,76,76,252,1,191,4,15,90,174,67,232,28,160,0,0,0,115,116,69,88,116,99,111,109,109,101,110,116,0,60,97,100,62,32,117,112,108,111,97,100,101,100,32,116,111,32,109,97,110,103,97,100,101,120,46,111,114,103,32,117,115,105,110,103,32,104,116,116,112,115,58,47,47,103,114,101,97,115,121,102,111,114,107,46,111,114,103,47,101,110,47,115,99,114,105,112,116,115,47,52,50,54,49,54,54,45,109,97,110,103,97,100,101,120,45,97,112,105,45,118,53,45,114,101,97,100,101,114,32,60,47,97,100,62,10,10,57,112,150,180,0,0,0,37,116,69,88,116,100,97,116,101,58,99,114,101,97,116,101,0,50,48,50,49,45,48,55,45,48,56,84,48,48,58,50,50,58,51,50,43,48,50,58,48,48,205,25,161,139,0,0,0,37,116,69,88,116,100,97,116,101,58,109,111,100,105,102,121,0,50,48,50,49,45,48,55,45,48,56,84,48,48,58,50,50,58,51,50,43,48,50,58,48,48,188,68,25,55,0,0,0,0,73,69,78,68,174,66,96,130]).buffer],"ad.png",{type:"image/png"})); let ids = []; for(let i=0; ie.id)); break; } await new Promise((res)=>setTimeout(res,500)); } } let result = await fetch(`https://api.mangadex.org/upload/${upload.data.id}/commit`,{ method: "POST", body: JSON.stringify({ chapterDraft: { volume: uploadMenu.querySelector("#upload-volume").value, chapter: uploadMenu.querySelector("#upload-chapter").value, title: uploadMenu.querySelector("#upload-title").value, translatedLanguage: uploadMenu.querySelector("#upload-language").value, }, pageOrder: ids }), mode: location.href.startsWith("https://api.mangadex.org")?"same-origin":"cors", headers: { "Content-Type": "application/json", Authorization: sessionToken } }); console.log(result); alert("done!") }); let chapters = await getChaptersFromMangaID(id); let reads = sessionToken?(await fetchJSON(`https://api.mangadex.org/manga/${id}/read`)).data:[]; for(let e of chapters) { let chapter = document.createElement("div"); let read = ""; if(sessionToken) read = reads.includes(e.data.id)?"[read] ":"[ ] "; getGroupNamesFromChapter(e).then((names)=>{ chapter.innerText = `${read}${e.data.attributes.volume||0}.${e.data.attributes.chapter||0} ${e.data.attributes.title} by ${names.join(" ")} `; }); chapter.addEventListener("click",()=>{ displayChapter(e); }); searchResults.appendChild(chapter); } }; const displayMangaList = async(mangaList)=>{ searchResults.innerHTML = ""; for(let e of mangaList) { let id = e.data.id; let title = e.data.attributes.title.en; let lastChapter = ""; if(e.data.attributes.lastChapter && e.data.attributes.lastChapter!=="0") lastChapter = `[last chapter: ${e.data.attributes.lastVolume||0}.${e.data.attributes.lastChapter}]`; let manga = document.createElement("div"); manga.innerText = `${title} ${lastChapter}`; manga.addEventListener("click",()=>{ displayManga(id); }); searchResults.appendChild(manga); } }; let div = document.createElement("div"); div.innerHTML = `
`; document.body.insertBefore(div,document.body.childNodes[0]) let searchResults = document.querySelector("#search-results"); let search_debounce_last_timeout; document.querySelector("#search").addEventListener("input",async(e)=>{ console.log(e.target.value); clearTimeout(search_debounce_last_timeout); search_debounce_last_timeout = setTimeout(async()=>{ let query = e.target.value; let result = await search(query); displayMangaList(result); },1000); }); document.querySelector("#show-follows").addEventListener("click",async(e)=>{ let username = document.querySelector("#username").value; let password = document.querySelector("#password").value; await login(username,password); let list = []; for await (let result of listIterator(`https://api.mangadex.org/user/follows/manga`)) { list.push(result) } displayMangaList(list); }); });