Back to Home
Gistify Snippet
kyiov
3h ago·3 file(s)
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
Raw
amq-extractor.jsjavascript
277 lines
1#!/usr/bin/env node
2
3const fs = require('fs');
4const path = require('path');
5const { pipeline } = require('stream/promises');
6
7const DEFAULT_QUERY = process.argv.slice(2).join(' ').trim() || 'Frieren';
8const CACHE_FILE = path.join(__dirname, '.amq_master_cache.json');
9const CDN_SERVERS = [
10 'https://eudist.animemusicquiz.com/',
11 'https://nawdist.animemusicquiz.com/',
12 'https://naedist.animemusicquiz.com/',
13 'https://files.catbox.moe/'
14];
15
16const 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
22class 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
215async 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
269if (require.main === module) {
270 main().catch(err => {
271 console.error('Error:', err.message);
272 process.exit(1);
273 });
274}
275
276module.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.