Skip to content

Instantly share code, notes, and snippets.

@heguro
Created July 25, 2026 21:58
Show Gist options
  • Select an option

  • Save heguro/198778412256731d0f4cc7b6c84ea7c8 to your computer and use it in GitHub Desktop.

Select an option

Save heguro/198778412256731d0f4cc7b6c84ea7c8 to your computer and use it in GitHub Desktop.
saku-scores-dl
javascript:(async()%3D%3E%7B%22use%20strict%22%3Bif(window.__sakuScoresDl%3F.running)%7Bconsole.warn(%22%5Bsaku-scores-dl%5D%20%5Cu3059%5Cu3067%5Cu306B%5Cu5B9F%5Cu884C%5Cu4E2D%5Cu3067%5Cu3059%22)%3Breturn%7Dconst%20d%3D%7Brunning%3A!0%2Ccancelled%3A!1%2Ccancel()%7Bthis.cancelled%3D!0%7D%7D%3Bwindow.__sakuScoresDl%3Dd%2Cdocument.getElementById(%22saku-scores-dl-progress%22)%3F.remove()%3Bconst%20a%3Ddocument.createElement(%22div%22)%3Ba.id%3D%22saku-scores-dl-progress%22%2Ca.style.cssText%3D%5B%22position%3Afixed%22%2C%22left%3A12px%22%2C%22right%3A12px%22%2C%22bottom%3A12px%22%2C%22z-index%3A2147483647%22%2C%22display%3Aflex%22%2C%22align-items%3Acenter%22%2C%22gap%3A12px%22%2C%22padding%3A12px%22%2C%22border-radius%3A8px%22%2C%22background%3Argba(0%2C0%2C0%2C.85)%22%2C%22color%3A%23fff%22%2C%22font%3A14px%2F1.4%20sans-serif%22%2C%22box-shadow%3A0%202px%2012px%20rgba(0%2C0%2C0%2C.35)%22%5D.join(%22%3B%22)%3Bconst%20u%3Ddocument.createElement(%22span%22)%3Bu.style.cssText%3D%22flex%3A1%3Bmin-width%3A0%22%3Bconst%20l%3Ddocument.createElement(%22button%22)%3Bl.type%3D%22button%22%2Cl.textContent%3D%22%5Cu4E2D%5Cu6B62%22%2Cl.style.cssText%3D%5B%22flex%3Anone%22%2C%22padding%3A8px%2012px%22%2C%22border%3A0%22%2C%22border-radius%3A6px%22%2C%22background%3A%23fff%22%2C%22color%3A%23000%22%2C%22font%3Ainherit%22%5D.join(%22%3B%22)%2Cl.onclick%3D()%3D%3E%7Bd.cancel()%2Cu.textContent%3D%22%5Cu4E2D%5Cu6B62%5Cu3057%5Cu3066%5Cu3044%5Cu307E%5Cu3059%5Cu2026%22%2Cl.disabled%3D!0%7D%2Ca.append(u%2Cl)%2Cdocument.body.append(a)%3Bfunction%20f(e)%7Bu.textContent%3De%7Dfunction%20h(e)%7Bu.textContent%3De%2Cl.disabled%3D!1%2Cl.textContent%3D%22%5Cu9589%5Cu3058%5Cu308B%22%2Cl.onclick%3D()%3D%3Ea.remove()%7Df(%22%5Cu6E96%5Cu5099%5Cu4E2D%5Cu2026%22)%3Bconst%20w%3D%7Bez%3A%22EASY%22%2Cnm%3A%22NORMAL%22%2Chd%3A%22HARD%22%2Cex%3A%22EXPERT%22%2Cmas%3A%22MASTER%22%7D%2Cy%3D%7B%22FLUFFY%5Cu2661POPPING%5Cu2606SUMMER!!!%22%3A%22FLUFFY%5Cu2661POPPING%5Cu2606SUMMER%5Cu200B!%5Cu200B!%5Cu200B!%22%2C%22edge%2F%2Flord%22%3A%22edge_lord%22%7D%2CE%3D%2F%5B%5Cu200b%5Cu200c%5Cu200d%5Cufeff%5D%2Fg%2Cb%3De%3D%3Enew%20Promise(r%3D%3EsetTimeout(r%2Ce))%2CS%3De%3D%3EString(e%3F%3F%22%22).replace(E%2C%22%22)%3Bfunction%20k()%7Bconst%20e%3D%5Bnew%20URL(location.href).searchParams.get(%22no%22)%2Cdocument.querySelector('%5Bname%3D%22no%22%5D')%3F.value%2Cdocument.querySelector(%22%5Bdata-account-no%5D%22)%3F.dataset.accountNo%5D%3Bfor(const%20t%20of%20e)if(%2F%5E%5Cd%2B%24%2F.test(String(t%3F%3F%22%22)))return%20String(t)%3Breturn%20document.documentElement.innerHTML.match(%2F(%3F%3Aaccount%5B_-%5D%3Fno%7Cmember%5B_-%5D%3Fno%7C%5Cbno)%5B%22'%5Cs%3A%3D%5D%2B(%5Cd%7B1%2C12%7D)%2Fi)%3F.%5B1%5D%3F%3F%22%22%7Dfunction%20%24(e)%7Bif(!e.trim())return%200%3Bconst%20r%3D%2F%5E%5Cd%7B4%7D-%5Cd%7B2%7D-%5Cd%7B2%7D%24%2F.test(e.trim())%3F%60%24%7Be.trim()%7DT00%3A00%3A00%2B09%3A00%60%3Ae.trim()%2Ct%3DDate.parse(r)%3Bif(!Number.isFinite(t))throw%20new%20Error(%60%5Cu65E5%5Cu6642%5Cu3092%5Cu89E3%5Cu91C8%5Cu3067%5Cu304D%5Cu307E%5Cu305B%5Cu3093%3A%20%24%7Be%7D%60)%3Breturn%20Math.floor(t%2F1e3)%7Dfunction%20T(e%2Cr)%7Breturn%20String(e%3F%3F%22%22).match(new%20RegExp(%60%2F%24%7Br%7D%2F(%5B%5E%2F%3F%22'%5D%2B)%5C%5C.png%60))%3F.%5B1%5D%3F%3Fnull%7Dfunction%20P(e%2Cr)%7Bconst%20t%3DS(e.title)%2Cn%3DString(e.play_date).replaceAll(%22.%22%2C%22-%22)%2Co%3DMath.floor(Date.parse(%60%24%7Bn%7DT%24%7Be.play_time%7DZ%60)%2F1e3)%2Cs%3DString(e.level_img).match(%2Flevel_(ez%7Cnm%7Chd%7Cex%7Cmas)_%2F)%3F.%5B1%5D%2Cg%3DString(e.play_grade).match(%2Fgrade_(perfectCircle%7CfullCircle%7Cclear%7Cfailed)%5C.png%2F)%3F.%5B1%5D%2Cc%3DNumber(String(e.score).replace(%22.%22%2C%22%22))%2Ci%3Dy%5Bt%5D%3F%3Ft%3Bif(!i)throw%20new%20Error(%22%5Cu66F2%5Cu540D%5Cu304C%5Cu7A7A%5Cu3067%5Cu3059%22)%3Bif(!w%5Bs%5D)throw%20new%20Error(%60%24%7Bt%7D%3A%20%5Cu96E3%5Cu6613%5Cu5EA6%5Cu3092%5Cu89E3%5Cu91C8%5Cu3067%5Cu304D%5Cu307E%5Cu305B%5Cu3093%60)%3Bif(!Number.isInteger(c)%7C%7Cc%3C0%7C%7Cc%3E101e3)throw%20new%20Error(%60%24%7Bt%7D%3A%20%5Cu30B9%5Cu30B3%5Cu30A2%5Cu3092%5Cu89E3%5Cu91C8%5Cu3067%5Cu304D%5Cu307E%5Cu305B%5Cu3093%20(%24%7Be.score%7D)%60)%3Bif(!Number.isInteger(o)%7C%7Co%3C%3D0)throw%20new%20Error(%60%24%7Bt%7D%3A%20%5Cu30D7%5Cu30EC%5Cu30A4%5Cu65E5%5Cu6642%5Cu3092%5Cu89E3%5Cu91C8%5Cu3067%5Cu304D%5Cu307E%5Cu305B%5Cu3093%60)%3Breturn%7Bjacket_key%3Ai%2Ctitle%3At%2Cdifficulty%3Aw%5Bs%5D%2Cscore_milli%3Ac%2Cjudgments%3Anull%2Cgrade%3Ag%3F%3Fnull%2Cplayed_at%3Ao%2Ccreated_at%3Ar%2Csource%3A%22import%22%2Canalyzer_raw%3A%7Bprovider%3A%22chrono-circle-official%22%2Cplay_no%3ANumber(e.play_no)%2Csong_img%3AT(e.song_img%2C%22song_img%22)%7D%2Ccorrected_fields%3A%5B%5D%7D%7Dasync%20function%20I(e%2Cr)%7Blet%20t%3Bfor(let%20n%3D1%3Bn%3C%3D3%3Bn%2B%3D1)%7Bif(d.cancelled)throw%20new%20Error(%22%5Cu30AD%5Cu30E3%5Cu30F3%5Cu30BB%5Cu30EB%5Cu3055%5Cu308C%5Cu307E%5Cu3057%5Cu305F%22)%3Btry%7Bconst%20o%3Dawait%20fetch(%22%2Flogic%2Fajax.my.play_list.php%22%2C%7Bmethod%3A%22POST%22%2Ccredentials%3A%22same-origin%22%2Cheaders%3A%7B%22Content-Type%22%3A%22application%2Fx-www-form-urlencoded%22%7D%2Cbody%3Anew%20URLSearchParams(%7Bno%3Ae%2Cpage%3AString(r)%7D)%7D)%3Bif(!o.ok)throw%20new%20Error(%60HTTP%20%24%7Bo.status%7D%60)%3Bconst%20s%3Dawait%20o.json()%3Bif(s.state%3D%3D%3D500)throw%20new%20Error(s.msg%7C%7C%22API%20state%3D500%22)%3Bif(!Array.isArray(s.data))throw%20new%20Error(%22API%5Cu30EC%5Cu30B9%5Cu30DD%5Cu30F3%5Cu30B9%5Cu306Edata%5Cu304C%5Cu914D%5Cu5217%5Cu3067%5Cu306F%5Cu3042%5Cu308A%5Cu307E%5Cu305B%5Cu3093%22)%3Breturn%20s%7Dcatch(o)%7Bt%3Do%2Cconsole.warn(%60%5Bsaku-scores-dl%5D%20page%3D%24%7Br%7D%20attempt%3D%24%7Bn%7D%20%5Cu5931%5Cu6557%60%2Co)%2Cn%3C3%26%26await%20b(n*3e3)%7D%7Dthrow%20t%7Dfunction%20N(e)%7Bconst%20r%3Dnew%20Blob(%5BJSON.stringify(e%2Cnull%2C2)%5D%2C%7Btype%3A%22application%2Fjson%3Bcharset%3Dutf-8%22%7D)%2Ct%3DURL.createObjectURL(r)%2Cn%3Ddocument.createElement(%22a%22)%2Co%3Dnew%20Date().toISOString().replaceAll(%22%3A%22%2C%22-%22).replace(%2F%5C.%5Cd%7B3%7DZ%24%2F%2C%22Z%22)%3Bn.download%3D%60saku-scores-dl_%24%7Bo%7D.json%60%2Cn.href%3Dt%2Cn.click()%2CsetTimeout(()%3D%3EURL.revokeObjectURL(t)%2C1e3)%7Dtry%7Bif(!%2Fchrono-circle%5C.com%24%2Fi.test(location.hostname)%26%26!confirm(%22chrono-circle.com%20%5Cu4E0A%5Cu3067%5Cu306F%5Cu3042%5Cu308A%5Cu307E%5Cu305B%5Cu3093%5Cu3002%5Cu516C%5Cu5F0F%5Cu30DE%5Cu30A4%5Cu30DA%5Cu30FC%5Cu30B8%5Cu3067%5Cu5B9F%5Cu884C%5Cu3057%5Cu3066%5Cu304F%5Cu3060%5Cu3055%5Cu3044%5Cu3002%5Cu7D9A%5Cu884C%5Cu3057%5Cu307E%5Cu3059%5Cu304B%5CuFF1F%22))%7Ba.remove()%3Breturn%7Dconst%20e%3Dprompt(%22%5Cu516C%5Cu5F0F%5Cu30DE%5Cu30A4%5Cu30DA%5Cu30FC%5Cu30B8%5Cu306E%5Cu30A2%5Cu30AB%5Cu30A6%5Cu30F3%5Cu30C8No.%22%2Ck())%3Bif(e%3D%3D%3Dnull)%7Ba.remove()%3Breturn%7Dif(!%2F%5E%5Cd%2B%24%2F.test(e.trim()))throw%20new%20Error(%22%5Cu30A2%5Cu30AB%5Cu30A6%5Cu30F3%5Cu30C8No.%5Cu306F%5Cu6570%5Cu5B57%5Cu3067%5Cu5165%5Cu529B%5Cu3057%5Cu3066%5Cu304F%5Cu3060%5Cu3055%5Cu3044%22)%3Bconst%20r%3Dprompt(%60%5Cu3053%5Cu306E%5Cu65E5%5Cu6642%5Cu3088%5Cu308A%5Cu5F8C%5Cu306E%5Cu30D7%5Cu30EC%5Cu30A4%5Cu3060%5Cu3051%5Cu53D6%5Cu5F97%5Cu3057%5Cu307E%5Cu3059%5Cu3002%5Cu7A7A%5Cu6B04%5Cu306A%5Cu3089%5Cu5168%5Cu4EF6%5Cu3002%0A%5Cu4F8B%3A%202026-07-01T00%3A00%3A00%2B09%3A00%60%2ClocalStorage.getItem(%22saku-scores-dl-after%22)%3F%3F%22%22)%3Bif(r%3D%3D%3Dnull)%7Ba.remove()%3Breturn%7Dconst%20t%3D%24(r)%3BlocalStorage.setItem(%22saku-scores-dl-after%22%2Cr.trim())%3Bconst%20n%3Dprompt(%22%5Cu30E6%5Cu30FC%5Cu30B6%5Cu30FC%5Cu540D%5CuFF08%5Cu30D5%5Cu30A1%5Cu30A4%5Cu30EB%5Cu5185%5Cu306E%5Cu8868%5Cu793A%5Cu7528%5Cu3002%5Cu7A7A%5Cu6B04%5Cu53EF%5CuFF09%22%2C%22%22)%3F%3F%22%22%2Co%3Dprompt(%22%5Cu30DA%5Cu30FC%5Cu30B8%5Cu9593%5Cu306E%5Cu5F85%5Cu6A5F%5Cu6642%5Cu9593%5CuFF08%5Cu30DF%5Cu30EA%5Cu79D2%5CuFF09%22%2C%225000%22)%3Bif(o%3D%3D%3Dnull)%7Ba.remove()%3Breturn%7Dconst%20s%3DNumber(o)%3Bif(!Number.isInteger(s)%7C%7Cs%3C500)throw%20new%20Error(%22%5Cu5F85%5Cu6A5F%5Cu6642%5Cu9593%5Cu306F500%5Cu4EE5%5Cu4E0A%5Cu306E%5Cu6574%5Cu6570%5Cu306B%5Cu3057%5Cu3066%5Cu304F%5Cu3060%5Cu3055%5Cu3044%22)%3Bconst%20g%3DMath.floor(Date.now()%2F1e3)%2Cc%3D%5B%5D%3Blet%20i%3D1%2Cp%3D!1%3Bfor(console.log(%22%5Bsaku-scores-dl%5D%20%5Cu53D6%5Cu5F97%5Cu958B%5Cu59CB%5Cu3002%5Cu4E2D%5Cu6B62%3A%20window.__sakuScoresDl.cancel()%22)%3B!p%3B)%7Bf(%60%24%7Bi%7D%5Cu30DA%5Cu30FC%5Cu30B8%5Cu76EE%5Cu3092%5Cu53D6%5Cu5F97%5Cu4E2D%5Cu2026%5CuFF08%24%7Bc.length%7D%5Cu4EF6%5CuFF09%60)%3Bconst%20m%3Dawait%20I(e.trim()%2Ci)%3Bfor(const%20A%20of%20m.data)%7Bconst%20_%3DP(A%2Cg)%3Bif(_.played_at%3C%3Dt)%7Bp%3D!0%3Bbreak%7Dc.push(_)%7Dif(f(%60%24%7Bc.length%7D%5Cu4EF6%5Cu53D6%5Cu5F97%60)%2Cconsole.log(%60%5Bsaku-scores-dl%5D%20page%3D%24%7Bi%7D%2C%20scores%3D%24%7Bc.length%7D%60)%2Cp%7C%7Cm.data_more%3D%3D%3D-1%7C%7Cm.data.length%3D%3D%3D0)break%3Bif(i%2B%3D1%2Cawait%20b(s)%2Cd.cancelled)throw%20new%20Error(%22%5Cu30AD%5Cu30E3%5Cu30F3%5Cu30BB%5Cu30EB%5Cu3055%5Cu308C%5Cu307E%5Cu3057%5Cu305F%22)%7Dconst%20x%3D%7Bformat%3A%22saku-scores-dl%22%2Cversion%3A1%2Cexported_at%3Anew%20Date().toISOString()%2Cuser%3A%7Bname%3An%7D%2Cscores%3Ac%7D%3BN(x)%2Ch(%60%5Cu5B8C%5Cu4E86%3A%20%24%7Bc.length%7D%5Cu4EF6%60)%2Cconsole.log(%60%5Bsaku-scores-dl%5D%20%5Cu5B8C%5Cu4E86%3A%20%24%7Bc.length%7D%5Cu4EF6%5Cu3092%5Cu30C0%5Cu30A6%5Cu30F3%5Cu30ED%5Cu30FC%5Cu30C9%5Cu3057%5Cu307E%5Cu3057%5Cu305F%60%2Cx)%7Dcatch(e)%7Bh(d.cancelled%3F%22%5Cu4E2D%5Cu6B62%5Cu3057%5Cu307E%5Cu3057%5Cu305F%22%3A%22%5Cu30A8%5Cu30E9%5Cu30FC%5Cu304C%5Cu767A%5Cu751F%5Cu3057%5Cu307E%5Cu3057%5Cu305F%22)%2Cconsole.error(%22%5Bsaku-scores-dl%5D%20%5Cu30A8%5Cu30AF%5Cu30B9%5Cu30DD%5Cu30FC%5Cu30C8%5Cu5931%5Cu6557%22%2Ce)%2Calert(%60%5Cu30B9%5Cu30B3%5Cu30A2%5Cu306E%5Cu30A8%5Cu30AF%5Cu30B9%5Cu30DD%5Cu30FC%5Cu30C8%5Cu306B%5Cu5931%5Cu6557%5Cu3057%5Cu307E%5Cu3057%5Cu305F%5Cu3002%0A%24%7Be%20instanceof%20Error%3Fe.message%3AString(e)%7D%60)%7Dfinally%7Bd.running%3D!1%7D%7D)()%3B%0A
/* cc official play list -> JSON */
(async () => {
'use strict';
if (window.__sakuScoresDl?.running) {
console.warn('[saku-scores-dl] すでに実行中です');
return;
}
const state = { running: true, cancelled: false, cancel() { this.cancelled = true; } };
window.__sakuScoresDl = state;
document.getElementById('saku-scores-dl-progress')?.remove();
const progressOverlay = document.createElement('div');
progressOverlay.id = 'saku-scores-dl-progress';
progressOverlay.style.cssText = [
'position:fixed',
'left:12px',
'right:12px',
'bottom:12px',
'z-index:2147483647',
'display:flex',
'align-items:center',
'gap:12px',
'padding:12px',
'border-radius:8px',
'background:rgba(0,0,0,.85)',
'color:#fff',
'font:14px/1.4 sans-serif',
'box-shadow:0 2px 12px rgba(0,0,0,.35)',
].join(';');
const progressText = document.createElement('span');
progressText.style.cssText = 'flex:1;min-width:0';
const progressButton = document.createElement('button');
progressButton.type = 'button';
progressButton.textContent = '中止';
progressButton.style.cssText = [
'flex:none',
'padding:8px 12px',
'border:0',
'border-radius:6px',
'background:#fff',
'color:#000',
'font:inherit',
].join(';');
progressButton.onclick = () => {
state.cancel();
progressText.textContent = '中止しています…';
progressButton.disabled = true;
};
progressOverlay.append(progressText, progressButton);
document.body.append(progressOverlay);
function showProgress(message) {
progressText.textContent = message;
}
function finishProgress(message) {
progressText.textContent = message;
progressButton.disabled = false;
progressButton.textContent = '閉じる';
progressButton.onclick = () => progressOverlay.remove();
}
showProgress('準備中…');
const DIFFICULTIES = { ez: 'EASY', nm: 'NORMAL', hd: 'HARD', ex: 'EXPERT', mas: 'MASTER' };
const JACKET_KEY_OVERRIDES = {
'FLUFFY♡POPPING☆SUMMER!!!': 'FLUFFY♡POPPING☆SUMMER\u200b!\u200b!\u200b!',
'edge//lord': 'edge_lord',
};
const ZERO_WIDTH = /[\u200b\u200c\u200d\ufeff]/g;
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const cleanTitle = (value) => String(value ?? '').replace(ZERO_WIDTH, '');
function detectAccountNo() {
const candidates = [
new URL(location.href).searchParams.get('no'),
document.querySelector('[name="no"]')?.value,
document.querySelector('[data-account-no]')?.dataset.accountNo,
];
for (const candidate of candidates) {
if (/^\d+$/.test(String(candidate ?? ''))) return String(candidate);
}
const match = document.documentElement.innerHTML.match(/(?:account[_-]?no|member[_-]?no|\bno)["'\s:=]+(\d{1,12})/i);
return match?.[1] ?? '';
}
function parseAfter(value) {
if (!value.trim()) return 0;
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(value.trim())
? `${value.trim()}T00:00:00+09:00`
: value.trim();
const milliseconds = Date.parse(normalized);
if (!Number.isFinite(milliseconds)) throw new Error(`日時を解釈できません: ${value}`);
return Math.floor(milliseconds / 1000);
}
function imageKey(html, directory) {
const match = String(html ?? '').match(new RegExp(`/${directory}/([^/?"']+)\\.png`));
return match?.[1] ?? null;
}
function convertPlay(play, createdAt) {
const title = cleanTitle(play.title);
const date = String(play.play_date).replaceAll('.', '-');
const playedAt = Math.floor(Date.parse(`${date}T${play.play_time}Z`) / 1000);
const difficultyCode = String(play.level_img).match(/level_(ez|nm|hd|ex|mas)_/)?.[1];
const grade = String(play.play_grade).match(/grade_(perfectCircle|fullCircle|clear|failed)\.png/)?.[1];
const scoreMilli = Number(String(play.score).replace('.', ''));
const jacketKey = JACKET_KEY_OVERRIDES[title] ?? title;
if (!jacketKey) throw new Error('曲名が空です');
if (!DIFFICULTIES[difficultyCode]) throw new Error(`${title}: 難易度を解釈できません`);
if (!Number.isInteger(scoreMilli) || scoreMilli < 0 || scoreMilli > 101000) {
throw new Error(`${title}: スコアを解釈できません (${play.score})`);
}
if (!Number.isInteger(playedAt) || playedAt <= 0) {
throw new Error(`${title}: プレイ日時を解釈できません`);
}
return {
jacket_key: jacketKey,
title,
difficulty: DIFFICULTIES[difficultyCode],
score_milli: scoreMilli,
judgments: null,
grade: grade ?? null,
played_at: playedAt,
created_at: createdAt,
source: 'import',
analyzer_raw: {
provider: 'chrono-circle-official',
play_no: Number(play.play_no),
song_img: imageKey(play.song_img, 'song_img'),
},
corrected_fields: [],
};
}
async function requestPage(accountNo, page) {
let lastError;
for (let attempt = 1; attempt <= 3; attempt += 1) {
if (state.cancelled) throw new Error('キャンセルされました');
try {
const response = await fetch('/logic/ajax.my.play_list.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ no: accountNo, page: String(page) }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.state === 500) throw new Error(data.msg || 'API state=500');
if (!Array.isArray(data.data)) throw new Error('APIレスポンスのdataが配列ではありません');
return data;
} catch (error) {
lastError = error;
console.warn(`[saku-scores-dl] page=${page} attempt=${attempt} 失敗`, error);
if (attempt < 3) await wait(attempt * 3000);
}
}
throw lastError;
}
function downloadJson(value) {
const blob = new Blob([JSON.stringify(value, null, 2)], { type: 'application/json;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
const slug = new Date().toISOString().replaceAll(':', '-').replace(/\.\d{3}Z$/, 'Z');
anchor.download = `saku-scores-dl_${slug}.json`;
anchor.href = url;
anchor.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
try {
if (!/chrono-circle\.com$/i.test(location.hostname)) {
const proceed = confirm('chrono-circle.com 上ではありません。公式マイページで実行してください。続行しますか?');
if (!proceed) {
progressOverlay.remove();
return;
}
}
const accountNo = prompt('公式マイページのアカウントNo.', detectAccountNo());
if (accountNo === null) {
progressOverlay.remove();
return;
}
if (!/^\d+$/.test(accountNo.trim())) throw new Error('アカウントNo.は数字で入力してください');
const afterText = prompt(
'この日時より後のプレイだけ取得します。空欄なら全件。\n例: 2026-07-01T00:00:00+09:00',
localStorage.getItem('saku-scores-dl-after') ?? '',
);
if (afterText === null) {
progressOverlay.remove();
return;
}
const after = parseAfter(afterText);
localStorage.setItem('saku-scores-dl-after', afterText.trim());
const userName = prompt('ユーザー名(ファイル内の表示用。空欄可)', '') ?? '';
const delayText = prompt('ページ間の待機時間(ミリ秒)', '5000');
if (delayText === null) {
progressOverlay.remove();
return;
}
const pageDelay = Number(delayText);
if (!Number.isInteger(pageDelay) || pageDelay < 500) throw new Error('待機時間は500以上の整数にしてください');
const createdAt = Math.floor(Date.now() / 1000);
const scores = [];
let page = 1;
let reachedAfter = false;
console.log('[saku-scores-dl] 取得開始。中止: window.__sakuScoresDl.cancel()');
while (!reachedAfter) {
showProgress(`${page}ページ目を取得中…(${scores.length}件)`);
const response = await requestPage(accountNo.trim(), page);
for (const play of response.data) {
const converted = convertPlay(play, createdAt);
if (converted.played_at <= after) {
reachedAfter = true;
break;
}
scores.push(converted);
}
showProgress(`${scores.length}件取得`);
console.log(`[saku-scores-dl] page=${page}, scores=${scores.length}`);
if (reachedAfter || response.data_more === -1 || response.data.length === 0) break;
page += 1;
await wait(pageDelay);
if (state.cancelled) throw new Error('キャンセルされました');
}
const file = {
format: 'saku-scores-dl',
version: 1,
exported_at: new Date().toISOString(),
user: { name: userName },
scores,
};
downloadJson(file);
finishProgress(`完了: ${scores.length}件`);
console.log(`[saku-scores-dl] 完了: ${scores.length}件をダウンロードしました`, file);
} catch (error) {
finishProgress(state.cancelled ? '中止しました' : 'エラーが発生しました');
console.error('[saku-scores-dl] エクスポート失敗', error);
alert(`スコアのエクスポートに失敗しました。\n${error instanceof Error ? error.message : String(error)}`);
} finally {
state.running = false;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment