Greasy Fork is available in English.

Anisongs

Adds Anisongs to anime entries on AniList

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