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