Greasy Fork

Mangadex API v5 reader

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

目前为 2021-06-06 提交的版本。查看 最新版本

// ==UserScript==
// @name        Mangadex API v5 reader
// @namespace   Violentmonkey Scripts
// @match       https://*.mangadex.org/*
// @grant       none
// @run-at document-start
// @version     1.11
// @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 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;
    }
    #show-follows {
      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);
  });
});