// ==UserScript==
// @name AMQ Detailed Song Info
// @namespace https://github.com/SlashNephy
// @version 0.7.2
// @author SlashNephy
// @description Display detailed information on the side panel of the song.
// @description:ja 曲のサイドパネルに詳細な情報を表示します。
// @homepage https://scrapbox.io/slashnephy/AMQ_%E3%81%A7%E6%9B%B2%E3%81%AE%E3%82%B5%E3%82%A4%E3%83%89%E3%83%91%E3%83%8D%E3%83%AB%E3%81%AB%E8%A9%B3%E7%B4%B0%E6%83%85%E5%A0%B1%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B_UserScript
// @homepageURL https://scrapbox.io/slashnephy/AMQ_%E3%81%A7%E6%9B%B2%E3%81%AE%E3%82%B5%E3%82%A4%E3%83%89%E3%83%91%E3%83%8D%E3%83%AB%E3%81%AB%E8%A9%B3%E7%B4%B0%E6%83%85%E5%A0%B1%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B_UserScript
// @icon https://animemusicquiz.com/favicon-32x32.png
// @supportURL https://github.com/SlashNephy/userscripts/issues
// @match https://animemusicquiz.com/*
// @require https://cdn.jsdelivr.net/gh/TheJoseph98/AMQ-Scripts@b97377730c4e8553d2dcdda7fba00f6e83d5a18a/common/amqScriptInfo.js
// @connect api.jikan.moe
// @connect api.myanimelist.net
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @license MIT license
// ==/UserScript==
(function () {
'use strict';
const awaitFor = async (predicate, timeout) => new Promise((resolve, reject) => {
let timer;
const interval = window.setInterval(() => {
if (predicate()) {
clearInterval(interval);
clearTimeout(timer);
resolve();
}
}, 500);
if (timeout !== undefined) {
timer = window.setTimeout(() => {
clearInterval(interval);
clearTimeout(timer);
reject(new Error('timeout'));
}, timeout);
}
});
const onReady = (callback) => {
if (document.getElementById('startPage')) {
return;
}
awaitFor(() => document.getElementById('loadingScreen')?.classList.contains('hidden') === true)
.then(callback)
.catch(console.error);
};
async function fetchJikanAnimeById(id) {
const response = await fetch(`https://api.jikan.moe/v4/anime/${id}`);
return response.json();
}
const MalClientId = '6b13c8a22ad3a5e16dd52f548ba7d545';
async function fetchMalAnimeScoreById(id) {
const response = await fetch(`https://api.myanimelist.net/v2/anime/${id}?fields=mean`, {
headers: {
'X-MAL-CLIENT-ID': MalClientId,
},
});
return response.json();
}
class GM_Value {
key;
defaultValue;
constructor(key, defaultValue, initialize = true) {
this.key = key;
this.defaultValue = defaultValue;
const value = GM_getValue(key, null);
if (initialize && value === null) {
GM_setValue(key, defaultValue);
}
}
get() {
return GM_getValue(this.key, this.defaultValue);
}
set(value) {
GM_setValue(this.key, value);
}
delete() {
GM_deleteValue(this.key);
}
pop() {
const value = this.get();
this.delete();
return value;
}
}
const scoreCache = new Map();
const titleCache = new Map();
const showDifficultyRow = new GM_Value('SHOW_DIFFICULTY_ROW', true);
const showVintageRow = new GM_Value('SHOW_VINTAGE_ROW', true);
const showFormatRow = new GM_Value('SHOW_FORMAT_ROW', true);
const showRatingRow = new GM_Value('SHOW_RATING_ROW', true);
const showSpotifyLink = new GM_Value('SHOW_SPOTIFY_LINK', true);
const showYouTubeLink = new GM_Value('SHOW_YOUTUBE_LINK', true);
const showAnnLink = new GM_Value('SHOW_ANN_LINK', true);
const rows = [
{
id: 'difficulty-row',
title: 'Difficulty',
isEnabled() {
return showDifficultyRow.get();
},
content(event) {
return `${event.songInfo.animeDifficulty.toFixed(1)} / 100`;
},
},
{
id: 'vintage-row',
title: 'Vintage',
isEnabled() {
return showVintageRow.get();
},
content(event) {
return event.songInfo.vintage;
},
},
{
id: 'format-row',
title: 'Format',
isEnabled() {
return showFormatRow.get();
},
content(event) {
return event.songInfo.animeType;
},
},
{
id: 'rating-row',
title: 'Rating',
isEnabled() {
return showRatingRow.get();
},
async content(event) {
const { malId } = event.songInfo.siteIds;
let score = scoreCache.get(malId);
let title = titleCache.get(malId);
if (score === undefined || title === undefined) {
try {
const result = await fetchJikanAnimeById(malId);
score = result.data.score;
title = result.data.title_japanese;
}
catch {
try {
const result = await fetchMalAnimeScoreById(malId);
score = result.mean;
title = result.alternative_titles.ja;
}
catch {
score = null;
title = null;
}
}
scoreCache.set(malId, score);
titleCache.set(malId, title);
}
if (title !== null && navigator.language === 'ja') {
const element = document.getElementById('qpAnimeName');
if (element !== null && title !== element.textContent?.trim()) {
element.innerHTML = `${title}<br/>(${element.textContent})`;
unsafeWindow.quiz.infoContainer.fitTextToContainer();
}
}
if (score === null) {
return `${event.songInfo.animeScore.toFixed(2)} / 10`;
}
return `${score.toFixed(2)} / 10 (MAL)`;
},
},
];
const links = [
{
id: 'spotify-link',
title: 'Spotify',
target: '_blank',
isEnabled() {
return showSpotifyLink.get();
},
href(event) {
return `spotify://search/${encodeURIComponent(event.songInfo.songName)}%20${encodeURIComponent(event.songInfo.artist)}/tracks`;
},
},
{
id: 'youtube-link',
title: 'YouTube',
target: '_blank',
isEnabled() {
return showYouTubeLink.get();
},
href(event) {
return `https://www.youtube.com/results?search_query=${encodeURIComponent(event.songInfo.songName)}+${encodeURIComponent(event.songInfo.artist)}`;
},
},
{
id: 'ann-link',
title: 'ANN',
target: '_blank',
isEnabled() {
return showAnnLink.get();
},
href(event) {
return `https://www.animenewsnetwork.com/encyclopedia/anime.php?id=${event.songInfo.annId}`;
},
},
];
const handle = (event) => {
const container = document.querySelector('#qpAnimeContainer #qpSongInfoContainer');
if (!container) {
throw new Error('container is not found.');
}
for (const row of rows) {
if (row.isEnabled !== undefined && !row.isEnabled()) {
continue;
}
const element = getOrCreateRow(container, row.id);
const contentElement = element.querySelector('.row-content');
if (contentElement !== null) {
const content = row.content(event);
if (content !== null && typeof content !== 'string') {
contentElement.textContent = 'Loading...';
}
Promise.resolve(content)
.then((c) => {
contentElement.textContent = c;
})
.catch(console.error);
}
else {
const content = row.content(event);
Promise.resolve(content)
.then((c) => {
renderRow(element, {
...row,
content: c,
});
})
.catch(console.error);
}
}
const element = getOrCreateLinkContainer(container, 'link-container');
renderLinks(element, links
.filter((link) => link.isEnabled === undefined || link.isEnabled())
.map((link) => {
const href = link.href(event);
if (href === null) {
return null;
}
return {
...link,
href,
};
})
.filter((x) => x !== null));
};
const getOrCreateRow = (container, id) => {
const existing = document.getElementById(id);
if (existing !== null) {
return existing;
}
const element = document.createElement('div');
element.id = id;
const hider = container.querySelector('div#qpInfoHider');
if (hider === null) {
throw new Error('div#qpInfoHider is not found.');
}
if (!hider.classList.contains('custom-hider')) {
hider.classList.add('custom-hider');
}
container.insertBefore(element, hider.previousElementSibling);
return element;
};
const renderRow = (element, row) => {
const h5 = document.createElement('h5');
const b = document.createElement('b');
const p = document.createElement('p');
h5.append(b);
element.append(h5);
element.append(p);
element.classList.add('row');
p.classList.add('row-content');
b.textContent = row.title;
p.textContent = row.content;
};
const getOrCreateLinkContainer = (container, id) => {
const existing = document.getElementById(id);
if (existing !== null) {
while (existing.lastElementChild !== null) {
existing.removeChild(existing.lastElementChild);
}
return existing;
}
const element = document.createElement('div');
element.id = id;
const hider = container.querySelector('div#qpInfoHider');
if (hider === null) {
throw new Error('div#qpInfoHider is not found.');
}
if (!hider.classList.contains('custom-hider')) {
hider.classList.add('custom-hider');
}
container.insertBefore(element, hider);
return element;
};
const renderLinks = (element, ls) => {
const b = document.createElement('b');
element.append(b);
const lastIndex = ls.length - 1;
for (const [index, link] of ls.entries()) {
const a = document.createElement('a');
b.append(a);
a.href = link.href;
a.textContent = link.title;
if (link.target !== undefined) {
a.target = link.target;
}
if (index !== lastIndex) {
b.append(' - ');
}
}
};
unsafeWindow.detailedSongInfo = {
register(item) {
const container = 'content' in item ? rows : links;
if (container.some((x) => x.id === item.id)) {
return;
}
container.push(item);
},
unregister(item) {
const container = 'content' in item ? rows : links;
const index = container.findIndex((x) => x.id === item.id);
if (index >= 0) {
container.splice(index, 1);
}
},
get rows() {
return rows;
},
get links() {
return links;
},
};
onReady(() => {
new Listener('answer results', handle).bindListener();
AMQ_addScriptData({
name: 'Detailed Song Info',
author: 'SlashNephy <[email protected]>',
description: 'Display detailed information on the side panel of the song.',
});
AMQ_addStyle(`
.custom-hider {
padding: 50% 0;
}
`);
});
})();