├── forceSongLinkRedirect.user.js ├── LICENSE ├── amqDisableAnimatedAvatars.user.js ├── README.md ├── json_combiner.py ├── amqPlayButtonShortcuts.user.js ├── amqShowAllSongLinks.user.js ├── amqCatboxHostSwitch.user.js ├── json_downloader.py ├── amqShowRoomPlayers.user.js ├── anisongdbUtilities.user.js ├── amqAnisongdbSearch.user.js ├── amqCustomScoreCounter.user.js ├── amqQuickLoadLists.user.js └── amqNewGameModeUI.user.js /forceSongLinkRedirect.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Force Song Link Redirect 3 | // @namespace https://github.com/kempanator 4 | // @version 0.1 5 | // @description Force redirects on all animemusicquiz.com song links 6 | // @author kempanator 7 | // @match https://*.animemusicquiz.com/* 8 | // @grant none 9 | // @downloadURL https://github.com/kempanator/amq-scripts/raw/main/forceSongLinkRedirect.user.js 10 | // @updateURL https://github.com/kempanator/amq-scripts/raw/main/forceSongLinkRedirect.user.js 11 | // ==/UserScript== 12 | 13 | /* 14 | This is for opening song links in a new tab 15 | (not for AMQ) 16 | */ 17 | 18 | let host = 1; //only change this for different host 19 | let hostMap = {1: "eudist", 2: "nawdist", 3: "naedist"}; 20 | 21 | let regex = /^https:\/\/(\w+)\.animemusicquiz\.com/.exec(window.location.href); 22 | if (regex && Object.values(hostMap).includes(regex[1]) && hostMap[host] !== regex[1]) { 23 | let newURL = window.location.href.replace(/^https:\/\/\w+/, "https://" + hostMap[host]); 24 | window.location.replace(newURL); 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 kempanator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /amqDisableAnimatedAvatars.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Disable Animated Avatars 3 | // @namespace https://github.com/kempanator 4 | // @version 0.4 5 | // @description Disable animated avatars in AMQ 6 | // @author kempanator 7 | // @match https://*.animemusicquiz.com/* 8 | // @grant none 9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js 10 | // @downloadURL https://github.com/kempanator/amq-scripts/raw/main/amqDisableAnimatedAvatars.user.js 11 | // @updateURL https://github.com/kempanator/amq-scripts/raw/main/amqDisableAnimatedAvatars.user.js 12 | // ==/UserScript== 13 | 14 | "use strict"; 15 | if (typeof Listener === "undefined") return; 16 | 17 | SpineApp.prototype.render = function (canvas) { 18 | let renderer = canvas.renderer; 19 | if (!this.paused) { 20 | canvas.clear(0, 0, 0, 0); 21 | renderer.begin(); 22 | renderer.drawSkeleton(this.skeleton, this.pma); 23 | renderer.end(); 24 | this.paused = true; 25 | } 26 | } 27 | 28 | SpineApp.prototype.setAnimation = function (animationName, loop) { 29 | this.paused = false; 30 | if (this.animationState) this.animationState.setAnimation(0, animationName, loop); 31 | } 32 | 33 | SpineAnimation.prototype.updatePose = function (poseId) { 34 | this.poseId = poseId; 35 | this.spineApp.setAnimation(this.POSE_ID_ANIMATION_NAMES[this.poseId], true); 36 | } 37 | 38 | SimpleAnimationController.prototype.runAnimation = function () { } 39 | 40 | AMQ_addScriptData({ 41 | name: "Disable Animated Avatars", 42 | author: "kempanator", 43 | version: GM_info.script.version, 44 | link: "https://github.com/kempanator/amq-scripts/raw/main/amqDisableAnimatedAvatars.user.js", 45 | description: `

Disable animated avatars in lobby, quiz, and friends list

