Mangadex API v5 reader

5/8/2021, 10:46:50 AM

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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
// ==/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 ","): <input type="text" id="upload-groups" value="6cfad7a7-abee-436d-b306-55cf400f8f97">
    files(images not archives/zip files!): <input type="file" multiple id="upload-files">
    volume: <input type="text" id="upload-volume">
    chapter: <input type="text" id="upload-chapter">
    title: <input type="text" id="upload-title">
    language: <input type="text" id="upload-language" value="en">
    <button id="upload-submit">upload</button>
    `
    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; i<files.length; i++) {
        console.log("uploading image:",i);
        let formData = new FormData();
        formData.append("image", files[i]);
        while(true) {
          let res = await fetch(`https://api.mangadex.org/upload/${upload.data.id}`, {
            method: "POST",
            body: formData,
            mode: location.href.startsWith("https://api.mangadex.org")?"same-origin":"cors",
            headers: {
              Authorization: sessionToken
            }
          });
          if(res.status === 200) {
            let json = await res.json();
            console.log(json.data.id,json);
            ids.push(...json.data.map(e=>e.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 = `
  <style>
    body {
      margin: 0px;
      padding: 0px;
    }
    img:not([src^="data:"]) {
      display: block;
      text-align: centre;
    }
    input {
      margin: 10px;
      padding: 5px;
      padding-left: 10px;
      color: #f79421 !important;
      font-size: 15px;
      border-radius: 25px;
      border: solid;
      border-color: #f79421;
    }
    input#search {
      width: 95%;
      margin-bottom: 15px;
    }
    #search-results {
      font-family: monospace;
      white-space: pre;
    }
    button {
      background: #f79421;
      color: white;
      font-size: 15px;
      border-radius: 25px;
      border: none;
      padding: 9px 20px;
      margin: 10px;
    }
    #search {
      color: #f79421;
      margin: 10px 20px 0px 10px;
    }
    textarea:focus, input:focus{
      outline: none;
      box-shadow: 0 0 10px #f79421;
    }
  </style>
  <input id="username" placeholder="username" type="text" name="username">
  <input id="password" placeholder="password" type="password" name="password">
  <button id="show-follows">show follows</button>
  <br/>
  <input id="search" type="search" placeholder="manga title">
  <div id="search-results"></div>
  `;
  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);
  });
});