Mangadex API v5 reader

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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