` 46 | }); 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amq-scripts 2 | click on a script name below to install 3 | 4 | ### [AMQ Anisongdb Search](https://github.com/kempanator/amq-scripts/raw/main/amqAnisongdbSearch.user.js) 5 | - AnisongDB search in game https://imgur.com/a/TmqtE5K 6 | 7 | ### [AMQ Answer Stats](https://github.com/kempanator/amq-scripts/raw/main/amqAnswerStats.user.js) 8 | - Tools for analyzing ranked answers in real time https://imgur.com/QIcwtXE 9 | 10 | ### [AMQ Chat Plus](https://github.com/kempanator/amq-scripts/raw/main/amqChatPlus.user.js) 11 | - Customizable chat/dms https://imgur.com/FAT78Ep 12 | 13 | ### [AMQ Custom Score Counter](https://github.com/kempanator/amq-scripts/raw/main/amqCustomScoreCounter.user.js) 14 | - Set custom teams and weighted scores 15 | 16 | ### [AMQ Custom Song List Game](https://github.com/kempanator/amq-scripts/raw/main/amqCustomSongListGame.user.js) 17 | - Play a solo game with a custom song list https://imgur.com/a/tGzQAeU 18 | 19 | ### [AMQ Mega Commands](https://github.com/kempanator/amq-scripts/raw/main/amqMegaCommands.user.js) 20 | - Lots of commands (replacement for several other scripts) 21 | 22 | ### [AMQ New Game Mode UI](https://github.com/kempanator/amq-scripts/raw/main/amqNewGameModeUI.user.js) 23 | - A fully automatic guess counter for your team in new game mode https://imgur.com/a/OqrQ6nI 24 | 25 | ### [AMQ Notification Sounds](https://github.com/kempanator/amq-scripts/raw/main/amqNotificationSounds.user.js) 26 | - Play notification sounds on certain game events 27 | 28 | ### [AMQ Play Button Shortcuts](https://github.com/kempanator/amq-scripts/raw/main/amqPlayButtonShortcuts.user.js) 29 | - Add Solo, Multiplayer, Nexus shortcuts to the play button https://imgur.com/a/9nmYmSt 30 | 31 | ### [AMQ Quick Load Lists](https://github.com/kempanator/amq-scripts/raw/main/amqQuickLoadLists.user.js) 32 | - Add a window for quick loading lists + status https://imgur.com/a/B4ucCYV 33 | 34 | ### [AMQ Show All Song Links](https://github.com/kempanator/amq-scripts/raw/main/amqShowAllSongLinks.user.js) 35 | - Show all song links in the song info container https://imgur.com/a/bfnZoQu 36 | 37 | ### [AMQ Show Room Players](https://github.com/kempanator/amq-scripts/raw/main/amqShowRoomPlayers.user.js) 38 | - Mouse over the players bar on a room tile to show full player list https://imgur.com/jivcLDo 39 | 40 | ### [AnisongDB Utilities](https://github.com/kempanator/amq-scripts/raw/main/anisongdbUtilities.user.js) 41 | - Some extra functionality for AnisongDB https://imgur.com/a/IXB6xak 42 | -------------------------------------------------------------------------------- /json_combiner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Combine multiple JSON files into one per subfolder. 3 | Version 0.3 4 | 5 | Instructions: 6 | 1. Place a `json` folder in the working directory (or set a custom path below). 7 | 2. Inside `json`, create subfolders named after your desired output file. 8 | 3. Add JSON files into those subfolders. 9 | 4. Run this script. 10 | 5. Each subfolder will produce a combined JSON file in the `json` folder. 11 | """ 12 | 13 | import os 14 | import json 15 | 16 | # Optional custom path (absolute or relative) 17 | BASE_PATH = "" 18 | JSON_PATH = os.path.join(BASE_PATH, "json") 19 | 20 | 21 | def load_json(file_path: str): 22 | """Safely load a JSON file.""" 23 | with open(file_path, "r", encoding="utf-8") as file: 24 | return json.load(file) 25 | 26 | 27 | def combine_folder(folder_path: str) -> list: 28 | """Combine JSON data from all files in a folder into one list.""" 29 | combined_data = [] 30 | for file_name in os.listdir(folder_path): 31 | file_path = os.path.join(folder_path, file_name) 32 | if not os.path.isfile(file_path) or not file_name.endswith(".json"): 33 | continue # skip non-files and non-json 34 | 35 | data = load_json(file_path) 36 | 37 | if isinstance(data, dict) and "songs" in data: # AMQ JSON format 38 | combined_data.extend(data["songs"]) 39 | elif isinstance(data, list): # AnisongDB/Joseph format 40 | combined_data.extend(data) 41 | else: 42 | print(f"⚠️ Skipped unrecognized format: {file_name}") 43 | 44 | return combined_data 45 | 46 | 47 | def combine_all(): 48 | """Look for subfolders inside JSON_PATH and combine their contents.""" 49 | os.makedirs(JSON_PATH, exist_ok=True) 50 | 51 | for folder_name in os.listdir(JSON_PATH): 52 | folder_path = os.path.join(JSON_PATH, folder_name) 53 | if not os.path.isdir(folder_path): 54 | continue 55 | 56 | combined_data = combine_folder(folder_path) 57 | if not combined_data: 58 | print(f"⚠️ No valid data found in {folder_name}, skipping.") 59 | continue 60 | 61 | output_file = os.path.join(JSON_PATH, f"{folder_name}.json") 62 | with open(output_file, "w", encoding="utf-8") as out_file: 63 | json.dump(combined_data, out_file, ensure_ascii=False, separators=(",", ":")) 64 | 65 | print(f"✅ Created {output_file}") 66 | 67 | 68 | if __name__ == "__main__": 69 | combine_all() 70 | -------------------------------------------------------------------------------- /amqPlayButtonShortcuts.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Play Button Shortcuts 3 | // @namespace https://github.com/kempanator 4 | // @version 0.10 5 | // @description Add Solo, Multiplayer, Nexus shortcuts to the play button 6 | // @author kempanator 7 | // @match https://*.animemusicquiz.com/* 8 | // @grant none 9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js 10 | // @downloadURL https://github.com/kempanator/amq-scripts/raw/main/amqPlayButtonShortcuts.user.js 11 | // @updateURL https://github.com/kempanator/amq-scripts/raw/main/amqPlayButtonShortcuts.user.js 12 | // ==/UserScript== 13 | 14 | "use strict"; 15 | if (typeof Listener === "undefined") return; 16 | const loadInterval = setInterval(() => { 17 | if (document.querySelector("#loadingScreen.hidden")) { 18 | clearInterval(loadInterval); 19 | setup(); 20 | } 21 | }, 500); 22 | 23 | $("#mpPlayButton").removeAttr("data-toggle data-target").empty().append(/*html*/` 24 |
Play
25 | 30 | `); 31 | applyStyles(); 32 | 33 | function setup() { 34 | AMQ_addScriptData({ 35 | name: "Play Button Shortcuts", 36 | author: "kempanator", 37 | version: GM_info.script.version, 38 | link: "https://github.com/kempanator/amq-scripts/raw/main/amqPlayButtonShortcuts.user.js", 39 | description: `

