Last active
January 10, 2026 04:01
-
-
Save ruby0b/30d53d105d247480e494f8240d251bd0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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