Anilist Anime Diff View

Generated a diff view between your anime list and another user anime list. It only adds non shared entries to already present shared entries in compare page.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Anilist Anime Diff View
// @namespace    https://greasyfork.org/en/users/1544682-okabe-kiyouma
// @version      1.0.0
// @description  Generated a diff view between your anime list and another user anime list. It only adds non shared entries to already present shared entries in compare page.
// @author       Okabe Kiyouma
// @license      MIT License
// @run-at       document-end
// @connect       graphql.anilist.co
// @grant         GM_xmlhttpRequest
// @grant         GM_setValue
// @grant         GM_getValue
// @grant         GM.xmlHttpRequest
// @grant         GM.setValue
// @grant         GM.getValue
// @match        https://anilist.co/user/*/animelist/compare*
// ==/UserScript==

(async function () {
  "use strict";
  const ANILIST_API = "https://graphql.anilist.co";

  /**
   * User script manager functions.
   *
   * Provides compatibility between Tampermonkey, Greasemonkey 4+, etc...
   */
  const userScriptAPI = (() => {
    const api = {};

    if (typeof GM_xmlhttpRequest !== "undefined") {
      api.GM_xmlhttpRequest = GM_xmlhttpRequest;
    } else if (
      typeof GM !== "undefined" &&
      typeof GM.xmlHttpRequest !== "undefined"
    ) {
      api.GM_xmlhttpRequest = GM.xmlHttpRequest;
    }

    if (typeof GM_setValue !== "undefined") {
      api.GM_setValue = GM_setValue;
    } else if (
      typeof GM !== "undefined" &&
      typeof GM.setValue !== "undefined"
    ) {
      api.GM_setValue = GM.setValue;
    }

    if (typeof GM_getValue !== "undefined") {
      api.GM_getValue = GM_getValue;
    } else if (
      typeof GM !== "undefined" &&
      typeof GM.getValue !== "undefined"
    ) {
      api.GM_getValue = GM.getValue;
    }

    /** whether GM_xmlhttpRequest is supported. */
    api.supportsXHR = typeof api.GM_xmlhttpRequest !== "undefined";

    /** whether GM_setValue and GM_getValue are supported. */
    api.supportsStorage =
      typeof api.GM_getValue !== "undefined" &&
      typeof api.GM_setValue !== "undefined";

    return api;
  })();

  async function waitForElement(
    selector,
    container = document,
    timeoutSecs = 7
  ) {
    const element = container.querySelector(selector);
    if (element) {
      return Promise.resolve(element);
    }

    return new Promise((resolve, reject) => {
      const timeoutTime = Date.now() + timeoutSecs * 1000;
      const handler = () => {
        const element = document.querySelector(selector);
        if (element) {
          resolve(element);
        } else if (Date.now() > timeoutTime) {
          reject(new Error(`Timed out waiting for selector '${selector}'`));
        } else {
          setTimeout(handler, 100);
        }
      };
      setTimeout(handler, 1);
    });
  }

  async function fetchAnilist(query, variables = {}) {
    const response = await fetch(ANILIST_API, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/graphql-response+json",
      },
      body: JSON.stringify({
        query,
        variables,
      }),
    });
    if (!response.ok) {
      throw new Error(`${await response.text()}`);
    }
    return response.json();
  }

  const UserIdQuery = `
    query ($name: String) {
      User(name: $name) {
        id
        name
      }
    }
  `;
  async function getUserId(name) {
    const res = await fetchAnilist(UserIdQuery, { name });
    return res.data?.User?.id;
  }

  const MediaCollectionQuery = `
    query ($type: MediaType, $userId: Int) {
      MediaListCollection(type: $type, userId: $userId) {
        lists {
          name
          entries {
            id
            status
            score
            media {
              id
              title {
                romaji
              }
            }
          }
        }
      }
    }
  `;

  async function getAnimes(userId) {
    const list = await fetchAnilist(MediaCollectionQuery, {
      type: "ANIME",
      userId,
    });
    const entries = new Map();
    list.data?.MediaListCollection?.lists?.forEach((list) => {
      list?.entries?.forEach((entry) => {
        if (!entry || !entry.media || !entry.media.title) return;
        entries.set(entry.media.id, {
          id: entry.media.id,
          name: entry.media.title.romaji,
          status: entry.status,
          score: entry.score,
        });
      });
    });
    return entries;
  }

  function makeDiffFromLists(list1, list2) {
    const setA = new Set(list1.keys());
    const setB = new Set(list2.keys());

    const list1Exclusive = [];
    const list2Exclusive = [];

    setA.difference(setB).forEach((s) => {
      const { name: anime, status, score } = list1.get(s);
      list1Exclusive.push({
        id: s,
        name: anime,
        status: [status, null],
        score: [score, null],
      });
    });

    setB.difference(setA).forEach((s) => {
      const { name: anime, status, score } = list2.get(s);
      list2Exclusive.push({
        id: s,
        name: anime,
        status: [null, status],
        score: [null, score],
      });
    });
    return { list1Exclusive, list2Exclusive };
  }

  function titleCase(a) {
    return a[0].toUpperCase() + a.slice(1).toLowerCase();
  }

  await waitForElement("div.compare div.entry.header div.title");

  const button = document.createElement("button");
  button.innerText = "Diff";
  button.style.zIndex = "1000";
  button.style.padding = "10px";
  button.style.background = "rgb(147, 87, 193)";
  button.style.fontFamily =
    "Overpass, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif";
  button.style.fontSize = "14px";
  button.style.fontWeight = "600";
  button.onmouseover = () => {
    button.style.background = "rgb(179, 104, 230)";
  };
  button.onmouseout = () => {
    button.style.background = "rgb(147, 87, 193)";
  };
  button.style.borderRadius = "4px";
  button.style.color = "white";
  button.style.borderWidth = "0px";

  document.querySelector("div.compare").prepend(button);
  button.onclick = async () => {
    if (button.innerText !== "Diff") return;
    button.innerText = "Loading";
    const user = document.querySelectorAll("div.name-wrapper h1.name")[0]
      .innerText;
    const self = document
      .querySelectorAll("div.links a.link")[1]
      .href.split("/")
      .at(-2);
    const selfId = await getUserId(self);
    const userId = await getUserId(user);
    const selfList = await getAnimes(selfId);
    const userList = await getAnimes(userId);
    const { list1Exclusive, list2Exclusive } = makeDiffFromLists(
      selfList,
      userList
    );

    const entries = document.querySelectorAll("div.compare div.entry");
    const parent = document.querySelector("div.compare");

    list1Exclusive.forEach((l) => {
      const clonedNode = entries[1].cloneNode(true);
      clonedNode.children[0].children[0].innerText = l.name;
      clonedNode.children[0].children[0].href = `/anime/${l.id}/`;
      clonedNode.children[1].innerText = l.score[0];
      clonedNode.children[2].innerText = "-";
      clonedNode.children[3].innerText = titleCase(l.status[0]);
      clonedNode.children[4].innerText = "-";
      parent.insertBefore(clonedNode, entries[1]);
    });

    list2Exclusive.forEach((l) => {
      const clonedNode = entries[1].cloneNode(true);
      clonedNode.children[0].children[0].innerText = l.name;
      clonedNode.children[0].children[0].href = `/anime/${l.id}/`;
      clonedNode.children[1].innerText = "-";
      clonedNode.children[2].innerText = l.score[1];
      clonedNode.children[3].innerText = "-";
      clonedNode.children[4].innerText = titleCase(l.status[1]);
      parent.insertBefore(clonedNode, entries[1]);
    });
    button.innerText = "Success";
  };
})();