Add Solo, Multiplayer, Nexus shortcuts to the play button

` 40 | }); 41 | } 42 | 43 | // apply styles 44 | function applyStyles() { 45 | let css = /*css*/ ` 46 | #mpPlayButton { 47 | height: 120px; 48 | } 49 | #mpPlayButtonPlay { 50 | font-size: 50px; 51 | } 52 | #mpPlayButtonPlay:hover { 53 | color: #4497EA; 54 | } 55 | #mpPlayButton .gameModeShortcut { 56 | font-size: 25px; 57 | text-align: center; 58 | width: 33.33%; 59 | padding-top: 6px; 60 | float: left; 61 | } 62 | #mpPlayButton .gameModeShortcut:hover { 63 | color: #4497EA; 64 | } 65 | `; 66 | let style = document.getElementById("playButtonShortcutsStyle"); 67 | if (style) { 68 | style.textContent = css.trim(); 69 | } 70 | else { 71 | style = document.createElement("style"); 72 | style.id = "playButtonShortcutsStyle"; 73 | style.textContent = css.trim(); 74 | document.head.appendChild(style); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /amqShowAllSongLinks.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Show All Song Links 3 | // @namespace https://github.com/kempanator 4 | // @version 0.12 5 | // @description Show all song links in the song info container 6 | // @author kempanator 7 | // @match https://*.animemusicquiz.com/* 8 | // @grant none 9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js 10 | // @downloadURL https://github.com/kempanator/amq-scripts/raw/main/amqShowAllSongLinks.user.js 11 | // @updateURL https://github.com/kempanator/amq-scripts/raw/main/amqShowAllSongLinks.user.js 12 | // ==/UserScript== 13 | 14 | "use strict"; 15 | if (typeof Listener === "undefined") return; 16 | const loadInterval = setInterval(() => { 17 | if (document.querySelector("#loadingScreen.hidden")) { 18 | clearInterval(loadInterval); 19 | setup(); 20 | } 21 | }, 500); 22 | 23 | const LISTS = [ 24 | { label: "ANI", key: "aniListId", url: "https://anilist.co/anime/" }, 25 | { label: "KIT", key: "kitsuId", url: "https://kitsu.io/anime/" }, 26 | { label: "MAL", key: "malId", url: "https://myanimelist.net/anime/" }, 27 | { label: "ANN", key: "annId", url: "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=" }, 28 | ]; 29 | const SONG_LINKS = [ 30 | { label: "720", key: "720" }, 31 | { label: "480", key: "480" }, 32 | { label: "MP3", key: "0" }, 33 | ]; 34 | let $qpSongInfoLinkRow; 35 | 36 | function setup() { 37 | $qpSongInfoLinkRow = $("#qpSongInfoLinkRow"); 38 | new Listener("answer results", (data) => { 39 | setTimeout(() => { 40 | const $b = $(""); 41 | for (const item of LISTS) { 42 | $b.append(buildLink(getListSiteUrl(data, item.key, item.url), item.label)); 43 | } 44 | $b.append("
"); 45 | for (const item of SONG_LINKS) { 46 | $b.append(buildLink(getSongUrl(data, item.key), item.label)); 47 | } 48 | $qpSongInfoLinkRow.find("b").remove(); 49 | $qpSongInfoLinkRow.prepend($b); 50 | }, 0); 51 | }).bindListener(); 52 | 53 | applyStyles(); 54 | AMQ_addScriptData({ 55 | name: "Show All Song Links", 56 | author: "kempanator", 57 | version: GM_info.script.version, 58 | link: "https://github.com/kempanator/amq-scripts/raw/main/amqShowAllSongLinks.user.js", 59 | description: ` 60 |

