- 開啟該頁面 → 按 F12 或右鍵「檢查」 → 切到 Console。
- 貼上腳本 → Enter。
- 在提示框輸入範圍(例如 201-400),若知道列高也可輸入(或留空用自動偵測)。
- Console 會印出起點附近的預覽表格(index / 標題 / 作者 / 是否已勾選)。
- 確認後按「確定」,就會開始勾選。
Created
March 3, 2026 08:23
-
-
Save JuenTingShie/24e4e3939f40b1d7c458418137078dc7 to your computer and use it in GitHub Desktop.
Soundiiz.com batch export
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
| (async () => { | |
| // ========= 可調參數(如你的頁面類別不同請改這裡) ========= | |
| const MODAL_SELECTOR = '.modal-inner.modal-entrance'; // 可滾動的 Modal 內容容器 | |
| const GRID_IN_MODAL = '.ReactVirtualized__Grid'; // Modal 裡的虛擬清單 Grid(如果有) | |
| const INNER_SELECTOR = '.ReactVirtualized__Grid__innerScrollContainer'; | |
| const ROW_SELECTOR = '.list-selector.track-selector'; // 單列容器 | |
| const TITLE_SELECTOR = '.selector-content .item-title'; // 標題(顯示用) | |
| const CHECKBOX_SELECTOR = 'input[type="checkbox"]'; // 勾選的 input | |
| const CLICKABLE_FALLBACK = '.checkbox-inner, .selector-check, .checkbox'; // 非原生 input 的候選 | |
| // ========= 互動提示:輸入區間 ========= | |
| const rangeInput = prompt('請輸入要勾選的區間(格式:起-迄,例如 201-400)', '201-400'); | |
| if (!rangeInput) { console.warn('已取消。'); return; } | |
| const rangeMatch = rangeInput.trim().match(/^(\d+)\s*[-~–—]\s*(\d+)$/); | |
| if (!rangeMatch) { console.error('格式錯誤,請使用 201-400 這種格式'); return; } | |
| const START_INDEX = parseInt(rangeMatch[1], 10); | |
| const END_INDEX = parseInt(rangeMatch[2], 10); | |
| if (!(START_INDEX > 0 && END_INDEX >= START_INDEX)) { | |
| console.error('區間不合法:請確認為正整數且起點 ≤ 終點'); return; | |
| } | |
| // ========= 互動提示:行高 ========= | |
| let rowHeightInput = prompt('行高(像素)。輸入數字或留空使用自動偵測', ''); | |
| rowHeightInput = rowHeightInput && rowHeightInput.trim(); | |
| let USER_ROW_HEIGHT = rowHeightInput ? parseInt(rowHeightInput, 10) : null; | |
| if (rowHeightInput && (!Number.isFinite(USER_ROW_HEIGHT) || USER_ROW_HEIGHT <= 0)) { | |
| alert('行高輸入無效,改用自動偵測'); | |
| USER_ROW_HEIGHT = null; | |
| } | |
| // ========= 工具函式 ========= | |
| const wait = (ms) => new Promise(r => setTimeout(r, ms)); | |
| // 鎖住背景滾動(避免 body/html 跟著捲動) | |
| const lockBackgroundScroll = () => { | |
| const original = { | |
| bodyOverflow: document.body.style.overflow, | |
| htmlOverflow: document.documentElement.style.overflow, | |
| bodyPosition: document.body.style.position, | |
| bodyWidth: document.body.style.width, | |
| }; | |
| document.body.style.overflow = 'hidden'; | |
| document.documentElement.style.overflow = 'hidden'; | |
| // 有些頁面需要加 fixed 來避免滾輪穿透 | |
| document.body.style.position = 'fixed'; | |
| document.body.style.width = '100%'; | |
| const unlock = () => { | |
| document.body.style.overflow = original.bodyOverflow || ''; | |
| document.documentElement.style.overflow = original.htmlOverflow || ''; | |
| document.body.style.position = original.bodyPosition || ''; | |
| document.body.style.width = original.bodyWidth || ''; | |
| }; | |
| return unlock; | |
| }; | |
| // 尋找可滾動容器(modal 內的 grid,或 modal 自身) | |
| const modal = document.querySelector(MODAL_SELECTOR); | |
| if (!modal) { console.error('找不到 Modal:', MODAL_SELECTOR); return; } | |
| const grid = modal.querySelector(GRID_IN_MODAL) || modal; | |
| const inner = grid.querySelector(INNER_SELECTOR) || grid; | |
| // 焦點放到 grid(避免鍵盤/滾輪作用到 window) | |
| if (!grid.hasAttribute('tabindex')) grid.setAttribute('tabindex', '0'); | |
| grid.focus({ preventScroll: true }); | |
| // 事件攔截:避免滾動事件傳到背景頁面 | |
| const swallow = (evt) => { | |
| if (!grid.contains(evt.target)) { | |
| evt.stopPropagation(); | |
| evt.preventDefault(); | |
| } | |
| }; | |
| // 幾何/索引相關 | |
| const rowTop = (row) => { | |
| if (row.style && row.style.top) { | |
| const m = row.style.top.match(/(-?\d+(?:\.\d+)?)px/); | |
| if (m) return parseFloat(m[1]); | |
| } | |
| const rectTop = row.getBoundingClientRect().top; | |
| const gridTop = grid.getBoundingClientRect().top; | |
| return Math.round(grid.scrollTop + (rectTop - gridTop)); | |
| }; | |
| const computeIndexFromTop = (topPx, rh) => Math.floor(topPx / rh) + 1; | |
| // 偵測行高(若使用者沒指定) | |
| const detectRowHeight = () => { | |
| const rows = Array.from(inner.querySelectorAll(ROW_SELECTOR)); | |
| if (rows.length < 2) return 67; | |
| const tops = rows | |
| .map(r => { | |
| if (r.style && r.style.top) { | |
| const m = r.style.top.match(/(-?\d+(?:\.\d+)?)px/); | |
| if (m) return parseFloat(m[1]); | |
| } | |
| const rectTop = r.getBoundingClientRect().top; | |
| const gridTop = grid.getBoundingClientRect().top; | |
| return Math.round(grid.scrollTop + (rectTop - gridTop)); | |
| }) | |
| .sort((a, b) => a - b); | |
| const deltas = {}; | |
| for (let i = 1; i < tops.length; i++) { | |
| const d = Math.max(1, Math.round(tops[i] - tops[i - 1])); | |
| deltas[d] = (deltas[d] || 0) + 1; | |
| } | |
| const best = Object.entries(deltas).sort((a, b) => b[1] - a[1])[0]; | |
| return best ? parseInt(best[0], 10) : 67; | |
| }; | |
| // 捲動到指定 top(像素) | |
| const RENDER_DELAY_MS = 90; | |
| const FIND_DELAY_MS = 50; | |
| const scrollToTopPx = async (topPx) => { | |
| grid.scrollTop = topPx; | |
| if (typeof grid.scrollTo === 'function') { | |
| try { grid.scrollTo({ top: topPx, left: 0, behavior: 'instant' }); } catch {} | |
| } | |
| await wait(RENDER_DELAY_MS); | |
| }; | |
| // 解析單列顯示用資訊 | |
| const parseRowInfo = (row) => { | |
| const titleEl = row.querySelector(TITLE_SELECTOR); | |
| const title = titleEl ? titleEl.textContent.trim() : ''; | |
| const creators = Array.from(row.querySelectorAll('.selector-infos .creator-name')) | |
| .map(n => n.textContent.trim()) | |
| .filter(Boolean); | |
| const checked = !!row.querySelector(`${CHECKBOX_SELECTOR}:checked`); | |
| return { title, creators, checked }; | |
| }; | |
| // 收集可見列並依推估 index 存到 map | |
| const collectVisibleRows = (rowHeight, map) => { | |
| const rows = Array.from(inner.querySelectorAll(ROW_SELECTOR)); | |
| for (const row of rows) { | |
| const top = rowTop(row); | |
| if (!Number.isFinite(top)) continue; | |
| const idx = computeIndexFromTop(top, rowHeight); | |
| if (!map.has(idx)) map.set(idx, { index: idx, top, ...parseRowInfo(row), _el: row }); | |
| } | |
| return rows.length; | |
| }; | |
| // 勾選指定 index 對應列(可見區最接近者) | |
| const clickRowCheckboxByIndex = (targetIndex, rowHeight) => { | |
| const approxTop = (targetIndex - 1) * rowHeight; | |
| const rows = Array.from(inner.querySelectorAll(ROW_SELECTOR)); | |
| if (!rows.length) return false; | |
| let best = null, bestDelta = Infinity; | |
| for (const row of rows) { | |
| const top = rowTop(row); | |
| const d = Math.abs(top - approxTop); | |
| if (d < bestDelta) { bestDelta = d; best = row; } | |
| } | |
| if (!best) return false; | |
| let target = best.querySelector(CHECKBOX_SELECTOR) | |
| || best.querySelector(CLICKABLE_FALLBACK); | |
| if (!target) return false; | |
| if ('checked' in target) { | |
| if (!target.checked) target.click(); | |
| } else { | |
| target.click(); | |
| } | |
| return true; | |
| }; | |
| // ========= 主流程 ========= | |
| const unlock = lockBackgroundScroll(); | |
| window.addEventListener('wheel', swallow, { passive: false, capture: true }); | |
| window.addEventListener('keydown', swallow, { capture: true }); | |
| try { | |
| // 1) 回到頂端,準備偵測行高 | |
| await scrollToTopPx(0); | |
| await wait(FIND_DELAY_MS); | |
| // 2) 行高:使用者輸入優先,否則自動偵測 | |
| let rowHeight = USER_ROW_HEIGHT || detectRowHeight(); | |
| if (!Number.isFinite(rowHeight) || rowHeight <= 0) rowHeight = 67; | |
| console.log('[Info] 行高(像素):', rowHeight); | |
| // 3) 收集與驗證起點 | |
| const rowsByIndex = new Map(); | |
| // 先直接跳到起點附近 | |
| await scrollToTopPx((START_INDEX - 1) * rowHeight); | |
| await wait(FIND_DELAY_MS); | |
| collectVisibleRows(rowHeight, rowsByIndex); | |
| // 如果還看不到起點,就往回/往前微調幾次 | |
| let adjustSteps = 0; | |
| while (!rowsByIndex.has(START_INDEX) && adjustSteps < 8) { | |
| // 先微調往上,再往下 | |
| const delta = (adjustSteps % 2 === 0 ? -1 : 1) * Math.ceil((adjustSteps + 1) / 2); | |
| await scrollToTopPx((START_INDEX - 1 + delta) * rowHeight); | |
| await wait(FIND_DELAY_MS); | |
| collectVisibleRows(rowHeight, rowsByIndex); | |
| adjustSteps++; | |
| } | |
| // 預覽區間:起點前後各 6 列 | |
| const previewStart = Math.max(1, START_INDEX - 6); | |
| const previewEnd = START_INDEX + 6; | |
| const sorted = Array.from(rowsByIndex.values()).sort((a, b) => a.index - b.index); | |
| const preview = sorted.filter(r => r.index >= previewStart && r.index <= previewEnd); | |
| if (preview.length) { | |
| console.table(preview.map(r => ({ | |
| index: r.index, | |
| title: r.title, | |
| creators: r.creators?.join(' / ') || '', | |
| checked: r.checked | |
| }))); | |
| } else { | |
| console.warn('在起點附近沒有成功收集到可見列,稍後仍會嘗試勾選。'); | |
| } | |
| // 4) 二次確認是否繼續 | |
| const go = confirm(`已顯示第 ${START_INDEX} 附近的預覽。\n是否繼續自動勾選 ${START_INDEX}–${END_INDEX} ?`); | |
| if (!go) { console.warn('已取消執行。'); return; } | |
| // 5) 執行勾選 | |
| let ok = 0, fail = 0; | |
| for (let i = START_INDEX; i <= END_INDEX; i++) { | |
| await scrollToTopPx((i - 1) * rowHeight); | |
| await wait(FIND_DELAY_MS); | |
| const done = clickRowCheckboxByIndex(i, rowHeight); | |
| if (done) ok++; else fail++; | |
| // 順便豐富索引資料(非必要) | |
| collectVisibleRows(rowHeight, rowsByIndex); | |
| } | |
| alert(`完成。成功勾選:${ok},未能定位/勾選:${fail}`); | |
| } finally { | |
| // 清理:解除事件攔截與背景鎖定 | |
| window.removeEventListener('wheel', swallow, { capture: true }); | |
| window.removeEventListener('keydown', swallow, { capture: true }); | |
| unlock(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment