Back to Home
Gistify Snippet
Public
AnimeMusicQuiz (AMQ) Song Catalog & Stream Resolver
Reverse-engineered client for animemusicquiz.com. Automates guest sessions, caches the 16MB master catalog, and resolves direct MP3/WebM CDN stream links via Socket.IO v4.
#anime#scraper#reverse-engineering#music#javascript
7
Rawamq-extractor.jsjavascript
277 lines| 1 | #!/usr/bin/env node |
| 2 | |
| 3 | const fs = require('fs'); |
| 4 | const path = require('path'); |
| 5 | const { pipeline } = require('stream/promises'); |
| 6 | |
| 7 | const DEFAULT_QUERY = process.argv.slice(2).join(' ').trim() || 'Frieren'; |
| 8 | const CACHE_FILE = path.join(__dirname, '.amq_master_cache.json'); |
| 9 | const CDN_SERVERS = [ |
| 10 | 'https://eudist.animemusicquiz.com/', |
| 11 | 'https://nawdist.animemusicquiz.com/', |
| 12 | 'https://naedist.animemusicquiz.com/', |
| 13 | 'https://files.catbox.moe/' |
| 14 | ]; |
| 15 | |
| 16 | const HEADERS = { |
| 17 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', |
| 18 | 'Origin': 'https://animemusicquiz.com', |
| 19 | 'Referer': 'https://animemusicquiz.com/' |
| 20 | }; |
| 21 | |
| 22 | class AMQScraper { |
| 23 | constructor() { |
| 24 | this.cookie = ''; |
| 25 | this.masterData = null; |
| 26 | } |
| 27 | |
| 28 | async loginGuest() { |
| 29 | const res = await fetch('https://animemusicquiz.com/signUpGuest', { |
| 30 | method: 'POST', |
| 31 | headers: { ...HEADERS, 'Content-Type': 'application/json' }, |
| 32 | body: JSON.stringify({ username: 'notUsed', password: 'notUsed' }) |
| 33 | }); |
| 34 | |
| 35 | if (!res.ok) throw new Error(`Guest auth failed: HTTP ${res.status}`); |
| 36 | |
| 37 | const rawCookies = res.headers.getSetCookie ? res.headers.getSetCookie() : [res.headers.get('set-cookie')]; |
| 38 | this.cookie = rawCookies.map(c => (c || '').split(';')[0]).filter(Boolean).join('; '); |
| 39 | return this.cookie; |
| 40 | } |
| 41 | |
| 42 | async loadMasterData(forceRefresh = false) { |
| 43 | if (!forceRefresh && fs.existsSync(CACHE_FILE)) { |
| 44 | try { |
| 45 | const stats = fs.statSync(CACHE_FILE); |
| 46 | const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60); |
| 47 | if (ageHours < 24) { |
| 48 | const content = fs.readFileSync(CACHE_FILE, 'utf-8'); |
| 49 | this.masterData = JSON.parse(content); |
| 50 | return this.masterData; |
| 51 | } |
| 52 | } catch {} |
| 53 | } |
| 54 | |
| 55 | if (!this.cookie) await this.loginGuest(); |
| 56 | |
| 57 | const res = await fetch('https://animemusicquiz.com/libraryMasterList?masterId=0', { |
| 58 | headers: { ...HEADERS, Cookie: this.cookie } |
| 59 | }); |
| 60 | |
| 61 | if (!res.ok) throw new Error(`Failed to load master list: HTTP ${res.status}`); |
| 62 | |
| 63 | this.masterData = await res.json(); |
| 64 | |
| 65 | try { |
| 66 | fs.writeFileSync(CACHE_FILE, JSON.stringify(this.masterData)); |
| 67 | } catch {} |
| 68 | |
| 69 | return this.masterData; |
| 70 | } |
| 71 | |
| 72 | getArtistName(song) { |
| 73 | if (!song) return 'Unknown'; |
| 74 | if (song.songArtistId && this.masterData.artistMap?.[song.songArtistId]) { |
| 75 | return this.masterData.artistMap[song.songArtistId].name; |
| 76 | } |
| 77 | if (song.songGroupId && this.masterData.groupMap?.[song.songGroupId]) { |
| 78 | return this.masterData.groupMap[song.songGroupId].name; |
| 79 | } |
| 80 | return 'Unknown'; |
| 81 | } |
| 82 | |
| 83 | formatAnimeEntry(anime) { |
| 84 | const songs = []; |
| 85 | for (const [type, links] of Object.entries(anime.songLinks || {})) { |
| 86 | for (const l of links) { |
| 87 | const songObj = this.masterData.songMap[l.songId]; |
| 88 | songs.push({ |
| 89 | songId: l.songId, |
| 90 | annSongId: l.annSongId, |
| 91 | type: `${type}${l.number || ''}`, |
| 92 | title: songObj?.name || 'Unknown', |
| 93 | artist: this.getArtistName(songObj) |
| 94 | }); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | return { |
| 99 | id: anime.annId, |
| 100 | titleEN: anime.mainNames?.EN || null, |
| 101 | titleJA: anime.mainNames?.JA || null, |
| 102 | category: anime.category?.name || 'Anime', |
| 103 | year: anime.year, |
| 104 | season: anime.seasonId === 1 ? 'Winter' : anime.seasonId === 2 ? 'Spring' : anime.seasonId === 3 ? 'Summer' : anime.seasonId === 4 ? 'Autumn' : null, |
| 105 | songs |
| 106 | }; |
| 107 | } |
| 108 | |
| 109 | searchAnime(query) { |
| 110 | if (!this.masterData) throw new Error('Database not loaded'); |
| 111 | const q = query.toLowerCase().trim(); |
| 112 | const matches = []; |
| 113 | |
| 114 | for (const anime of Object.values(this.masterData.animeMap)) { |
| 115 | if (anime.names?.some(n => n.name.toLowerCase().includes(q))) { |
| 116 | matches.push(this.formatAnimeEntry(anime)); |
| 117 | } |
| 118 | } |
| 119 | return matches; |
| 120 | } |
| 121 | |
| 122 | searchSongs(query) { |
| 123 | if (!this.masterData) throw new Error('Database not loaded'); |
| 124 | const q = query.toLowerCase().trim(); |
| 125 | const songIdToAnime = {}; |
| 126 | |
| 127 | for (const anime of Object.values(this.masterData.animeMap)) { |
| 128 | for (const [type, links] of Object.entries(anime.songLinks || {})) { |
| 129 | for (const l of links) { |
| 130 | songIdToAnime[l.songId] = { anime, type, number: l.number, annSongId: l.annSongId }; |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | const matches = []; |
| 136 | for (const song of Object.values(this.masterData.songMap)) { |
| 137 | const artist = this.getArtistName(song); |
| 138 | if (song.name.toLowerCase().includes(q) || artist.toLowerCase().includes(q)) { |
| 139 | const info = songIdToAnime[song.songId]; |
| 140 | matches.push({ |
| 141 | songId: song.songId, |
| 142 | annSongId: info ? info.annSongId : null, |
| 143 | title: song.name, |
| 144 | artist, |
| 145 | anime: info ? (info.anime.mainNames?.EN || info.anime.mainNames?.JA) : 'Unknown', |
| 146 | type: info ? `${info.type}${info.number || ''}` : 'Song', |
| 147 | year: info ? info.anime.year : null |
| 148 | }); |
| 149 | } |
| 150 | } |
| 151 | return matches; |
| 152 | } |
| 153 | |
| 154 | async resolveSongMedia(annSongId) { |
| 155 | if (!this.cookie) await this.loginGuest(); |
| 156 | |
| 157 | const tokenRes = await fetch('https://animemusicquiz.com/socketToken', { |
| 158 | headers: { ...HEADERS, Cookie: this.cookie } |
| 159 | }); |
| 160 | const { token } = await tokenRes.json(); |
| 161 | |
| 162 | const socketUrl = 'https://socket.animemusicquiz.com/socket.io/?EIO=4&transport=polling'; |
| 163 | const hsRes = await fetch(`${socketUrl}&token=${token}`); |
| 164 | const hsText = await hsRes.text(); |
| 165 | const sid = JSON.parse(hsText.slice(1)).sid; |
| 166 | |
| 167 | await fetch(`${socketUrl}&sid=${sid}`, { method: 'POST', body: '40' }); |
| 168 | await fetch(`${socketUrl}&sid=${sid}`); |
| 169 | |
| 170 | const cmd = { |
| 171 | type: 'library', |
| 172 | command: 'get song extended info', |
| 173 | data: { annSongId, includeFileNames: true } |
| 174 | }; |
| 175 | |
| 176 | await fetch(`${socketUrl}&sid=${sid}`, { |
| 177 | method: 'POST', |
| 178 | body: '42' + JSON.stringify(['command', cmd]) |
| 179 | }); |
| 180 | |
| 181 | const pollResp = await (await fetch(`${socketUrl}&sid=${sid}`)).text(); |
| 182 | const match = pollResp.match(/42\["command",\s*(\{.*?"get song extended info".*?\})\s*\]/); |
| 183 | if (!match) throw new Error('Socket command response not found'); |
| 184 | |
| 185 | const extInfo = JSON.parse(match[1]).data; |
| 186 | const fileNameMap = extInfo.fileNameMap || {}; |
| 187 | |
| 188 | return { |
| 189 | annSongId, |
| 190 | annId: extInfo.annId, |
| 191 | songId: extInfo.songId, |
| 192 | globalGuessRate: `${extInfo.globalPercent || 0}%`, |
| 193 | mediaUrls: { |
| 194 | mp3: fileNameMap['0'] ? `${CDN_SERVERS[0]}${fileNameMap['0']}` : null, |
| 195 | video480p: fileNameMap['480'] ? `${CDN_SERVERS[0]}${fileNameMap['480']}` : null, |
| 196 | video720p: fileNameMap['720'] ? `${CDN_SERVERS[0]}${fileNameMap['720']}` : null, |
| 197 | catboxMp3: fileNameMap['0'] ? `https://files.catbox.moe/${fileNameMap['0']}` : null, |
| 198 | catboxVideo: fileNameMap['720'] ? `https://files.catbox.moe/${fileNameMap['720']}` : (fileNameMap['480'] ? `https://files.catbox.moe/${fileNameMap['480']}` : null), |
| 199 | files: fileNameMap |
| 200 | } |
| 201 | }; |
| 202 | } |
| 203 | |
| 204 | async downloadFile(url, destPath) { |
| 205 | fs.mkdirSync(path.dirname(destPath), { recursive: true }); |
| 206 | const res = await fetch(url, { headers: { 'User-Agent': HEADERS['User-Agent'] } }); |
| 207 | if (!res.ok) throw new Error(`HTTP ${res.status}`); |
| 208 | const fileStream = fs.createWriteStream(destPath); |
| 209 | await pipeline(res.body, fileStream); |
| 210 | const stat = fs.statSync(destPath); |
| 211 | return (stat.size / (1024 * 1024)).toFixed(2); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | async function main() { |
| 216 | const query = DEFAULT_QUERY; |
| 217 | const amq = new AMQScraper(); |
| 218 | |
| 219 | await amq.loadMasterData(); |
| 220 | |
| 221 | const animeMatches = amq.searchAnime(query); |
| 222 | |
| 223 | if (animeMatches.length > 0) { |
| 224 | const target = animeMatches[0]; |
| 225 | console.log(`[anime] ${target.titleEN || target.titleJA} (${target.year || '-'})`); |
| 226 | if (target.titleJA && target.titleEN) console.log(`[alias] ${target.titleJA}`); |
| 227 | console.log(`[format] ${target.category} / ${target.season || '-'}`); |
| 228 | console.log(`[tracks] ${target.songs.length} song(s)`); |
| 229 | |
| 230 | target.songs.forEach((s, i) => { |
| 231 | console.log(` ${i + 1}. [${s.type}] ${s.title} - ${s.artist} (annSongId: ${s.annSongId})`); |
| 232 | }); |
| 233 | |
| 234 | const firstSong = target.songs[0]; |
| 235 | if (firstSong && firstSong.annSongId) { |
| 236 | console.log(`\nResolving stream for [${firstSong.type}] ${firstSong.title}...`); |
| 237 | const media = await amq.resolveSongMedia(firstSong.annSongId); |
| 238 | console.log(`Audio MP3 : ${media.mediaUrls.mp3 || '-'}`); |
| 239 | console.log(`Video 720p : ${media.mediaUrls.video720p || '-'}`); |
| 240 | console.log(`Video 480p : ${media.mediaUrls.video480p || '-'}`); |
| 241 | console.log(`Catbox : ${media.mediaUrls.catboxMp3 || '-'}`); |
| 242 | console.log(`Guess rate : ${media.globalGuessRate}`); |
| 243 | } |
| 244 | return; |
| 245 | } |
| 246 | |
| 247 | const songMatches = amq.searchSongs(query); |
| 248 | if (songMatches.length > 0) { |
| 249 | console.log(`Found ${songMatches.length} matching track(s):`); |
| 250 | songMatches.slice(0, 10).forEach((s, i) => { |
| 251 | console.log(` ${i + 1}. [${s.type}] ${s.title} - ${s.artist} (Anime: ${s.anime})`); |
| 252 | }); |
| 253 | |
| 254 | const firstSong = songMatches[0]; |
| 255 | if (firstSong?.annSongId) { |
| 256 | console.log(`\nResolving stream for ${firstSong.title}...`); |
| 257 | const media = await amq.resolveSongMedia(firstSong.annSongId); |
| 258 | console.log(`Audio MP3 : ${media.mediaUrls.mp3 || '-'}`); |
| 259 | console.log(`Video 720p : ${media.mediaUrls.video720p || '-'}`); |
| 260 | console.log(`Catbox : ${media.mediaUrls.catboxMp3 || '-'}`); |
| 261 | console.log(`Guess rate : ${media.globalGuessRate}`); |
| 262 | } |
| 263 | return; |
| 264 | } |
| 265 | |
| 266 | console.log(`No results for "${query}"`); |
| 267 | } |
| 268 | |
| 269 | if (require.main === module) { |
| 270 | main().catch(err => { |
| 271 | console.error('Error:', err.message); |
| 272 | process.exit(1); |
| 273 | }); |
| 274 | } |
| 275 | |
| 276 | module.exports = AMQScraper; |
| 277 | |
Discussion & Comments0
No comments yet — be the first to leave feedback or start a code discussion.
Sign in with your GitHub account to write comments and join the discussion.