Show all song links in the song info container

61 | ` 62 | }); 63 | } 64 | 65 | // get list site url 66 | function getListSiteUrl(data, type, url) { 67 | const id = data.songInfo?.siteIds?.[type]; 68 | if (!id) return ""; 69 | return url + id; 70 | } 71 | 72 | // get song url from answer results data, handle bad data 73 | function getSongUrl(data, type) { 74 | const url = data.songInfo?.videoTargetMap?.catbox?.[type] ?? data.songInfo?.videoTargetMap?.openingsmoe?.[type]; 75 | if (!url) return ""; 76 | if (url.startsWith("http")) return url; 77 | return videoResolver.formatUrl(url); 78 | } 79 | 80 | // build link element in song info box 81 | function buildLink(url, text) { 82 | return $("", { href: url, target: "_blank", rel: "noreferrer", text: text }).toggleClass("disabled", !url); 83 | } 84 | 85 | // apply styles 86 | function applyStyles() { 87 | let css = /*css*/ ` 88 | #qpSongInfoLinkRow b a { 89 | margin: 0 3px; 90 | } 91 | #qpExtraSongInfo { 92 | z-index: 1; 93 | } 94 | `; 95 | let style = document.getElementById("showAllSongLinksStyle"); 96 | if (style) { 97 | style.textContent = css.trim(); 98 | } 99 | else { 100 | style = document.createElement("style"); 101 | style.id = "showAllSongLinksStyle"; 102 | style.textContent = css.trim(); 103 | document.head.appendChild(style); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /amqCatboxHostSwitch.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Catbox Host Switch 3 | // @namespace https://github.com/kempanator 4 | // @version 0.21 5 | // @description Switch your catbox host 6 | // @author kempanator 7 | // @match https://*.animemusicquiz.com/* 8 | // @grant none 9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js 10 | // @downloadURL https://github.com/kempanator/amq-scripts/raw/main/amqCatboxHostSwitch.user.js 11 | // @updateURL https://github.com/kempanator/amq-scripts/raw/main/amqCatboxHostSwitch.user.js 12 | // ==/UserScript== 13 | 14 | /* 15 | Settings are located in: bottom right gear icon > settings > video hosts > catbox link 16 | 17 | Features: 18 | - Modify all incoming catbox song links 19 | */ 20 | 21 | "use strict"; 22 | if (typeof Listener === "undefined") return; 23 | const loadInterval = setInterval(() => { 24 | if (document.querySelector("#loadingScreen.hidden")) { 25 | clearInterval(loadInterval); 26 | setup(); 27 | } 28 | }, 500); 29 | 30 | const saveData = validateLocalStorage("catboxHostSwitch"); 31 | const hostDict = { 1: "eudist.animemusicquiz.com", 2: "nawdist.animemusicquiz.com", 3: "naedist.animemusicquiz.com" }; 32 | let catboxHost = parseInt(saveData.catboxHost); 33 | if (!hostDict.hasOwnProperty(catboxHost)) catboxHost = 0; 34 | 35 | // setup 36 | function setup() { 37 | $("#settingsVideoHostsContainer .col-xs-6").first() 38 | .append("

