Skip to content

Instantly share code, notes, and snippets.

@rama-adi
Last active November 16, 2025 14:08
Show Gist options
  • Select an option

  • Save rama-adi/9f32e8b97fd8aed5b04fed8d0ace3eb8 to your computer and use it in GitHub Desktop.

Select an option

Save rama-adi/9f32e8b97fd8aed5b04fed8d0ace3eb8 to your computer and use it in GitHub Desktop.
Blue gate (or any gate idk) kaleidxscope checker
(function () {
if(window.location.hostname !== "maimaidx-eng.com" || window.location.pathname !== "/maimai-mobile/home/userOption/favorite/updateMusic") {
const go = confirm("This bookmarklet should be run on the music favorite list page, do you want to go there and rerun the script");
if(go) {
window.location.href = "https://maimaidx-eng.com/maimai-mobile/home/userOption/favorite/updateMusic"
}
return
}
// basically you just need to tweak this for the correct gate
const GATE_CFG = {
gateName: "青の扉",
unlockDate: new Date("2025-01-16T10:00:00+09:00"),
songs: [
'STEREOSCAPE',
'Crazy Circle',
'Ututu',
'シエルブルーマルシェ',
'ブレインジャックシンドローム',
'共鳴',
'REAL VOICE',
'オリフィス',
'ユメヒバナ',
'パラボラ',
'星めぐり、果ての君へ。',
'スローアライズ',
'生命不詳',
'チエルカ/エソテリカ',
'RIFFRAIN',
'Falling',
'ピリオドサイン',
'群青シグナル',
'アンバークロニクル',
'Kairos',
'リフヴェイン',
'宵の鳥',
'フタタビ',
'シックスプラン',
'ふらふらふら、',
'フェイクフェイス・フェイルセイフ',
'パラドクスイヴ',
'YKWTD',
'184億回のマルチトニック'
]
}
const host = document.createElement('div');
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>#modal,#modal button{border-radius:.375rem}:host{position:fixed;inset:0;z-index:999999; font-family:"ヒラギノ角ゴ Pro W3", メイリオ, Meiryo, "MS Pゴシック", "MS P Gothic", sans-serif;}#backdrop{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(17,24,39,.3);z-index:999998}#modal{width:26rem;background:#bee5fa;padding:1.5rem;border-width:3px;box-shadow:rgba(0,0,0,.4) 1px 3px 0 0;z-index:999999}#modal h1{margin-bottom:1rem;font-size:1.25rem;font-weight:700}#modal .section{margin-top:1rem;margin-bottom:1rem}#modal .loading,#modal ol{margin-top:.5rem}#modal .loading,#modal .section p,#modal li{margin-bottom:.5rem}#modal ol{list-style:decimal inside;margin-left:1.25rem;padding-left:0}#song-list{margin-top:.75rem;padding-left:0;max-height:12rem;overflow:auto;border:1px solid rgba(31,41,55,.25);border-radius:.5rem;background:#f8fdff;padding:.5rem .75rem}#song-list li{margin-bottom:.35rem;font-size:.9rem;display:flex;align-items:center;gap:.35rem}#song-list .icon{width:1.5rem;text-align:center}#modal .btn-row{display:flex;gap:1rem;margin-top:1rem}#modal button{padding:.5rem .75rem;cursor:pointer;font-size:.875rem}#modal .btn-light{background:#f3f4f6;color:#1f2937}#modal .btn-dark{background:#1f2937;color:#fff}</style>
<div id="backdrop">
<div id="modal">
<h1>${GATE_CFG.gateName} Song checker</h1>
<div class="section">
<p>Checking ${GATE_CFG.gateName} song played on or after ${GATE_CFG.unlockDate.toLocaleDateString()}</p>
<p>Below are all songs with their current status:</p>
<ol id="song-list"></ol>
</div>
<p id="current-checking" class="loading"></p>
<p id="loading-indicator" class="loading"></p>
<div class="btn-row" id="action-row"></div>
</div>
</div>
`;
const songStatuses = new Map();
GATE_CFG.songs.forEach(title => songStatuses.set(title, "unplayed"));
function getUnplayedTitles() {
return GATE_CFG.songs.filter(title => songStatuses.get(title) === "unplayed");
}
function renderSongList(currentTitle = null) {
const sl = shadow.getElementById("song-list");
if (!sl) return;
sl.innerHTML = GATE_CFG.songs.map(title => {
let icon = "❌";
if (title === currentTitle) {
icon = "➡️";
} else if (songStatuses.get(title) === "completed") {
icon = "✅";
}
return `<li><span class="icon">${icon}</span><span>${title}</span></li>`;
}).join("");
}
renderSongList();
// :(
async function htmlGetFetch(url) {
const result = await fetch(url, {
credentials: 'include'
});
return await result.text();
}
function searchSong(html, titles) {
const base = 'https://maimaidx-eng.com/maimai-mobile/record/musicDetail/';
const doc = new DOMParser().parseFromString(html, 'text/html');
// All song blocks on the page
const blocks = [...doc.querySelectorAll('.w_450.m_15.p_r.f_0')];
const entries = blocks.map(block => {
const titleEl = block.querySelector('.music_name_block');
const form = block.querySelector('form[action*="musicDetail"]');
const idx = form?.querySelector('input[name="idx"]')?.value;
return {
title: titleEl?.textContent.trim() || null,
idx: idx || null
};
});
// Normalize for matching
const normalize = s => s.replace(/\s+/g, '').trim();
const results = titles.map(t => {
const match = entries.find(e => e.title && normalize(e.title) === normalize(t));
if (!match || !match.idx) {
return { title: t, url: null };
}
return {
title: t,
url: `${base}?idx=${encodeURIComponent(match.idx)}`
};
});
return results;
}
function parsePlay(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
const diffNames = {
basic: "basic",
advanced: "advanced",
expert: "expert",
master: "master",
remaster: "remaster"
};
const parseJST = (dateStr) => {
// Expect "YYYY/MM/DD HH:mm"
const m = dateStr.match(/^(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2})$/);
if (!m) return null;
const [_, y, mo, d, h, mi] = m;
const iso = `${y}-${mo}-${d}T${h}:${mi}:00+09:00`; // Safe ISO with JST offset
return new Date(iso);
}
const results = [];
for (const id of Object.keys(diffNames)) {
const block = doc.querySelector(`#${id}`);
if (!block) continue;
const level = block.querySelector(".music_lv_back")?.textContent.trim() || null;
const isDX = !!block.querySelector('.music_kind_icon[src*="music_dx"]');
const scorePercent =
block.querySelector(".music_score_block.w_120")?.textContent.trim() || null;
const deluxeBlock = block.querySelector(".music_score_block.w_310");
let deluxeScore = null;
let deluxeMax = null;
if (deluxeBlock) {
const text = deluxeBlock.textContent.replace(/\s+/g, " ").trim();
const match = text.match(/(\d+)\s*\/\s*(\d+)/);
if (match) {
deluxeScore = Number(match[1]);
deluxeMax = Number(match[2]);
}
}
let lastPlayedRaw = null;
let playCount = null;
const infoRows = block.querySelectorAll("table.collapse tr");
infoRows.forEach(tr => {
const tds = [...tr.querySelectorAll("td")];
const label = tds[0]?.textContent.trim();
const val = tds[1]?.textContent.trim();
if (label?.startsWith("Last played")) lastPlayedRaw = val;
if (label?.startsWith("PLAY COUNT")) playCount = Number(val);
});
results.push({
difficulty: diffNames[id],
level,
isDX,
scorePercent,
deluxeScore,
deluxeMax,
lastPlayed: lastPlayedRaw ? parseJST(lastPlayedRaw) : null, // converted
playCount
});
}
return results;
}
async function loadData() {
const indicatorEl = shadow.getElementById("loading-indicator");
const statusEl = shadow.getElementById("current-checking");
indicatorEl.innerHTML = `Loading songs from ${GATE_CFG.gateName}...<br>`;
statusEl.innerHTML = "";
renderSongList();
try {
const songListHtml = await htmlGetFetch("https://maimaidx-eng.com/maimai-mobile/record/musicGenre/search/?genre=99&diff=0");
const searchResults = searchSong(songListHtml, GATE_CFG.songs);
indicatorEl.innerHTML += `Fetched ${searchResults.length} songs. Checking play history...<br>`;
const missingSongs = [];
const failedSongs = [];
let processed = 0;
const total = searchResults.length || 1;
for (const result of searchResults) {
if (!result.url) {
missingSongs.push(result.title);
songStatuses.set(result.title, "unplayed");
processed += 1;
indicatorEl.innerHTML = `Checked ${processed}/${total} songs...<br>`;
renderSongList();
continue;
}
statusEl.innerHTML = `Currently checking ${result.title}...`;
renderSongList(result.title);
try {
const playHtml = await htmlGetFetch(result.url);
const parsedPlays = parsePlay(playHtml);
const latestPlay = parsedPlays.reduce((latest, entry) => {
if (!entry.lastPlayed) return latest;
if (!latest || entry.lastPlayed > latest) return entry.lastPlayed;
return latest;
}, null);
if (!latestPlay || latestPlay < GATE_CFG.unlockDate) {
songStatuses.set(result.title, "unplayed");
} else {
songStatuses.set(result.title, "completed");
}
} catch (err) {
console.error(`Failed to load play data for ${result.title}`, err);
failedSongs.push(result.title);
songStatuses.set(result.title, "unplayed");
}
processed += 1;
indicatorEl.innerHTML = `Checked ${processed}/${total} songs...<br>`;
renderSongList();
}
statusEl.innerHTML = "Finished checking songs.";
const remainingSongs = getUnplayedTitles();
indicatorEl.innerHTML += `Found ${remainingSongs.length} song(s) that still need to be played.<br>`;
if (missingSongs.length || failedSongs.length) {
indicatorEl.innerHTML += `Skipped: ${(missingSongs.concat(failedSongs)).join(", ")}<br>`;
}
return { missingSongs, failedSongs };
} catch (error) {
statusEl.innerHTML = "Failed to load songs.";
indicatorEl.innerHTML += `An error occurred: ${error.message}`;
throw error;
}
}
loadData()
.catch(err => {
console.error("Unable to complete song checks.", err);
})
.finally(() => {
shadow.getElementById("action-row").innerHTML = `
<button class="btn-light" id="closeBtn">Close</button>
<button class="btn-dark" id="addBtn">Add to favorites</button>
`
shadow.getElementById('addBtn').addEventListener('click', () => {
const remainingSongs = getUnplayedTitles();
if (!remainingSongs.length) {
alert("All songs have already been played since the gate opened!");
return;
}
const nodes = document.querySelectorAll('.favorite_checkbox_frame.m_10');
nodes.forEach(node => {
if (remainingSongs.some(song => node.innerText.includes(song))) {
node.click();
}
});
alert("Songs are checked, now you just need to save them :-)");
host.remove();
});
shadow.getElementById('closeBtn').addEventListener('click', () => {
host.remove();
});
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment