// ==UserScript== // @name Anisongs // @namespace Morimasa // @author Morimasa // @description Adds Anisongs to anime entries on AniList // @match https://anilist.co/* // @version 1.08 // @license GPL-3.0-or-later // @grant GM_xmlhttpRequest // @grant GM_addStyle // @downloadURL none // ==/UserScript== (() => { const options = { cacheName: 'anison', // name in localstorage cacheLife: 604800000, // 1 week in ms class: 'anisongs', // container class styleAttr: "data-v-202cfa27" // tag el attr from anilist to get the same style } const temp = { last: null, target: null } const Cache = { add(id, data) { let cache = JSON.parse(localStorage.getItem(options.cacheName)) || {} cache[id] = data localStorage.setItem(options.cacheName, JSON.stringify(cache)) }, get(id) { let cache = localStorage.getItem(options.cacheName) if (cache) return JSON.parse(cache)[id] || {time:0} else return {time:0} } } const API = { async getMalId(id) { const query = "query($id:Int){Media(id:$id){idMal}}" const vars = {id} const options = { method: 'POST', body: JSON.stringify({query: query, variables: vars}), } const resp = await request("https://graphql.anilist.co", options) try { return resp.data.Media.idMal } catch { console.error("anisongs: Error getting malId") return null } }, async getSongs(mal_id) { const splitSongs = list => list.flatMap(e => e.split(/\#\d{1,2}\s/)).filter(e => e!=="") let {opening_themes, ending_themes} = await request(`https://api.jikan.moe/v3/anime/${mal_id}/`) opening_themes = splitSongs(opening_themes) ending_themes = splitSongs(ending_themes) return {opening_themes, ending_themes} } } function request(url, options={}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: 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 }) }) } function insert(songs, parent) { if (!songs || !songs.length){ let node = document.createElement('div'); node.innerText = 'No songs to show (つ﹏<)・゚。'; node.style.textAlign = "center"; parent.appendChild(node); return 0; } else{ songs.forEach( (song, i) =>{ let node = document.createElement('p'); node.innerText = `${i+1}. ${song}`; node.setAttribute(options.styleAttr, ''); node.classList = "tag"; parent.appendChild(node); }) } } function createTargetDiv(text, target, pos) { let el = document.createElement('div'); el.appendChild(document.createElement('h2')); el.children[0].innerText = text; el.classList = options.class; target.insertBefore(el, target.children[pos]); return el; } function placeData(data) { cleaner(temp.target); let op = createTargetDiv('Openings', temp.target, 0); let ed = createTargetDiv('Endings', temp.target, 1); insert(data.opening_themes, op); insert(data.ending_themes, ed); } function cleaner(target) { if (!target) return; let el = target.querySelectorAll(`.${options.class}`); el.forEach((e) => { target.removeChild(e) }) } async function launch(currentid) { // get from cache and check TTL const cache = Cache.get(currentid); const TTLpassed = (cache.time + options.cacheLife) < +new Date(); if (TTLpassed){ const mal_id = await API.getMalId(currentid); if (mal_id) { const {opening_themes, ending_themes} = await API.getSongs(mal_id); // add songs to cache if they're not empty if (opening_themes.length || ending_themes.length) { Cache.add(currentid, {opening_themes, ending_themes, time: +new Date()}); } // place the data onto site placeData({opening_themes, ending_themes}); return "Downloaded songs" } else { return "No malid" } } else { // place the data onto site placeData(cache); return "Used cache" } } let observer = new MutationObserver(() => { let currentpath = window.location.pathname.split("/"); if (currentpath[1] === 'anime') { let currentid = currentpath[2]; let location = currentpath.pop(); if (location!=='') temp.last=0; temp.target = document.querySelectorAll('.grid-section-wrap')[2]; if(temp.last!==currentid && location==='' && temp.target) { temp.last = currentid; launch(currentid).then(e => console.log(`Anisongs: ${e}`)); } } else if (currentpath[1] === 'manga'){ cleaner(temp.target); temp.last = 0; } else temp.last = 0; }); observer.observe(document.getElementById('app'), {childList: true, subtree: true}); })()