Catbox Link

") 39 | .append($(``) 40 | .append(``) 41 | .append(``) 42 | .append(``) 43 | .append(``) 44 | .val(catboxHost) 45 | .on("change", function () { 46 | catboxHost = parseInt(this.value); 47 | saveSettings(); 48 | }) 49 | ); 50 | 51 | QuizVideoController.prototype.nextVideoInfo = function (songInfo, playLength, startPoint, firstVideo, startTime, playbackSpeed, fullSongRange, forceBuffering, forcedSamplePoint) { 52 | if (catboxHost && songInfo.videoMap?.catbox) { 53 | for (const key of Object.keys(songInfo.videoMap.catbox)) { 54 | const url = songInfo.videoMap.catbox[key]; 55 | if (url) { 56 | if (/^https:\/\/\w+\.animemusicquiz\.com\/\w+\.\w{3,4}$/i.test(url)) { 57 | songInfo.videoMap.catbox[key] = url.replace(/^https:\/\/\w+\.animemusicquiz\.com/, `https://${hostDict[catboxHost]}`); 58 | } 59 | else if (/^\w+\.\w{3,4}$/i.test(url)) { //normal quiz 60 | songInfo.videoMap.catbox[key] = `https://${hostDict[catboxHost]}/${url}`; 61 | } 62 | else if (/^\w+:\w+$/i.test(url)) { //encrypted lobby (ranked, event, tournament) 63 | songInfo.videoMap.catbox[key] = `https://${hostDict[catboxHost]}/internals/dist.php?enc=${url}`; 64 | } 65 | } 66 | } 67 | } 68 | this._nextVideoInfo = { 69 | songInfo: songInfo, 70 | playLength: playLength, 71 | startPoint: startPoint, 72 | playbackSpeed: playbackSpeed, 73 | firstVideo: firstVideo, 74 | startTime: startTime, 75 | fullSongRange: fullSongRange, 76 | forcedSamplePoint: forcedSamplePoint, 77 | }; 78 | if (forceBuffering) { 79 | this.loadNextVideo(); 80 | } 81 | else if (firstVideo) { 82 | this.readyToBufferNextVideo = false; 83 | } 84 | else if (this.readyToBufferNextVideo) { 85 | this.loadNextVideo(); 86 | } 87 | }; 88 | 89 | AMQ_addScriptData({ 90 | name: "Catbox Host Switch", 91 | author: "kempanator", 92 | version: GM_info.script.version, 93 | link: "https://github.com/kempanator/amq-scripts/raw/main/amqCatboxHostSwitch.user.js", 94 | description: ` 95 |

Modify all incoming catbox song links

96 |

Settings are located in: bottom right gear icon > settings > video hosts > catbox link

