Mangadex API v5 reader

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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