Anisongs

Adds Anisongs to anime entries on AniList

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name        Anisongs
// @description Adds Anisongs to anime entries on AniList
// @namespace   Morimasa
// @license     GPL-3.0-or-later
// @require     https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js
// @include     https://anilist.co/*
// @connect     graphql.anilist.co
// @connect     api.animethemes.moe
// @version     2.0.2
// @author      Morimasa
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// ==/UserScript==

/*
*/


(function (localforage) {
  'use strict';

  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

  var localforage__default = /*#__PURE__*/_interopDefaultLegacy(localforage);

  function request(url, options = {}) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        url,
        method: options.method || "GET",
        headers: options.headers || {
          Accept: "application/json",
          "Content-Type": "application/json"
        },
        responseType: options.responseType || "json",
        data: options.body || options.data,
        onload: res => resolve(res.response),
        onerror: reject
      });
    });
  }

  localforage__default["default"].config({
    name: 'Anisongs-v2'
  });
  var cache = Cache = {
    async set(key, value, expire_in = 86400000) {
      await localforage__default["default"].setItem(key, value);
      const expire_timestamp = +new Date() + expire_in;
      await localforage__default["default"].setItem(`${key}_expire`, expire_timestamp);
      return value;
    },

    async get(key) {
      const expire_timestamp = await localforage__default["default"].getItem(`${key}_expire`);
      const timestamp_now = +new Date();

      if (expire_timestamp > timestamp_now) {
        console.debug("Cache hit!");
        return localforage__default["default"].getItem(key);
      }

      console.debug("Cache expired!");
      await localforage__default["default"].removeItem(`${key}_expire`);
      await localforage__default["default"].removeItem(key);
      return null;
    }

  };

  var AnimeThemeType;
  (function (AnimeThemeType) {
      AnimeThemeType["OP"] = "OP";
      AnimeThemeType["ED"] = "ED";
  })(AnimeThemeType || (AnimeThemeType = {}));
  var VideoSource;
  (function (VideoSource) {
      VideoSource["WEB"] = "WEB";
      VideoSource["RAW"] = "RAW";
      VideoSource["BD"] = "BD";
      VideoSource["DVD"] = "DVD";
      VideoSource["VHS"] = "VHS";
      VideoSource["LD"] = "LD";
  })(VideoSource || (VideoSource = {}));
  async function getAnimeThemes(Anilist_id) {
      let cached = await cache.get(`animethemes${Anilist_id}`);
      if (cached != null) {
          return cached;
      }
      const include = ["animethemes.animethemeentries.videos", "animethemes.song", "animethemes.song.artists"].join(",");
      const url = `https://api.animethemes.moe/anime?filter[has]=resources&filter[site]=AniList&filter[external_id]=${Anilist_id}&include=${include}`;
      const res = (await request(url)).anime;
      await cache.set(`animethemes${Anilist_id}`, res[0]);
      return res[0];
  }
  function stringifyTheme(sequence, title, artists, episodes, group) {
      let artists_str = artists.map(e => `${e.name}`).join(", ");
      if (artists_str.length > 0) {
          artists_str = ` by ${artists_str}`;
      }
      let eps = episodes ? ` (${episodes.includes("-") ? "eps" : "ep"} ${episodes})` : "";
      let dub = group && group.includes("Dubbed") ? ` (${group})` : "";
      return `${sequence || 1}. "${title}"${artists_str}${eps}${dub}`;
  }
  function groupThemes(anime_themes) {
      const OP = anime_themes.filter(e => e.type == AnimeThemeType.OP).sort((a, b) => a.sequence - b.sequence);
      const ED = anime_themes.filter(e => e.type == AnimeThemeType.ED).sort((a, b) => a.sequence - b.sequence);
      console.log(OP);
      const parse = (theme) => {
          const song_title = theme.song.title;
          const artists = theme.song.artists;
          const sequence = theme.sequence;
          const episodes = theme.animethemeentries.map(e => e.episodes).join(", ");
          const url = theme.animethemeentries[0].videos[0].link;
          const group = theme.group;
          return { url, name: stringifyTheme(sequence, song_title, artists, episodes, group) };
      };
      return { OP: OP.map(parse), ED: ED.map(parse) };
  }

  const GLOBAL_APP = new Promise(resolve => {
      let search_interval = setInterval(() => {
          const app = document.getElementById("app");
          if (app) {
              clearInterval(search_interval);
              resolve(app.__vue__);
          }
      }, 100);
  });
  var AnilistStatus;
  (function (AnilistStatus) {
      AnilistStatus["Releasing"] = "Releasing";
      AnilistStatus["Finished"] = "Finished";
      AnilistStatus["Cancelled"] = "Cancelled";
  })(AnilistStatus || (AnilistStatus = {}));
  async function addRouterAfterHook(func) {
      (await GLOBAL_APP)._router.afterHooks.push(func);
  }
  async function getCurrentView() {
      return (await GLOBAL_APP)._router.history.current;
  }

  const css_class = "anisongs";
  GM_addStyle(`
  .${css_class} {
    width: 50vw;
  }
  .${css_class} .anisong-entry {
    background: rgb(var(--color-foreground));
    border-radius: 3px;
    padding: 8px 10px;
    font-size: 1.3rem;
    margin-bottom: 10px;
  }
  .${css_class} .has-video {
    cursor: pointer;
    color: rgb(var(--color-text));
  }
  .${css_class} .has-video:hover {
	  transition: .15s;
    color: rgb(var(--color-blue));
  }
  .${css_class} .anisong-entry video {
    cursor: auto;
    margin-top: 10px;
    width: 39em;
  }
`);

  class VideoElement {
    constructor(parent, url) {
      this.url = url;
      this.parent = parent;
      this.make();
    }

    toggle() {
      if (this.el.parentNode) {
        this.el.remove();
      } else {
        this.parent.append(this.el);
        this.el.children[0].autoplay = true; // autoplay
      }
    }

    make() {
      const box = document.createElement('div'),
            vid = document.createElement('video');
      vid.src = this.url;
      vid.controls = true;
      vid.preload = "none";
      vid.volume = 0.4;
      box.append(vid);
      this.el = box;
    }

  }

  function createRootElement() {
    const parent = document.querySelector('.overview');
    let root_element = document.createElement("div");
    root_element.style.display = "flex";
    root_element.style.columnGap = "30px";
    parent.append(root_element);
    return root_element;
  }

  function createGroupElement(text, target, pos) {
    let el = document.createElement('div');
    el.appendChild(document.createElement('h2'));
    el.children[0].innerText = text;
    el.classList = css_class;
    target.insertBefore(el, target.children[pos]);
    return el;
  }

  function insertSongs(songs, parent) {
    if (!songs || !songs.length) {
      const node = document.createElement('div');
      node.innerText = 'No songs to show (つ﹏<)・゚。';
      node.style.textAlign = "center";
      parent.appendChild(node);
      return;
    }

    songs.forEach(song => {
      const node = document.createElement('div');
      node.innerText = song.name;

      if (song.url) {
        const vid = new VideoElement(node, song.url);
        node.addEventListener("click", () => vid.toggle());
        node.classList.add("has-video");
      }

      node.classList.add("anisong-entry");
      parent.appendChild(node);
    });
  }

  async function addSongElements(themes, root_element) {
    let current_view = await getCurrentView();

    if (current_view.name != "MediaOverview" || current_view.params.type != "anime") {
      return;
    }

    const op = createGroupElement("Openings", root_element, 0);
    const ed = createGroupElement("Endings", root_element, 1);
    insertSongs(themes.OP, op);
    insertSongs(themes.ED, ed);
  }

  function cleanup(current_anime_id) {
    let el = document.getElementsByClassName("anisongs");

    if (el) {
      [...el].forEach(e => {
        if (e.dataset.anime != current_anime_id) {
          e.parentNode.remove();
          console.debug("cleanup started!");
        }
      });
    }
  }

  async function handleRoute(current, previous) {
    const anime_id = current.params.id;
    cleanup(anime_id);

    if (current.name != "MediaOverview" || current.params.type != "anime") {
      return;
    }

    let anime_themes = [];

    try {
      anime_themes = (await getAnimeThemes(anime_id)).animethemes;
    } catch {
      console.debug("Can't find any songs for this media");
      return;
    }

    anime_themes = groupThemes(anime_themes);
    let inject_interval = setInterval(async () => {
      console.debug("try to inject");
      const injected = createRootElement();

      if (injected) {
        clearInterval(inject_interval);
        injected.dataset.anime = anime_id;
        await addSongElements(anime_themes, injected);
      }
    }, 500);
  }

  (async () => {
    // start function for the first route check
    const current_view = await getCurrentView();
    handleRoute(current_view, null); // mount function into vue router

    addRouterAfterHook(handleRoute);
  })();

})(localforage);