97 | ` 98 | }); 99 | } 100 | 101 | // validate json data in local storage 102 | function validateLocalStorage(item) { 103 | try { 104 | const json = JSON.parse(localStorage.getItem(item)); 105 | if (!json || typeof json !== "object") return {}; 106 | return json; 107 | } 108 | catch { 109 | return {}; 110 | } 111 | } 112 | 113 | // save settings 114 | function saveSettings() { 115 | localStorage.setItem("catboxHostSwitch", JSON.stringify({ 116 | catboxHost 117 | })); 118 | } 119 | -------------------------------------------------------------------------------- /json_downloader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Download audio or video files from JSON files 3 | Version 0.7 4 | 5 | Instructions: 6 | 1. Put song list JSONs inside the "json" folder 7 | 2. Run this script 8 | 3. Downloaded songs will appear in "audio" and "video" folders 9 | 10 | Supported file formats: 11 | 1. anisongdb JSON 12 | 2. official AMQ song history export 13 | 3. joseph song list script export 14 | 4. blissfulyoshi ranked song list 15 | 5. csl song list 16 | """ 17 | 18 | import os 19 | import json 20 | import subprocess 21 | import urllib.request 22 | 23 | # Ensure eyed3 is available 24 | try: 25 | import eyed3 26 | except ModuleNotFoundError: 27 | subprocess.run(["python", "-m", "pip", "install", "-U", "eyed3"], check=True) 28 | import eyed3 29 | 30 | 31 | # --- CONFIGURATION --- 32 | MODE = "both" # "audio", "video", "both" 33 | HOST = 1 34 | HOSTS = { 35 | 1: "eudist.animemusicquiz.com", 36 | 2: "nawdist.animemusicquiz.com", 37 | 3: "naedist.animemusicquiz.com", 38 | } 39 | BASE_PATH = "" # optional custom path 40 | JSON_PATH = os.path.join(BASE_PATH, "json") 41 | AUDIO_PATH = os.path.join(BASE_PATH, "audio") 42 | VIDEO_PATH = os.path.join(BASE_PATH, "video") 43 | 44 | 45 | # --- UTILITIES --- 46 | def extract_data(obj: dict, possible_keys: list[str]): 47 | """Try multiple keys (supports dot notation) and return first match.""" 48 | for key in possible_keys: 49 | if "." in key: 50 | temp = obj 51 | for k in key.split("."): 52 | if isinstance(temp, dict) and k in temp: 53 | temp = temp[k] 54 | else: 55 | break 56 | else: 57 | return temp 58 | elif key in obj: 59 | return obj[key] 60 | return None 61 | 62 | 63 | def convert_url(url: str): 64 | """Convert relative URL to full host URL.""" 65 | if not url: 66 | return None 67 | if url.startswith("http"): 68 | return url 69 | return f"https://{HOSTS[HOST]}/{url}" 70 | 71 | 72 | def sanitize_filename(name: str): 73 | """Remove forbidden characters from filenames.""" 74 | for char in "<>:\"/\\|?*": 75 | name = name.replace(char, "") 76 | return name.strip() 77 | 78 | 79 | def shorten_type(text: str): 80 | """Shorten song type text to OP/ED/IN.""" 81 | if not text: 82 | return None 83 | if text.startswith("O"): 84 | return f"OP{text.split()[1]}" 85 | if text.startswith("E"): 86 | return f"ED{text.split()[1]}" 87 | if text.startswith("I"): 88 | return "IN" 89 | return text 90 | 91 | 92 | # --- SONG CLASS --- 93 | class Song: 94 | def __init__(self, data: dict): 95 | self.animeRomajiName = extract_data(data, ["animeRomajiName", "animeJPName", "songInfo.animeNames.romaji", "anime.romaji", "animeRomaji"]) 96 | self.animeEnglishName = extract_data(data, ["animeEnglishName", "animeENName", "songInfo.animeNames.english", "anime.english", "animeEnglish"]) 97 | self.songArtist = extract_data(data, ["songArtist", "artist", "songInfo.artist"]) 98 | self.songName = extract_data(data, ["songName", "name", "songInfo.songName"]) 99 | self.songType = self._extract_type(data) 100 | self.songDifficulty = self._extract_difficulty(data) 101 | self.animeType = extract_data(data, ["animeType", "songInfo.animeType"]) 102 | self.animeVintage = extract_data(data, ["animeVintage", "songInfo.vintage", "vintage"]) 103 | self.audio = self._extract_media(data, ["audio", "videoUrl", "urls.catbox.0", "LinkMp3"], ".mp3") 104 | self.video480 = self._extract_media(data, ["video480", "MQ", "videoUrl", "urls.catbox.480"], ".webm") 105 | self.video720 = self._extract_media(data, ["video720", "HQ", "videoUrl", "urls.catbox.720", "LinkVideo"], ".webm") 106 | 107 | def _extract_type(self, data: dict): 108 | raw_type = extract_data(data, ["songType", "type", "songInfo.type"]) 109 | if isinstance(raw_type, int): 110 | type_dict = {1: "Opening", 2: "Ending", 3: "Insert Song"} 111 | number = extract_data(data, ["songTypeNumber", "songInfo.typeNumber"]) 112 | text = type_dict.get(raw_type) 113 | return f"{text} {number}" if text and number else text 114 | return raw_type 115 | 116 | def _extract_difficulty(self, data: dict): 117 | dif = extract_data(data, ["songDifficulty", "songInfo.animeDifficulty", "songInfo.songName"]) 118 | try: 119 | return float(dif) 120 | except (TypeError, ValueError): 121 | return None 122 | 123 | def _extract_media(self, data: dict, keys: list[str], suffix: str): 124 | link = extract_data(data, keys) 125 | if isinstance(link, str) and link.endswith(suffix): 126 | return convert_url(link) 127 | return None 128 | 129 | def has_valid_link(self): 130 | return any(link and link.strip() for link in [self.audio, self.video480, self.video720]) 131 | 132 | def __str__(self): 133 | return f"0:{self.audio}, 480:{self.video480}, 720:{self.video720}" 134 | 135 | 136 | # --- DOWNLOADERS --- 137 | def download_audio(song: Song): 138 | os.makedirs(AUDIO_PATH, exist_ok=True) 139 | if not song.audio: 140 | print(f"Missing audio: {song.animeEnglishName or song.animeRomajiName} {shorten_type(song.songType)}") 141 | return 142 | 143 | filename = song.audio.split("/")[-1] 144 | filepath = os.path.join(AUDIO_PATH, filename) 145 | if os.path.isfile(filepath): 146 | return 147 | 148 | try: 149 | print(f"Downloading {filename}") 150 | urllib.request.urlretrieve(song.audio, filepath) 151 | 152 | eyed3.log.setLevel("ERROR") 153 | id3 = eyed3.load(filepath) 154 | id3.initTag() 155 | id3.tag.artist = song.songArtist or "" 156 | id3.tag.title = song.songName or "" 157 | id3.tag.genre = song.songType or "" 158 | id3.tag.album = song.animeEnglishName or song.animeRomajiName or "" 159 | id3.tag.save() 160 | except Exception as e: 161 | print(f"Error downloading {filename}: {e}") 162 | 163 | 164 | def download_video(song: Song): 165 | os.makedirs(VIDEO_PATH, exist_ok=True) 166 | link = song.video720 or song.video480 167 | if not (link and link.startswith("http")): 168 | print(f"Missing video: {song.animeEnglishName or song.animeRomajiName} {shorten_type(song.songType)}") 169 | return 170 | 171 | filename = link.split("/")[-1] 172 | filepath = os.path.join(VIDEO_PATH, filename) 173 | if os.path.isfile(filepath): 174 | return 175 | 176 | try: 177 | print(f"Downloading {filename}") 178 | urllib.request.urlretrieve(link, filepath) 179 | except Exception as e: 180 | print(f"Error downloading {filename}: {e}") 181 | 182 | 183 | # --- MAIN --- 184 | def create_song_list(data): 185 | """Convert JSON structure into Song objects.""" 186 | if not data: 187 | print("Error: no data") 188 | return [] 189 | 190 | songs = [] 191 | if isinstance(data, dict): 192 | if "songs" in data: 193 | songs.extend(Song(x) for x in data["songs"]) 194 | if "songHistory" in data: 195 | songs.extend(Song(x) for x in data["songHistory"].values()) 196 | elif isinstance(data, list): 197 | songs.extend(Song(x) for x in data) 198 | 199 | return songs 200 | 201 | 202 | def main(): 203 | os.makedirs(JSON_PATH, exist_ok=True) 204 | 205 | json_files = [f for f in os.listdir(JSON_PATH) if f.endswith(".json")] 206 | if not json_files: 207 | print("Error: no json files found") 208 | return 209 | 210 | for file_name in json_files: 211 | with open(os.path.join(JSON_PATH, file_name), encoding="utf-8") as f: 212 | song_list = create_song_list(json.load(f)) 213 | 214 | for song in song_list: 215 | if not song.has_valid_link(): 216 | continue 217 | if MODE in ("audio", "both"): 218 | download_audio(song) 219 | if MODE in ("video", "both"): 220 | download_video(song) 221 | 222 | if os.path.isdir(AUDIO_PATH): 223 | print(f"Total songs in audio folder = {len(os.listdir(AUDIO_PATH))}") 224 | if os.path.isdir(VIDEO_PATH): 225 | print(f"Total songs in video folder = {len(os.listdir(VIDEO_PATH))}") 226 | 227 | 228 | if __name__ == "__main__": 229 | main() 230 | -------------------------------------------------------------------------------- /amqShowRoomPlayers.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name AMQ Show Room Players 3 | // @namespace https://github.com/kempanator 4 | // @version 0.30 5 | // @description Adds extra functionality to room tiles 6 | // @author kempanator 7 | // @match https://*.animemusicquiz.com/* 8 | // @grant none 9 | // @require https://github.com/joske2865/AMQ-Scripts/raw/master/common/amqScriptInfo.js 10 | // @downloadURL https://github.com/kempanator/amq-scripts/raw/main/amqShowRoomPlayers.user.js 11 | // @updateURL https://github.com/kempanator/amq-scripts/raw/main/amqShowRoomPlayers.user.js 12 | // ==/UserScript== 13 | 14 | /* 15 | New room tile features: 16 | 1. Mouse over players bar to show full player list (friends & blocked have color) 17 | 2. Click name in player list to open profile 18 | 3. Click host name to open profile 19 | 4. Invisible friends are no longer hidden 20 | 5. Bug fix for friends list and host avatar not getting updated 21 | */ 22 | 23 | "use strict"; 24 | if (typeof Listener === "undefined") return; 25 | const loadInterval = setInterval(() => { 26 | if (document.querySelector("#loadingScreen.hidden")) { 27 | clearInterval(loadInterval); 28 | setup(); 29 | } 30 | }, 500); 31 | 32 | let showPlayerColors = true; 33 | let showCustomColors = true; 34 | let customColorMap = {}; 35 | 36 | function setup() { 37 | new Listener("New Rooms", (data) => { 38 | for (let item of data?.standard ?? data) { 39 | setTimeout(() => { 40 | const room = roomBrowser.activeRooms[item.id]; 41 | if (room) { 42 | room.refreshRoomPlayers(); 43 | room.clickHostName(item.host); 44 | } 45 | }, 1); 46 | } 47 | }).bindListener(); 48 | new Listener("Room Change", (data) => { 49 | if (data.changeType === "players" || data.changeType === "spectators") { 50 | setTimeout(() => { 51 | const room = roomBrowser.activeRooms[data.roomId]; 52 | if (room) { 53 | room.updateFriends(); 54 | room.refreshRoomPlayers(); 55 | if (data.newHost) { 56 | room.updateAvatar(data.newHost.avatar); 57 | room.clickHostName(data.newHost.name); 58 | } 59 | } 60 | }, 1); 61 | } 62 | }).bindListener(); 63 | 64 | applyStyles(); 65 | AMQ_addScriptData({ 66 | name: "Show Room Players", 67 | author: "kempanator", 68 | version: GM_info.script.version, 69 | link: "https://github.com/kempanator/amq-scripts/raw/main/amqShowRoomPlayers.user.js", 70 | description: ` 71 | 78 | ` 79 | }); 80 | } 81 | 82 | // override updateFriends function to also show invisible friends 83 | RoomTile.prototype.updateFriends = function () { 84 | this._friendsInGameMap = {}; 85 | for (const player of this._players) { 86 | if (socialTab.isFriend(player)) { 87 | this._friendsInGameMap[player] = true; 88 | } 89 | } 90 | this.updateFriendInfo(); 91 | }; 92 | 93 | // override removeRoomTile function to also remove room players popover 94 | const oldRemoveRoomTile = RoomBrowser.prototype.removeRoomTile; 95 | RoomBrowser.prototype.removeRoomTile = function (tileId) { 96 | $(`#rbRoom-${tileId} .rbrProgressContainer`).popover("destroy"); 97 | oldRemoveRoomTile.apply(this, arguments); 98 | }; 99 | 100 | // add click event to host name to open player profile 101 | RoomTile.prototype.clickHostName = function (host) { 102 | this.$tile.find(".rbrHost") 103 | .css("cursor", "pointer") 104 | .off("click") 105 | .on("click", () => { 106 | playerProfileController.loadProfile(host, $(`#rbRoom-${this.id}`), {}, () => { }, false, true); 107 | }); 108 | }; 109 | 110 | // create or update room players popover 111 | RoomTile.prototype.refreshRoomPlayers = function () { 112 | const $progress = this.$tile.find(".rbrProgressContainer"); 113 | const players = [...this._players].sort((a, b) => a.localeCompare(b)); 114 | const $list = $("