// ==UserScript== // @name 爬取全民K歌指定用户的全部歌曲 // @namespace https://kg.qq.com/ // @version 1.3.0 // @description 一键下载全民K歌用户的所有歌曲(MP4/M4A) // @author aspen138 // @match https://kg.qq.com/* // @match https://static-play.kg.qq.com/* // @match https://node.kg.qq.com/* // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect kg.qq.com // @connect node.kg.qq.com // @connect static-play.kg.qq.com // @connect * // @run-at document-idle // @license MIT // @icon data:image/x-icon;base64,AAABAAEAGBgAAAEAIACICQAAFgAAACgAAAAYAAAAMAAAAAEAIAAAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAgH/AAIB/wAAAv8AQw7+AIs1+QCKNPwAnjr8Apc5+z2ZOvyKnDn7v5s6+9+XOfvvkzf72o42+7KKNvx6gjL9KnYu/ABfJv0AUSDBAAAAAAB2CUELiQdfC////wD///8AAQD/AAEA/wAAAP8AQw3/AIgz+QCQNvsxozv7xqE8+v+cOfr/ljf7/480+/+LNPv/iTP7/4g0+/+HNfr/hjX7/n8z+59nKfwZWCDNACwCExWHC0vniwxTu////wD///8AAAD/AAAA/wAAAP8ANQD/AJ88+oKhOvr5mjn6/5M0+/+MM/v/iDH7/4Iw+/9+Lvz/ey78/3kt+/93Lfz/dy77/3sw+/97MPvxeSjkWaMFRbuQDU//lwhKsv///wD///8AAAu/AAALvwAAB8UAdSr4iaU7+/+XN/v/jTP7/4cw+/+BLvz/fCz8/3cr/P9yKfz/cCf8/24o/P9rJ/3/ayj8/2sp/f9vK/3/di72/4kYgP6QBzr9fgRWOv///wD///8AZCfhAGEm4QB3LuNfpz33+480+/+KMfv/gjD8/30u/P92Kfz/cSn8/24m/P9oJvz/ZiT8/2Ik/P9hI/v/XSP//1Un//9WKf//YCn//2ou//+IEWOzVQpGAP///wD///8AXhv/AGMe/winOf/ykTb8/4cx+/+ALvz/eSv8/3In/P9sJvz/aCT8/2Qi/v9eIv7/XCD+/1kg/v9XHf//Uh///3YWm/93Fpn/XCH2/1co//9mJ/2/ayT2AP///wD///8AaCb7AH8w/IWVN/v/hjD7/30s/P92Kfz/byf8/2kk/v9kIv7/XiD+/1ke/v9WHv7/VBz+/1Ec/v9MG///eROF/5UFKP+UBCz/hQ5g/2Ed4P9bJf/9UyX/S////wD///8AhTX6AJk5+/eFMfv/fSz8/3Qr/P9uKPz/ZiT+/2Ag/f9aHv7/Vh7+/1Qc/v9PGf7/TBn//0oZ/v9GGf//lAcx/4sNUP+NC07/kwc2/3AWp/9UHv7/VB7+ov///wD///8AhjH8QIgz+/9+Lvv/dSv8/2wo/P9mJP7/XiL8/1ge/v9UHP7/TRT+/zoA/P8sAPv/NgD9/0UW//8+Fv//exJ+/5kGKP+XBjD/kgg9/1sU1/9OGf7/UBz+2P///wD///8AiDT7cYAw+/92K/z/bij8/2Uk/P9eIP7/WB7+/1Qe/v87APz/LQD7/1g2/v9tVf//YUb//0IR//9AFv//Pxb//28Ul/93E4T/XRbE/z8Y//9IF/7/Thz++v///wD///8AgjH7jnou/P9wKfz/ZyT8/14i/v9YHv7/Uhz+/zoA/P9XNv//wrn///j4////////tqz//z0O/vpAFP6IPRP/5ikl//8vJ///LCr//zwl//9AIv7/RiD+/////wD///8AfTD8jnQr/P9qJ/3/YSL+/1kg/v9SHP7/SxT+/1Uz/v/u6///////////////////koP//0AU/sVCFP4APhf/AEEo/6o3Nv//Nzf9/zk1/f88K/3/QSb9/////wD///8Adi35cW8r/P9lJvz/XSD9/1Ue/f9OHP7/SyD+/720////////////////////////mo3//0EN/49BHv4ANyL/KkoZ/ys4P/3TNUL9/zY//f85Nf3/PSj9+f///wD///8AbSn/QGwp/f9gJPz/WCD8/1Ae/f9KGf7/UzH+//j3////////////////////////4Nz//xkA++w9JP/aOzD+/zku/qI1MP03Mk78/zZD/f84Of3/Oyv92P///wD///8ASB3iAGco+vdeIvz/VR79/04c/f9IGf7/WD7//////////////////////////////////4d+//8FAPv/Mi/+/zZD/ts2Nf0DNE781jZG/P83O/3/PC3+ov///wD///8AWSLsAGkq/4NfJP7/VB79/0sc/f9GHP7/VTr//////////////////9DK//+Qgf//6uf///v7//+Hiv//HCf7/zBH/Ow4Q/sYMkL9gjJL/P8zRvz9Pjv8Sf///wD///8AThj3AEgU8AhXIvnwVB7+/0sc/f9GHP7/SSv+/+Lg/////////////3Vg//8AAPj/wLj/////////////5eT//0Bh/egvTP0YMFH8Xy9b+/8tQ/3NRCH7AP///wD///8ARwzzAEcM8wBVG/qDVR7+/0we/v9FG///QBr+/5CF///+/v///////+7s//+7tP//9/b/////////////6+///x9i+8wtUP0DMVz+cCFk+/+GDf8YhwD/AP///wD///8AaCf/AGgn/wBhJvyKVSL+/0si/v9EJP7/PyT//ykA/f+opf/////////////////////////////T3f//Kmj8/0BQ/HxQSf0ANm78pD0+/VZoAP8AZwD/AP///wD///8AVy/7AFcv+wBbMvuNVTD8/0k0/P9ANv3/Ozj9/zA0/f8LAfv/aHH//6yy///GzP//u8X//4Of//8VYPv/EWn82ExD/wB1HP8AS07+JEUt/QBQAP8AUAD/AP///wD///8AUj/9AFI//QBZQPxzWEf+/0ZM+/49U/z9NFj8/y9a+/8sW/r/ADn6/wAw+v8EQPv/AEf5/wBS+f8ZbvvIK2D8FR9b/wB4Dv8AUTz/AEIz/QBVAP8AVQD/AP///wD///8AUkX5AFJF+QBXRvo9UUryc0FV/iQ7X/wbM2L8Mi1u+1sqcvySKXX8xClw++MmbvvhJHL8tjJr/mEtZPsHK177ACpX/wB2GP8ATkP/AEIz/QBVAP8AVQD/AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A/gB5APwAEQD4AAEA8AABAOAAAwDAAAMAwAABAMAAAQCAAAEAgAABAIAAAQCAAYEAgAEBAIAAAQDAAAEAwAABAMAAAwDgAAMA4AAnAOAAbwDgAH8A4AD/AP///wA= // ==/UserScript== // @credit Ported from https://www.manshaoco.com/1354.html Python code: Auto-fetches UID, grabs all songs, and downloads them. (function () { 'use strict'; // ─── Helpers ──────────────────────────────────────────────────────────────── function getCookies() { return document.cookie; } function parseCookies(cookieStr) { const dict = {}; cookieStr.split(';').forEach(pair => { const [k, ...rest] = pair.trim().split('='); if (k) dict[k.trim()] = rest.join('=').trim(); }); return dict; } function cleanFilename(name) { return name.replace(/[\\/:*?"<>|\r\n"]/g, '').trim(); } function uidFromURL() { try { const url = new URL(window.location.href); return url.searchParams.get('uid') || ''; } catch { return ''; } } function gmFetch(url, opts = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: opts.method || 'GET', url, headers: opts.headers || {}, responseType: opts.responseType || 'text', onload: r => resolve(r), onerror: e => reject(e), }); }); } function extractJSON(text) { // Generic fallback: find outermost {...} and parse const s = text.indexOf('{'); const e = text.lastIndexOf('}'); if (s === -1 || e === -1) return null; try { return JSON.parse(text.slice(s, e + 1)); } catch { return null; } } /** * Extract a window.VARNAME = {...}; assignment from raw HTML. * Uses brace-balancing so large nested JSON is handled correctly. */ function extractVar(html, varName) { const marker = `window.${varName}`; const markerIdx = html.indexOf(marker); if (markerIdx === -1) return null; const eqIdx = html.indexOf('=', markerIdx); if (eqIdx === -1) return null; const start = html.indexOf('{', eqIdx); if (start === -1) return null; let depth = 0; for (let i = start; i < html.length; i++) { if (html[i] === '{') depth++; else if (html[i] === '}') { depth--; if (depth === 0) { try { return JSON.parse(html.slice(start, i + 1)); } catch { return null; } } } } return null; } /** * Try both page formats and return a unified object with a .data field. * Old kg.qq.com/node/personal → window.__DATA__ → .data * New static-play React app → window.__FETCH_RES__ → .userInfoRes.data * * For the play detail page: * Old → window.__DATA__ → .detail * New → window.__FETCH_RES__ → .detailRes.data (or similar) */ function extractPageData(html) { // ── New React page (__FETCH_RES__) ── const fetchRes = extractVar(html, '__FETCH_RES__'); if (fetchRes) { const data = fetchRes.userInfoRes && fetchRes.userInfoRes.data; const detail = fetchRes.ugcDetailRes && fetchRes.ugcDetailRes.data; if (data || detail) return { source: 'FETCH_RES', data, detail }; } // ── Old SSR page (__DATA__) ── const dataObj = extractVar(html, '__DATA__'); if (dataObj) return { source: '__DATA__', data: dataObj.data, detail: dataObj.detail }; return null; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // ─── UI ───────────────────────────────────────────────────────────────────── GM_addStyle(` #kgdl-panel *{box-sizing:border-box;font-family:'PingFang SC','Microsoft YaHei',sans-serif} #kgdl-panel{ position:fixed;bottom:24px;right:24px;z-index:2147483647; width:340px;background:#0d0d0d;border:1px solid #222; border-radius:16px;box-shadow:0 8px 40px #000a; overflow:hidden;transition:height .3s ease; } #kgdl-header{ display:flex;align-items:center;justify-content:space-between; padding:14px 18px;background:#161616;cursor:pointer; border-bottom:1px solid #222;user-select:none; } #kgdl-header .kgdl-title{ font-size:14px;font-weight:700;letter-spacing:.04em; color:#fff;display:flex;align-items:center;gap:8px; } #kgdl-header .kgdl-title span.dot{ width:8px;height:8px;border-radius:50%;background:#ff4d4d; display:inline-block;animation:kgdl-pulse 1.4s infinite; } @keyframes kgdl-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.85)}} #kgdl-toggle{color:#555;font-size:18px;line-height:1;transition:transform .3s} #kgdl-body{padding:16px 18px;display:flex;flex-direction:column;gap:10px} .kgdl-label{font-size:11px;color:#555;letter-spacing:.06em;text-transform:uppercase;margin-bottom:2px} .kgdl-input{ width:100%;background:#1c1c1c;border:1px solid #2a2a2a;border-radius:8px; color:#e0e0e0;font-size:13px;padding:8px 12px;outline:none; transition:border .2s; } .kgdl-input:focus{border-color:#ff4d4d} .kgdl-input::placeholder{color:#3a3a3a} .kgdl-row{display:flex;gap:8px} .kgdl-btn{ flex:1;padding:9px 0;border:none;border-radius:9px; font-size:13px;font-weight:600;cursor:pointer; transition:opacity .2s,transform .1s; } .kgdl-btn:active{transform:scale(.97)} .kgdl-btn:disabled{opacity:.35;cursor:not-allowed} .kgdl-btn.primary{background:#ff4d4d;color:#fff} .kgdl-btn.secondary{background:#222;color:#aaa;border:1px solid #2a2a2a} #kgdl-log{ background:#111;border:1px solid #1e1e1e;border-radius:10px; font-size:11.5px;color:#5a5a5a;padding:10px 12px; height:120px;overflow-y:auto;line-height:1.7; font-family:'Menlo','Consolas',monospace; } #kgdl-log .ok{color:#3d9e6a} #kgdl-log .err{color:#c44} #kgdl-log .info{color:#5a8fc4} #kgdl-log .hi{color:#e0e0e0} #kgdl-progress-wrap{display:none;flex-direction:column;gap:4px} #kgdl-progress-label{font-size:11px;color:#666} #kgdl-progress-bar-bg{background:#1e1e1e;border-radius:99px;height:5px;overflow:hidden} #kgdl-progress-bar{height:5px;border-radius:99px;background:#ff4d4d;width:0%;transition:width .3s ease} `); const panel = document.createElement('div'); panel.id = 'kgdl-panel'; panel.innerHTML = `
全民K歌 下载器
用户 UID
准备中…
等待操作…
`; document.body.appendChild(panel); // collapse / expand let collapsed = false; const body = panel.querySelector('#kgdl-body'); const toggleBtn = panel.querySelector('#kgdl-toggle'); panel.querySelector('#kgdl-header').addEventListener('click', () => { collapsed = !collapsed; body.style.display = collapsed ? 'none' : ''; toggleBtn.textContent = collapsed ? '▸' : '▾'; }); // pre-fill uid from URL const autoUID = uidFromURL(); const uidInput = panel.querySelector('#kgdl-uid'); if (autoUID) uidInput.value = autoUID; // log helper const logEl = panel.querySelector('#kgdl-log'); function log(msg, cls = '') { const line = document.createElement('div'); if (cls) line.className = cls; line.textContent = msg; logEl.appendChild(line); logEl.scrollTop = logEl.scrollHeight; } function clearLog() { logEl.innerHTML = ''; } // progress const progressWrap = panel.querySelector('#kgdl-progress-wrap'); const progressBar = panel.querySelector('#kgdl-progress-bar'); const progressLbl = panel.querySelector('#kgdl-progress-label'); function setProgress(cur, total) { progressWrap.style.display = 'flex'; const pct = total ? Math.round((cur / total) * 100) : 0; progressBar.style.width = pct + '%'; progressLbl.textContent = `下载进度:${cur} / ${total}(${pct}%)`; } // ─── Core logic ───────────────────────────────────────────────────────────── let aborted = false; const startBtn = panel.querySelector('#kgdl-start'); const stopBtn = panel.querySelector('#kgdl-stop'); stopBtn.addEventListener('click', () => { aborted = true; log('⛔ 用户已停止下载', 'err'); startBtn.disabled = false; }); startBtn.addEventListener('click', async () => { aborted = false; clearLog(); startBtn.disabled = true; progressWrap.style.display = 'none'; progressBar.style.width = '0%'; const cookie = getCookies(); const cookieDict = parseCookies(cookie); const muid = cookieDict['muid'] || ''; let uid = uidInput.value.trim() || autoUID || muid; if (!uid) { log('❌ 无法获取 UID,请手动输入', 'err'); startBtn.disabled = false; return; } log(`📌 使用 UID:${uid}`, 'info'); const headers = { 'Cookie': cookie, 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1', 'Referer': 'https://static-play.kg.qq.com/', }; // ── Step 1: get user info + total count ────────────────────────────────── log('🔍 正在获取用户信息…', 'info'); let userInfo = null; let totalCount = 0; let nickname = uid; try { // Try the new React page first (static-play), then fall back to old SSR page const urls = [ `https://static-play.kg.qq.com/node/personal?uid=${uid}`, `https://kg.qq.com/node/personal?uid=${uid}`, ]; let parsed = null; for (const url of urls) { const res = await gmFetch(url, { headers }); parsed = extractPageData(res.responseText); if (parsed && parsed.data) break; } if (parsed && parsed.data) { userInfo = parsed.data; nickname = userInfo.kgnick || userInfo.nickname || uid; totalCount = userInfo.ugc_total_count || 0; log(`👤 昵称:${nickname} 歌曲总数:${totalCount}(来源:${parsed.source})`, 'ok'); } else { log('⚠️ 未能解析用户数据,请检查登录状态', 'err'); } } catch (e) { log(`❌ 获取用户信息失败:${e}`, 'err'); startBtn.disabled = false; return; } if (!totalCount) { log('⚠️ 该用户没有公开歌曲或获取失败', 'err'); startBtn.disabled = false; return; } // ── Step 2: collect all song metadata ─────────────────────────────────── log('📋 正在获取歌曲列表…', 'info'); const songList = []; const NUM = 15; let page = 1; while (!aborted) { const params = new URLSearchParams({ outCharset: 'utf-8', from: '1', nocache: Date.now().toString(), format: 'json', type: 'get_uinfo', start: page, num: NUM, share_uid: uid, g_tk: '5381', g_tk_openkey: '1970486037', }); try { const res = await gmFetch(`https://node.kg.qq.com/fcgi-bin/kg_ugc_get_homepage?${params}`, { headers }); const obj = extractJSON(res.responseText); if (!obj || !obj.data || !obj.data.ugclist || obj.data.ugclist.length === 0) break; const cleaned = obj.data.ugclist.map(s => ({ ...s, title: cleanFilename(s.title) })); songList.push(...cleaned); log(` 获取第 ${page} 页,已收集 ${songList.length} 首`, ''); page++; await sleep(300); } catch (e) { log(`❌ 第 ${page} 页获取失败:${e}`, 'err'); break; } } log(`✅ 歌曲列表获取完成,共 ${songList.length} 首`, 'ok'); // ── Inject utc8time alongside every time field, then save JSON ─────────── function toUTC8String(unixSec) { // UTC+8 offset in ms const d = new Date((unixSec + 8 * 3600) * 1000); const pad = n => String(n).padStart(2, '0'); return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ` + `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`; } // Build annotated list: insert utc8time right after each time key const annotatedList = songList.map(song => { const out = {}; for (const [k, v] of Object.entries(song)) { out[k] = v; if (k === 'time' && typeof v === 'number') { out['utc8time'] = toUTC8String(v); } } return out; }); // Download JSON file via blob URL (no GM_download needed for text) const jsonStr = JSON.stringify(annotatedList, null, 4); const blob = new Blob([jsonStr], { type: 'application/json' }); const blobUrl = URL.createObjectURL(blob); const jsonFilename = `${nickname}_${uid}.json`; GM_download({ url: blobUrl, name: jsonFilename, onload: () => { log(`📄 JSON 已保存:${jsonFilename}`, 'ok'); URL.revokeObjectURL(blobUrl); }, onerror: e => log(`❌ JSON 保存失败:${JSON.stringify(e)}`, 'err'), }); // ── Step 3: download each song ─────────────────────────────────────────── for (let i = 0; i < songList.length && !aborted; i++) { const song = songList[i]; setProgress(i, songList.length); log(`⬇️ [${i + 1}/${songList.length}] ${song.title}`, 'hi'); try { const pageRes = await gmFetch(`https://node.kg.qq.com/play?s=${song.shareid}`, { headers }); let playUrl = ''; let ext = ''; const parsed = extractPageData(pageRes.responseText); const detail = parsed && parsed.detail; if (detail) { if (detail.playurl_video) { playUrl = detail.playurl_video; ext = '.mp4'; } else if (detail.playurl) { playUrl = detail.playurl; ext = '.m4a'; } } if (!playUrl) { log(` ⚠️ 未找到播放链接,跳过`, 'err'); continue; } const filename = `${nickname}_${cleanFilename(song.title)}_${song.shareid}${ext}`; GM_download({ url: playUrl, name: filename, onload: () => log(` ✅ 已保存:${filename}`, 'ok'), onerror: e => log(` ❌ 下载失败:${JSON.stringify(e)}`, 'err'), }); await sleep(600); // polite delay } catch (e) { log(` ❌ 处理失败:${e}`, 'err'); } } setProgress(songList.length, songList.length); if (!aborted) log('🎉 全部下载任务已触发,请查看浏览器下载列表!', 'ok'); startBtn.disabled = false; }); })();