🏠 Home 

Anisongs

Adds Anisongs to anime entries on AniList


Install this script?
// ==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);