Created
July 25, 2026 21:58
-
-
Save heguro/198778412256731d0f4cc7b6c84ea7c8 to your computer and use it in GitHub Desktop.
saku-scores-dl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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