Last active
January 19, 2026 00:56
-
-
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 | |
| // ==/UserScript== | |
| // todo shortcut for skip | |
| '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; | |
| }); | |
| } | |
| // submit selected autocomplete item instead of literal text when guess phase is over | |
| { | |
| const old_handleGuessPhaseOver = QuizTypeAnswerInput.prototype.handleGuessPhaseOver; | |
| QuizTypeAnswerInput.prototype.handleGuessPhaseOver = function (...args) { | |
| try { | |
| if (options.autoSubmit && this.autoSubmitEligible && !this.$input.attr("disabled")) | |
| this.autoCompleteController.awesomepleteInstance.select(); | |
| } finally { | |
| return old_handleGuessPhaseOver.call(this, ...args); | |
| } | |
| }; | |
| } | |
| // don't loop on reveal, continue playing, amq uses https://videojs.org/ | |
| { | |
| QuizVideoController.prototype.replayVideo = function () { | |
| this.getCurrentPlayer().unpauseVideo(); // replaces this.getCurrentPlayer().replayVideo() | |
| this.getCurrentPlayer().show(); | |
| }; | |
| // loop when a video ends | |
| MoeVideoPlayer = afterNew(MoeVideoPlayer, function () { | |
| this.player.on('ended', function () { | |
| // workaround for a bug on chromium in audio-only mode | |
| if (IS_CHROMIUM && quizVideoController.getCurrentResolution() === 0) { | |
| // will restart from clip start, couldn't find a better workaround | |
| this.player.load(); | |
| } else { | |
| // 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 | |
| $('head').append($('<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: 21% !important; } | |
| #qpDownvoteContainer { width: 21% !important; } | |
| /* move quest button down a bit lest it collides 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; } | |
| `)); | |
| // swap standings and song info | |
| $('#qpAnimeCenterContainer') | |
| .before($('#qpAnimeContainer > :has(#qpSongInfoContainer)')) | |
| .after($('#qpAnimeContainer > :has(#qpStandingContainer)')); | |
| // modify the song info container | |
| $('#qpSongInfoContainer div:has([data-i18n="quiz.song_info.title"])') | |
| .after($('<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' }))))); | |
| $('#qpSongInfoLinkRow') | |
| .before($('<div>', { class: 'row' }) | |
| .append($('<h5>').append($('<b>').text('Release'))) | |
| .append($('<p>', { id: 'qolRelease' }))); | |
| $('#qpSongInfoLinkRow') | |
| .before($('<div>', { class: 'row' }) | |
| .append($('<h5>').append($('<b>').text('Popularity'))) | |
| .append($('<p>', { id: 'qolPopularity' }))); | |
| 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