Skip to content

Instantly share code, notes, and snippets.

@ruby0b
Last active January 10, 2026 04:01
Show Gist options
  • Select an option

  • Save ruby0b/30d53d105d247480e494f8240d251bd0 to your computer and use it in GitHub Desktop.

Select an option

Save ruby0b/30d53d105d247480e494f8240d251bd0 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name AMQ QoL scripts
// @match https://animemusicquiz.com/*
// @grant none
// @run-at document-idle
// @version 1.0
// @author ruby0b
// @updateURL https://gist.github.com/ruby0b/30d53d105d247480e494f8240d251bd0/raw/amq-qol.user.js
// ==/UserScript==
(() => {
'use strict';
if (typeof AutoCompleteController === 'undefined')
return console.warn('AMQ QoL userscript - No AutoCompleteController found, bye!');
const IS_CHROMIUM = !!window.chrome;
const after = (f, g) => function (...args) {
f.call(this, ...args);
return g.call(this, ...args);
};
const afterNew = (c, g) => function (...args) {
const instance = new c(...args);
g.call(instance, ...args);
return instance;
};
// auto select first autocomplete item, amq uses https://projects.verou.me/awesomplete/
(() => {
AutoCompleteController.prototype.newList = after(AutoCompleteController.prototype.newList, function () {
this.awesomepleteInstance.autoFirst = true;
});
})();
// don't loop on reveal, continue playing, amq uses https://videojs.org/
// due to a bug on chromium, this is disabled there when in audio-only mode
(() => {
const isChromiumAudioOnly = () => IS_CHROMIUM && quizVideoController.getCurrentResolution() === 0;
const old_replayVideo = QuizVideoController.prototype.replayVideo;
QuizVideoController.prototype.replayVideo = function () {
if (isChromiumAudioOnly())
return old_replayVideo.call(this);
this.getCurrentPlayer().unpauseVideo(); // replaces this.getCurrentPlayer().replayVideo()
this.getCurrentPlayer().show();
};
// loop when a video ends
MoeVideoPlayer = afterNew(MoeVideoPlayer, function () {
this.player.on('ended', function () {
if (isChromiumAudioOnly())
return;
// we loop from the very start, instead one could use the clip start: this.startPoint
this.player.currentTime(0);
this.player.play();
}.bind(this));
});
// make sure that video is paused during extra guess time
quiz._extraGuessTimeListener.callback = after(quiz._extraGuessTimeListener.callback, function () {
quizVideoController.getCurrentPlayer().pauseVideo();
}.bind(quiz));
})();
// add more song info (including a thumbnail) and redo some layout
(() => {
// custom css
$('<style>').text(`
/* make the chat a bit smaller */
#gameChatContainer { width: 20% !important; }
#gameChatPage > .col-xs-9 { width: 80% !important; }
/* use a more reasonable unit for the top margin */
.qpSideContainer { margin-top: 12rem !important; }
/* default is 25% for both */
#qpAnimeContainer > :has(#qpStandingContainer) { width: 20% !important; }
#qpAnimeContainer > :has(#qpSongInfoContainer) { width: 30% !important; }
#qpSongInfoContainer { width: 85% !important; max-width: none !important; }
#qpExtraSongInfo { position: relative !important; right: 0px !important; }
/* fix scuffed upvote/downvote widths */
#qpUpvoteContainer { width: 20% !important; }
#qpDownvoteContainer { width: 20% !important; }
/* move quest button down a bit lest we collide with the song info */
#questContainer { bottom: 8em; }
/* new elements */
#qolInfoGrid { display: grid; grid-template-columns: 5fr 4fr; gap: 20px; padding-bottom: 5px; }
#qolThumbnail img { width: 100%; border-radius: 2px; }
`).appendTo($('head'));
// swap standings and song info
$('#qpAnimeCenterContainer')
.before($('#qpAnimeContainer > :has(#qpSongInfoContainer)'))
.after($('#qpAnimeContainer > :has(#qpStandingContainer)'));
// modify the song info container
$('<div>', { id: 'qolInfoGrid' })
.append($('<div>', { id: 'qolInfo' })
.append($('#qpSongInfoContainer')
.contents()
.not('div:has([data-i18n="quiz.song_info.title"])')
.not('#qpReportFeedbackContainer')
.not('#qpRateOuterContainer')))
.append($('<div>', { id: 'qolThumbnail' })
.append($('<a>', { target: '_blank', rel: 'noopener noreferrer' })
.append($('<img>', { alt: 'Thumbnail' }))))
.insertAfter($('#qpSongInfoContainer div:has([data-i18n="quiz.song_info.title"])'));
$('<div>', { class: 'row' })
.append($('<h5>').append($('<b>').text('Release')))
.append($('<p>', { id: 'qolRelease' }))
.insertBefore($('#qpSongInfoLinkRow'));
$('<div>', { class: 'row' })
.append($('<h5>').append($('<b>').text('Popularity')))
.append($('<p>', { id: 'qolPopularity' }))
.insertBefore($('#qpSongInfoLinkRow'));
const qolSetInfo = (anilistId, imageUrl, release, popularity) => {
$('#qolThumbnail a').attr({ href: anilistId ? 'https://anilist.co/anime/' + anilistId : null });
$('#qolThumbnail img').attr({ src: imageUrl ?? null });
$('#qolRelease').text(release ?? '—');
$('#qolPopularity').text(popularity ? (popularity >= 1000 ? Math.floor(popularity / 1000) + 'K' : popularity) : '—');
};
let qolFetchController = null;
QuizInfoContainer.prototype.showInfo = after(QuizInfoContainer.prototype.showInfo, function (
_animeNames,
_songName,
_artist,
_type,
_typeNumber,
_videoTargetMap,
siteIds,
_animeScore,
animeType,
vintage,
_animeDifficulty,
_animeTags,
_animeGenre,
_altAnimeNames,
_altAnimeNamesAnswers,
_likedState,
_artistHoverInformation,
_rebroadcast,
_dub,
_composerHowerInformation,
_arrangerHoverInformation
) {
qolFetchController?.abort();
qolSetInfo();
const id = siteIds?.aniListId;
if (!id)
return console.warn('No anilist id: ' + siteIds);
const controller = qolFetchController = new AbortController();
fetch("https://graphql.anilist.co", {
signal: controller.signal,
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({
query: `query ($id: Int) { Media(id: $id) { popularity coverImage { large } } }`,
variables: { id }
})
})
.then(res => res.json())
.then(json => {
if (controller.signal.aborted)
return console.info('Aborted showing image for ' + id)
const info = json?.data?.Media;
if (!info)
return console.warn('Could not retrieve info for ' + id);
const amqType = localizationHandler.translate(localizationHandler.convertCategoryToBaseKey(animeType));
const amqSeason = localizationHandler.translate(vintage.key, vintage.data);
qolSetInfo(
id,
info.coverImage?.large,
`${amqSeason} (${amqType})`,
info.popularity,
);
}).catch(err => {
if (err.name !== 'AbortError')
console.error(err);
});
});
QuizInfoContainer.prototype.hideContent = after(QuizInfoContainer.prototype.hideContent, function () {
qolFetchController?.abort();
qolSetInfo();
});
})();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment