#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { pipeline } = require('stream/promises'); const DEFAULT_QUERY = process.argv.slice(2).join(' ').trim() || 'Frieren'; const CACHE_FILE = path.join(__dirname, '.amq_master_cache.json'); const CDN_SERVERS = [ 'https://eudist.animemusicquiz.com/', 'https://nawdist.animemusicquiz.com/', 'https://naedist.animemusicquiz.com/', 'https://files.catbox.moe/' ]; const HEADERS = { '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', 'Origin': 'https://animemusicquiz.com', 'Referer': 'https://animemusicquiz.com/' }; class AMQScraper { constructor() { this.cookie = ''; this.masterData = null; } async loginGuest() { const res = await fetch('https://animemusicquiz.com/signUpGuest', { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'notUsed', password: 'notUsed' }) }); if (!res.ok) throw new Error(`Guest auth failed: HTTP ${res.status}`); const rawCookies = res.headers.getSetCookie ? res.headers.getSetCookie() : [res.headers.get('set-cookie')]; this.cookie = rawCookies.map(c => (c || '').split(';')[0]).filter(Boolean).join('; '); return this.cookie; } async loadMasterData(forceRefresh = false) { if (!forceRefresh && fs.existsSync(CACHE_FILE)) { try { const stats = fs.statSync(CACHE_FILE); const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60); if (ageHours < 24) { const content = fs.readFileSync(CACHE_FILE, 'utf-8'); this.masterData = JSON.parse(content); return this.masterData; } } catch {} } if (!this.cookie) await this.loginGuest(); const res = await fetch('https://animemusicquiz.com/libraryMasterList?masterId=0', { headers: { ...HEADERS, Cookie: this.cookie } }); if (!res.ok) throw new Error(`Failed to load master list: HTTP ${res.status}`); this.masterData = await res.json(); try { fs.writeFileSync(CACHE_FILE, JSON.stringify(this.masterData)); } catch {} return this.masterData; } getArtistName(song) { if (!song) return 'Unknown'; if (song.songArtistId && this.masterData.artistMap?.[song.songArtistId]) { return this.masterData.artistMap[song.songArtistId].name; } if (song.songGroupId && this.masterData.groupMap?.[song.songGroupId]) { return this.masterData.groupMap[song.songGroupId].name; } return 'Unknown'; } formatAnimeEntry(anime) { const songs = []; for (const [type, links] of Object.entries(anime.songLinks || {})) { for (const l of links) { const songObj = this.masterData.songMap[l.songId]; songs.push({ songId: l.songId, annSongId: l.annSongId, type: `${type}${l.number || ''}`, title: songObj?.name || 'Unknown', artist: this.getArtistName(songObj) }); } } return { id: anime.annId, titleEN: anime.mainNames?.EN || null, titleJA: anime.mainNames?.JA || null, category: anime.category?.name || 'Anime', year: anime.year, season: anime.seasonId === 1 ? 'Winter' : anime.seasonId === 2 ? 'Spring' : anime.seasonId === 3 ? 'Summer' : anime.seasonId === 4 ? 'Autumn' : null, songs }; } searchAnime(query) { if (!this.masterData) throw new Error('Database not loaded'); const q = query.toLowerCase().trim(); const matches = []; for (const anime of Object.values(this.masterData.animeMap)) { if (anime.names?.some(n => n.name.toLowerCase().includes(q))) { matches.push(this.formatAnimeEntry(anime)); } } return matches; } searchSongs(query) { if (!this.masterData) throw new Error('Database not loaded'); const q = query.toLowerCase().trim(); const songIdToAnime = {}; for (const anime of Object.values(this.masterData.animeMap)) { for (const [type, links] of Object.entries(anime.songLinks || {})) { for (const l of links) { songIdToAnime[l.songId] = { anime, type, number: l.number, annSongId: l.annSongId }; } } } const matches = []; for (const song of Object.values(this.masterData.songMap)) { const artist = this.getArtistName(song); if (song.name.toLowerCase().includes(q) || artist.toLowerCase().includes(q)) { const info = songIdToAnime[song.songId]; matches.push({ songId: song.songId, annSongId: info ? info.annSongId : null, title: song.name, artist, anime: info ? (info.anime.mainNames?.EN || info.anime.mainNames?.JA) : 'Unknown', type: info ? `${info.type}${info.number || ''}` : 'Song', year: info ? info.anime.year : null }); } } return matches; } async resolveSongMedia(annSongId) { if (!this.cookie) await this.loginGuest(); const tokenRes = await fetch('https://animemusicquiz.com/socketToken', { headers: { ...HEADERS, Cookie: this.cookie } }); const { token } = await tokenRes.json(); const socketUrl = 'https://socket.animemusicquiz.com/socket.io/?EIO=4&transport=polling'; const hsRes = await fetch(`${socketUrl}&token=${token}`); const hsText = await hsRes.text(); const sid = JSON.parse(hsText.slice(1)).sid; await fetch(`${socketUrl}&sid=${sid}`, { method: 'POST', body: '40' }); await fetch(`${socketUrl}&sid=${sid}`); const cmd = { type: 'library', command: 'get song extended info', data: { annSongId, includeFileNames: true } }; await fetch(`${socketUrl}&sid=${sid}`, { method: 'POST', body: '42' + JSON.stringify(['command', cmd]) }); const pollResp = await (await fetch(`${socketUrl}&sid=${sid}`)).text(); const match = pollResp.match(/42\["command",\s*(\{.*?"get song extended info".*?\})\s*\]/); if (!match) throw new Error('Socket command response not found'); const extInfo = JSON.parse(match[1]).data; const fileNameMap = extInfo.fileNameMap || {}; return { annSongId, annId: extInfo.annId, songId: extInfo.songId, globalGuessRate: `${extInfo.globalPercent || 0}%`, mediaUrls: { mp3: fileNameMap['0'] ? `${CDN_SERVERS[0]}${fileNameMap['0']}` : null, video480p: fileNameMap['480'] ? `${CDN_SERVERS[0]}${fileNameMap['480']}` : null, video720p: fileNameMap['720'] ? `${CDN_SERVERS[0]}${fileNameMap['720']}` : null, catboxMp3: fileNameMap['0'] ? `https://files.catbox.moe/${fileNameMap['0']}` : null, catboxVideo: fileNameMap['720'] ? `https://files.catbox.moe/${fileNameMap['720']}` : (fileNameMap['480'] ? `https://files.catbox.moe/${fileNameMap['480']}` : null), files: fileNameMap } }; } async downloadFile(url, destPath) { fs.mkdirSync(path.dirname(destPath), { recursive: true }); const res = await fetch(url, { headers: { 'User-Agent': HEADERS['User-Agent'] } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const fileStream = fs.createWriteStream(destPath); await pipeline(res.body, fileStream); const stat = fs.statSync(destPath); return (stat.size / (1024 * 1024)).toFixed(2); } } async function main() { const query = DEFAULT_QUERY; const amq = new AMQScraper(); await amq.loadMasterData(); const animeMatches = amq.searchAnime(query); if (animeMatches.length > 0) { const target = animeMatches[0]; console.log(`[anime] ${target.titleEN || target.titleJA} (${target.year || '-'})`); if (target.titleJA && target.titleEN) console.log(`[alias] ${target.titleJA}`); console.log(`[format] ${target.category} / ${target.season || '-'}`); console.log(`[tracks] ${target.songs.length} song(s)`); target.songs.forEach((s, i) => { console.log(` ${i + 1}. [${s.type}] ${s.title} - ${s.artist} (annSongId: ${s.annSongId})`); }); const firstSong = target.songs[0]; if (firstSong && firstSong.annSongId) { console.log(`\nResolving stream for [${firstSong.type}] ${firstSong.title}...`); const media = await amq.resolveSongMedia(firstSong.annSongId); console.log(`Audio MP3 : ${media.mediaUrls.mp3 || '-'}`); console.log(`Video 720p : ${media.mediaUrls.video720p || '-'}`); console.log(`Video 480p : ${media.mediaUrls.video480p || '-'}`); console.log(`Catbox : ${media.mediaUrls.catboxMp3 || '-'}`); console.log(`Guess rate : ${media.globalGuessRate}`); } return; } const songMatches = amq.searchSongs(query); if (songMatches.length > 0) { console.log(`Found ${songMatches.length} matching track(s):`); songMatches.slice(0, 10).forEach((s, i) => { console.log(` ${i + 1}. [${s.type}] ${s.title} - ${s.artist} (Anime: ${s.anime})`); }); const firstSong = songMatches[0]; if (firstSong?.annSongId) { console.log(`\nResolving stream for ${firstSong.title}...`); const media = await amq.resolveSongMedia(firstSong.annSongId); console.log(`Audio MP3 : ${media.mediaUrls.mp3 || '-'}`); console.log(`Video 720p : ${media.mediaUrls.video720p || '-'}`); console.log(`Catbox : ${media.mediaUrls.catboxMp3 || '-'}`); console.log(`Guess rate : ${media.globalGuessRate}`); } return; } console.log(`No results for "${query}"`); } if (require.main === module) { main().catch(err => { console.error('Error:', err.message); process.exit(1); }); } module.exports = AMQScraper;