您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
// ==UserScript==
当前为
// ==UserScript== // @name MyFreeMP3批量操作 // @namespace http://tampermonkey.net/ // @version 1.0 // @description // ==UserScript== // @name MyFreeMP3批量操作 // @namespace http://tampermonkey.net/ // @description 多次点击列标题可升序/降序排序;读取真实歌单列表;批量收藏或取消收藏所选歌曲。 // @match *://tools.liumingye.cn/music/* // @grant none // ==/UserScript== (function() { 'use strict'; let observer = null; let lastUrl = location.href; /** * 等待元素加载 */ async function waitForElement(selector, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const elem = document.querySelector(selector); if (elem) return elem; await new Promise(resolve => setTimeout(resolve, 200)); } return null; } /** * 一些全局或静态配置 */ // 用来排除的菜单项:这些不是歌单,而是其他功能 const EXCLUDE_MENU_TEXTS = [ '收藏到歌单', // 本身这个是父项 '下一首播放', '播放', '复制歌名', '下载', '取消收藏' // 这个一般是“取消收藏”项,也不是歌单 ]; // 如果子菜单里出现了这行文本,就表示是“取消收藏”选项 // 你可以改成自己在菜单中看到的文本,例如“从歌单移除”或“移除收藏” const CANCEL_FAV_TEXT = '取消收藏'; // 用来记录每个列当前是 "asc" 还是 "desc" (升序还是降序) let sortOrderState = { title: 'asc', artist: 'asc', album: 'asc', duration: 'asc' }; /** * 延时函数,用于在菜单展开、请求发送时做等待,防止操作过快 */ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 获取表头行: * 因为类名中含有 `$`,我们采用 [class*="text-$gray"] 来规避选择器冲突 */ function getHeaderRow() { // 例子: .arco-row.h-10.leading-10.px-2[class*="text-$gray"] return document.querySelector('.arco-row.h-10.leading-10.px-2[class*="text-$gray"]'); } /** * 获取所有“真正的歌曲行” * 你提到它是 div.item.relative[playlist] */ function getSongItems() { return document.querySelectorAll('div.item.relative[playlist]'); } /** * 给表头第一列插入“全选”复选框,给其他列绑定“多次点击->升序/降序”事件 */ function enhanceHeaderRow() { const headerRow = getHeaderRow(); if (!headerRow) { console.log('未找到表头行,无法设置全选和列点击排序。'); return; } const colEls = headerRow.querySelectorAll('.arco-col'); if (colEls.length < 2) { console.log('表头列数不足,无法正常设置复选框或排序事件。'); return; } // 假设 colEls[0] 就是最左侧的一列 const firstCol = colEls[0]; // 创建“全选”checkbox const selectAllCb = document.createElement('input'); selectAllCb.type = 'checkbox'; selectAllCb.style.cursor = 'pointer'; selectAllCb.title = '全选 / 全不选'; // 点击事件:选中/取消选中所有歌曲复选框 selectAllCb.addEventListener('change', () => { const checked = selectAllCb.checked; const songCbs = document.querySelectorAll('.song-select-checkbox'); songCbs.forEach(cb => { cb.checked = checked; }); }); firstCol.appendChild(selectAllCb); // 绑定列点击 -> 升序 / 降序 // 这里假设:colEls[1] 是标题, colEls[2] 是歌手, colEls[3] 是专辑, colEls[4] 是时长 function bindSort(colIndex, fieldKey) { if (colEls[colIndex]) { colEls[colIndex].style.cursor = 'pointer'; colEls[colIndex].addEventListener('click', () => { // 切换 sortOrderState[fieldKey] 的 asc/desc sortOrderState[fieldKey] = (sortOrderState[fieldKey] === 'asc') ? 'desc' : 'asc'; sortSongsBy(fieldKey, sortOrderState[fieldKey]); }); } } bindSort(1, 'title'); // 标题 bindSort(2, 'artist'); // 歌手 bindSort(3, 'album'); // 专辑 bindSort(4, 'duration'); // 时长 } /** * 在每首歌曲行最左侧插入复选框。若已经有了就不重复添加。 * 我们假设 .arco-row 里 colEls[0] 可能是图片/空位 */ function addCheckboxesToSongs() { const songItems = getSongItems(); songItems.forEach(song => { const row = song.querySelector('.arco-row'); if (!row) return; const cols = row.querySelectorAll('.arco-col'); if (cols.length === 0) return; // 在 cols[0] 里放一个复选框(如果还没有) const newCol = document.createElement('div'); newCol.className = 'arco-col text-center'; newCol.style.flex = '0 0 30px'; newCol.style.paddingLeft = '6px'; newCol.style.paddingRight = '6px'; if (!cols[0].querySelector('.song-select-checkbox')) { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'song-select-checkbox'; checkbox.style.cursor = 'pointer'; // 也可以根据需要给它 data-* 属性,比如 data-title = ... newCol.appendChild(checkbox); row.prepend(newCol); } }); } /** * 排序函数:对所有歌曲进行升序/降序排列 * @param {string} field - 'title' | 'artist' | 'album' | 'duration' * @param {string} order - 'asc' or 'desc' */ function sortSongsBy(field, order) { const songItems = Array.from(getSongItems()); if (songItems.length === 0) return; // 拿到它们的共同父容器 const parent = songItems[0].parentElement; if (!parent) { console.log('未能定位歌曲父容器,无法排序。'); return; } // 提取某列文本 function getFieldValue(songEl, f) { const row = songEl.querySelector('.arco-row'); if (!row) return ''; const cols = row.querySelectorAll('.arco-col'); // 这里要对应上: // 0 => 复选框 + 图片? // 1 => 标题 // 2 => 歌手 // 3 => 专辑 // 4 => 时长 // 5 => 三点按钮? switch (f) { case 'title': return cols[2]?.textContent.trim() ?? ''; case 'artist': return cols[3]?.textContent.trim() ?? ''; case 'album': return cols[4]?.textContent.trim() ?? ''; case 'duration': return cols[5]?.textContent.trim() ?? ''; default: return ''; } } songItems.sort((a, b) => { const valA = getFieldValue(a, field); const valB = getFieldValue(b, field); if (order === 'asc') { return valA.localeCompare(valB); } else { return valB.localeCompare(valA); } }); // 重新插入 DOM songItems.forEach(el => parent.appendChild(el)); console.log(`已按 [${field}] 字段 ${order === 'asc' ? '升序' : '降序'} 排列`); } /** * 从子菜单里读取“可用的歌单”列表 * 做法:对一首歌执行:三点 -> 收藏到歌单(悬停) -> 读取子菜单 -> 排除 EXCLUDE_MENU_TEXTS * * 返回一个对象数组:[{text: 'xxx', element: <div>}, ...] */ async function readPlaylistsFromOneSong(songItem) { // 1. 点击“三点”按钮 const moreBtn = songItem.querySelector('button[class*="arco-btn"][class*="arco-btn-text"][class*="arco-btn-shape-circle"]'); if (!moreBtn) { console.log('readPlaylistsFromOneSong: 未找到三点按钮'); return []; } moreBtn.click(); await delay(500); // 2. 找到“收藏到歌单” let menuItems = document.querySelectorAll('.mx-context-menu-item'); const favMenu = Array.from(menuItems).find(m => m.textContent.includes('收藏到歌单') ); if (!favMenu) { console.log('readPlaylistsFromOneSong: 未找到“收藏到歌单”菜单项'); moreBtn.click(); // 关菜单 return []; } // 3. 悬停展开 favMenu.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true, view: window })); await delay(500); // 4. 再次获取子菜单项 menuItems = document.querySelectorAll('.mx-context-menu-item'); // 5. 关掉菜单 moreBtn.click(); await delay(300); // 6. 过滤掉 EXCLUDE_MENU_TEXTS // 只保留真正是“歌单名称”的那几项 const result = []; Array.from(menuItems).forEach(m => { const t = m.textContent.trim(); if (!t) return; // 如果 t 包含排除列表,则跳过 if (EXCLUDE_MENU_TEXTS.some(ex => t.includes(ex))) { return; } result.push({ text: t, element: m }); }); console.log(`读取到 ${result.length} 个歌单:`, result.map(r => r.text)); return result; } /** * 在页面左侧插入一个浮动面板,包含: * - 一个“读取歌单列表”按钮 * - 动态显示所有歌单项,点击后可执行批量收藏 * - 一个“批量取消收藏”按钮(对勾选歌曲执行“取消收藏”) */ function addControlPanel() { if (document.getElementById('customControlPanel')) { return; // 已添加过就不重复 } const panel = document.createElement('div'); panel.id = 'customControlPanel'; Object.assign(panel.style, { position: 'fixed', top: '600px', left: '10px', width: '200px', padding: '10px', backgroundColor: '#f7f7f7', border: '1px solid #ddd', borderRadius: '6px', zIndex: 999999 }); const title = document.createElement('div'); title.textContent = '批量收藏 / 取消收藏'; title.style.fontWeight = 'bold'; title.style.marginBottom = '8px'; panel.appendChild(title); // “读取歌单列表”按钮 const readListBtn = document.createElement('button'); readListBtn.textContent = '读取歌单列表'; readListBtn.style.width = '100%'; readListBtn.addEventListener('click', async () => { // 先随便选一首歌曲(比如第一首)来读取其子菜单 const allSongs = getSongItems(); if (allSongs.length === 0) { alert('找不到任何歌曲,无法读取歌单列表'); return; } const playlists = await readPlaylistsFromOneSong(allSongs[0]); // 然后在 panel 里显示这些歌单名称 showPlaylistButtons(playlists); }); panel.appendChild(readListBtn); // 占位:用于插入歌单按钮 const playlistContainer = document.createElement('div'); playlistContainer.id = 'playlistContainer'; playlistContainer.style.marginTop = '8px'; panel.appendChild(playlistContainer); // “批量取消收藏”按钮 const cancelFavBtn = document.createElement('button'); cancelFavBtn.textContent = '批量取消收藏(选中)'; cancelFavBtn.style.marginTop = '8px'; cancelFavBtn.style.width = '100%'; cancelFavBtn.addEventListener('click', async () => { await batchCancelFavoriteSelected(); }); panel.appendChild(cancelFavBtn); document.body.appendChild(panel); } /** * 在面板里生成一组按钮,每个按钮对应一个歌单。 * 点击后,对选中的歌曲执行“收藏到该歌单” */ function showPlaylistButtons(playlists) { const container = document.getElementById('playlistContainer'); if (!container) return; // 先清空 container.innerHTML = ''; if (!playlists || playlists.length === 0) { container.innerHTML = '<div style="color:red;margin-top:6px;">未读取到任何歌单</div>'; return; } container.innerHTML = '<div style="margin: 4px 0;">请选择要收藏到的歌单:</div>'; playlists.forEach(p => { const btn = document.createElement('button'); btn.textContent = p.text; btn.style.display = 'block'; btn.style.width = '100%'; btn.style.marginBottom = '4px'; btn.addEventListener('click', () => { batchFavoriteSelectedSongs(p.text); }); container.appendChild(btn); }); } /** * 对勾选的歌曲执行“收藏到指定歌单” */ async function batchFavoriteSelectedSongs(playlistName) { const allSongs = getSongItems(); if (allSongs.length === 0) { alert('找不到任何歌曲行'); return; } // 筛选出勾选的 const selectedSongs = Array.from(allSongs).filter(s => { const cb = s.querySelector('.song-select-checkbox'); return cb && cb.checked; }); if (selectedSongs.length === 0) { alert('你还没勾选任何歌曲'); return; } console.log(`开始收藏 ${selectedSongs.length} 首歌曲到【${playlistName}】...`); for (let i = 0; i < selectedSongs.length; i++) { const song = selectedSongs[i]; console.log(`第 ${i+1} 首: 准备收藏到【${playlistName}】`); const moreBtn = song.querySelector('button[class*="arco-btn"][class*="arco-btn-text"][class*="arco-btn-shape-circle"]'); if (!moreBtn) { console.log('未找到三点按钮,跳过'); continue; } moreBtn.click(); await delay(500); let menuItems = document.querySelectorAll('.mx-context-menu-item'); const favMenu = Array.from(menuItems).find(m => m.textContent.includes('收藏到歌单') ); if (!favMenu) { console.log('未找到“收藏到歌单”菜单项'); moreBtn.click(); continue; } // 悬停 favMenu.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true, view: window })); await delay(500); // 找到目标歌单 menuItems = document.querySelectorAll('.mx-context-menu-item'); const targetItem = Array.from(menuItems).find(m => m.textContent.includes(playlistName) ); if (!targetItem) { console.log(`未找到【${playlistName}】选项`); moreBtn.click(); continue; } targetItem.click(); console.log(`已收藏到【${playlistName}】`); await delay(300); } console.log('批量收藏完毕'); } /** * 对勾选的歌曲执行“取消收藏” * 即点击三点 -> 找到“取消收藏” -> 点击 */ async function batchCancelFavoriteSelected() { const allSongs = getSongItems(); if (allSongs.length === 0) { alert('没有歌曲行'); return; } // 找勾选 const selectedSongs = Array.from(allSongs).filter(s => { const cb = s.querySelector('.song-select-checkbox'); return cb && cb.checked; }); if (selectedSongs.length === 0) { alert('你还没勾选任何歌曲'); return; } console.log(`准备对 ${selectedSongs.length} 首歌曲执行“取消收藏”...`); for (let i = 0; i < selectedSongs.length; i++) { const song = selectedSongs[i]; console.log(`第 ${i+1} 首: 准备取消收藏`); const moreBtn = song.querySelector('button[class*="arco-btn"][class*="arco-btn-text"][class*="arco-btn-shape-circle"]'); if (!moreBtn) { console.log('未找到三点按钮,跳过'); continue; } moreBtn.click(); await delay(500); const menuItems = document.querySelectorAll('.mx-context-menu-item'); const cancelItem = Array.from(menuItems).find(m => m.textContent.includes(CANCEL_FAV_TEXT) ); if (!cancelItem) { console.log(`未找到“${CANCEL_FAV_TEXT}”选项,此歌曲可能不是已收藏状态`); moreBtn.click(); continue; } cancelItem.click(); console.log('已点击 取消收藏'); await delay(300); } console.log('批量取消收藏操作结束'); } /** * 等待指定元素出现(避免 `main()` 执行时找不到表头) */ async function waitForElement(selector, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const elem = document.querySelector(selector); if (elem) return elem; await new Promise(resolve => setTimeout(resolve, 200)); } console.warn(`⚠️ 超时: 未能找到元素 ${selector}`); return null; } /** * 主函数:先等待表头元素加载,再执行主要逻辑 */ async function main() { console.log("🔄 执行 main()..."); // 等待表头加载 const headerRow = await waitForElement('.arco-row.h-10.leading-10.px-2[class*="text-$gray"]'); if (!headerRow) { console.warn('🚨 表头未能正确加载,跳过执行 main()'); return; } enhanceHeaderRow(); addCheckboxesToSongs(); addControlPanel(); } /** * 监听 URL 变化,并在目标元素加载后执行 `main()` */ function checkUrlChange() { if (location.href !== lastUrl) { lastUrl = location.href; console.log('🔄 检测到 URL 变化,重新执行脚本...'); // **等待表头加载后再执行 main()** waitForElement('.arco-row.h-10.leading-10.px-2[class*="text-$gray"]').then(header => { if (header) { main(); } }); } setTimeout(checkUrlChange, 1000); } // **首次执行** if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main, { once: true }); } else { main(); } checkUrlChange(); })(); // @match *://tools.liumingye.cn/music/* // @grant none // ==/UserScript== (function() { 'use strict'; let observer = null; let lastUrl = location.href; /** * 等待元素加载 */ async function waitForElement(selector, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const elem = document.querySelector(selector); if (elem) return elem; await new Promise(resolve => setTimeout(resolve, 200)); } return null; } /** * 一些全局或静态配置 */ // 用来排除的菜单项:这些不是歌单,而是其他功能 const EXCLUDE_MENU_TEXTS = [ '收藏到歌单', // 本身这个是父项 '下一首播放', '播放', '复制歌名', '下载', '取消收藏' // 这个一般是“取消收藏”项,也不是歌单 ]; // 如果子菜单里出现了这行文本,就表示是“取消收藏”选项 // 你可以改成自己在菜单中看到的文本,例如“从歌单移除”或“移除收藏” const CANCEL_FAV_TEXT = '取消收藏'; // 用来记录每个列当前是 "asc" 还是 "desc" (升序还是降序) let sortOrderState = { title: 'asc', artist: 'asc', album: 'asc', duration: 'asc' }; /** * 延时函数,用于在菜单展开、请求发送时做等待,防止操作过快 */ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 获取表头行: * 因为类名中含有 `$`,我们采用 [class*="text-$gray"] 来规避选择器冲突 */ function getHeaderRow() { // 例子: .arco-row.h-10.leading-10.px-2[class*="text-$gray"] return document.querySelector('.arco-row.h-10.leading-10.px-2[class*="text-$gray"]'); } /** * 获取所有“真正的歌曲行” * 你提到它是 div.item.relative[playlist] */ function getSongItems() { return document.querySelectorAll('div.item.relative[playlist]'); } /** * 给表头第一列插入“全选”复选框,给其他列绑定“多次点击->升序/降序”事件 */ function enhanceHeaderRow() { const headerRow = getHeaderRow(); if (!headerRow) { console.log('未找到表头行,无法设置全选和列点击排序。'); return; } const colEls = headerRow.querySelectorAll('.arco-col'); if (colEls.length < 2) { console.log('表头列数不足,无法正常设置复选框或排序事件。'); return; } // 假设 colEls[0] 就是最左侧的一列 const firstCol = colEls[0]; // 创建“全选”checkbox const selectAllCb = document.createElement('input'); selectAllCb.type = 'checkbox'; selectAllCb.style.cursor = 'pointer'; selectAllCb.title = '全选 / 全不选'; // 点击事件:选中/取消选中所有歌曲复选框 selectAllCb.addEventListener('change', () => { const checked = selectAllCb.checked; const songCbs = document.querySelectorAll('.song-select-checkbox'); songCbs.forEach(cb => { cb.checked = checked; }); }); firstCol.appendChild(selectAllCb); // 绑定列点击 -> 升序 / 降序 // 这里假设:colEls[1] 是标题, colEls[2] 是歌手, colEls[3] 是专辑, colEls[4] 是时长 function bindSort(colIndex, fieldKey) { if (colEls[colIndex]) { colEls[colIndex].style.cursor = 'pointer'; colEls[colIndex].addEventListener('click', () => { // 切换 sortOrderState[fieldKey] 的 asc/desc sortOrderState[fieldKey] = (sortOrderState[fieldKey] === 'asc') ? 'desc' : 'asc'; sortSongsBy(fieldKey, sortOrderState[fieldKey]); }); } } bindSort(1, 'title'); // 标题 bindSort(2, 'artist'); // 歌手 bindSort(3, 'album'); // 专辑 bindSort(4, 'duration'); // 时长 } /** * 在每首歌曲行最左侧插入复选框。若已经有了就不重复添加。 * 我们假设 .arco-row 里 colEls[0] 可能是图片/空位 */ function addCheckboxesToSongs() { const songItems = getSongItems(); songItems.forEach(song => { const row = song.querySelector('.arco-row'); if (!row) return; const cols = row.querySelectorAll('.arco-col'); if (cols.length === 0) return; // 在 cols[0] 里放一个复选框(如果还没有) const newCol = document.createElement('div'); newCol.className = 'arco-col text-center'; newCol.style.flex = '0 0 30px'; newCol.style.paddingLeft = '6px'; newCol.style.paddingRight = '6px'; if (!cols[0].querySelector('.song-select-checkbox')) { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'song-select-checkbox'; checkbox.style.cursor = 'pointer'; // 也可以根据需要给它 data-* 属性,比如 data-title = ... newCol.appendChild(checkbox); row.prepend(newCol); } }); } /** * 排序函数:对所有歌曲进行升序/降序排列 * @param {string} field - 'title' | 'artist' | 'album' | 'duration' * @param {string} order - 'asc' or 'desc' */ function sortSongsBy(field, order) { const songItems = Array.from(getSongItems()); if (songItems.length === 0) return; // 拿到它们的共同父容器 const parent = songItems[0].parentElement; if (!parent) { console.log('未能定位歌曲父容器,无法排序。'); return; } // 提取某列文本 function getFieldValue(songEl, f) { const row = songEl.querySelector('.arco-row'); if (!row) return ''; const cols = row.querySelectorAll('.arco-col'); // 这里要对应上: // 0 => 复选框 + 图片? // 1 => 标题 // 2 => 歌手 // 3 => 专辑 // 4 => 时长 // 5 => 三点按钮? switch (f) { case 'title': return cols[2]?.textContent.trim() ?? ''; case 'artist': return cols[3]?.textContent.trim() ?? ''; case 'album': return cols[4]?.textContent.trim() ?? ''; case 'duration': return cols[5]?.textContent.trim() ?? ''; default: return ''; } } songItems.sort((a, b) => { const valA = getFieldValue(a, field); const valB = getFieldValue(b, field); if (order === 'asc') { return valA.localeCompare(valB); } else { return valB.localeCompare(valA); } }); // 重新插入 DOM songItems.forEach(el => parent.appendChild(el)); console.log(`已按 [${field}] 字段 ${order === 'asc' ? '升序' : '降序'} 排列`); } /** * 从子菜单里读取“可用的歌单”列表 * 做法:对一首歌执行:三点 -> 收藏到歌单(悬停) -> 读取子菜单 -> 排除 EXCLUDE_MENU_TEXTS * * 返回一个对象数组:[{text: 'xxx', element: <div>}, ...] */ async function readPlaylistsFromOneSong(songItem) { // 1. 点击“三点”按钮 const moreBtn = songItem.querySelector('button[class*="arco-btn"][class*="arco-btn-text"][class*="arco-btn-shape-circle"]'); if (!moreBtn) { console.log('readPlaylistsFromOneSong: 未找到三点按钮'); return []; } moreBtn.click(); await delay(500); // 2. 找到“收藏到歌单” let menuItems = document.querySelectorAll('.mx-context-menu-item'); const favMenu = Array.from(menuItems).find(m => m.textContent.includes('收藏到歌单') ); if (!favMenu) { console.log('readPlaylistsFromOneSong: 未找到“收藏到歌单”菜单项'); moreBtn.click(); // 关菜单 return []; } // 3. 悬停展开 favMenu.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true, view: window })); await delay(500); // 4. 再次获取子菜单项 menuItems = document.querySelectorAll('.mx-context-menu-item'); // 5. 关掉菜单 moreBtn.click(); await delay(300); // 6. 过滤掉 EXCLUDE_MENU_TEXTS // 只保留真正是“歌单名称”的那几项 const result = []; Array.from(menuItems).forEach(m => { const t = m.textContent.trim(); if (!t) return; // 如果 t 包含排除列表,则跳过 if (EXCLUDE_MENU_TEXTS.some(ex => t.includes(ex))) { return; } result.push({ text: t, element: m }); }); console.log(`读取到 ${result.length} 个歌单:`, result.map(r => r.text)); return result; } /** * 在页面左侧插入一个浮动面板,包含: * - 一个“读取歌单列表”按钮 * - 动态显示所有歌单项,点击后可执行批量收藏 * - 一个“批量取消收藏”按钮(对勾选歌曲执行“取消收藏”) */ function addControlPanel() { if (document.getElementById('customControlPanel')) { return; // 已添加过就不重复 } const panel = document.createElement('div'); panel.id = 'customControlPanel'; Object.assign(panel.style, { position: 'fixed', top: '600px', left: '10px', width: '200px', padding: '10px', backgroundColor: '#f7f7f7', border: '1px solid #ddd', borderRadius: '6px', zIndex: 999999 }); const title = document.createElement('div'); title.textContent = '批量收藏 / 取消收藏'; title.style.fontWeight = 'bold'; title.style.marginBottom = '8px'; panel.appendChild(title); // “读取歌单列表”按钮 const readListBtn = document.createElement('button'); readListBtn.textContent = '读取歌单列表'; readListBtn.style.width = '100%'; readListBtn.addEventListener('click', async () => { // 先随便选一首歌曲(比如第一首)来读取其子菜单 const allSongs = getSongItems(); if (allSongs.length === 0) { alert('找不到任何歌曲,无法读取歌单列表'); return; } const playlists = await readPlaylistsFromOneSong(allSongs[0]); // 然后在 panel 里显示这些歌单名称 showPlaylistButtons(playlists); }); panel.appendChild(readListBtn); // 占位:用于插入歌单按钮 const playlistContainer = document.createElement('div'); playlistContainer.id = 'playlistContainer'; playlistContainer.style.marginTop = '8px'; panel.appendChild(playlistContainer); // “批量取消收藏”按钮 const cancelFavBtn = document.createElement('button'); cancelFavBtn.textContent = '批量取消收藏(选中)'; cancelFavBtn.style.marginTop = '8px'; cancelFavBtn.style.width = '100%'; cancelFavBtn.addEventListener('click', async () => { await batchCancelFavoriteSelected(); }); panel.appendChild(cancelFavBtn); document.body.appendChild(panel); } /** * 在面板里生成一组按钮,每个按钮对应一个歌单。 * 点击后,对选中的歌曲执行“收藏到该歌单” */ function showPlaylistButtons(playlists) { const container = document.getElementById('playlistContainer'); if (!container) return; // 先清空 container.innerHTML = ''; if (!playlists || playlists.length === 0) { container.innerHTML = '<div style="color:red;margin-top:6px;">未读取到任何歌单</div>'; return; } container.innerHTML = '<div style="margin: 4px 0;">请选择要收藏到的歌单:</div>'; playlists.forEach(p => { const btn = document.createElement('button'); btn.textContent = p.text; btn.style.display = 'block'; btn.style.width = '100%'; btn.style.marginBottom = '4px'; btn.addEventListener('click', () => { batchFavoriteSelectedSongs(p.text); }); container.appendChild(btn); }); } /** * 对勾选的歌曲执行“收藏到指定歌单” */ async function batchFavoriteSelectedSongs(playlistName) { const allSongs = getSongItems(); if (allSongs.length === 0) { alert('找不到任何歌曲行'); return; } // 筛选出勾选的 const selectedSongs = Array.from(allSongs).filter(s => { const cb = s.querySelector('.song-select-checkbox'); return cb && cb.checked; }); if (selectedSongs.length === 0) { alert('你还没勾选任何歌曲'); return; } console.log(`开始收藏 ${selectedSongs.length} 首歌曲到【${playlistName}】...`); for (let i = 0; i < selectedSongs.length; i++) { const song = selectedSongs[i]; console.log(`第 ${i+1} 首: 准备收藏到【${playlistName}】`); const moreBtn = song.querySelector('button[class*="arco-btn"][class*="arco-btn-text"][class*="arco-btn-shape-circle"]'); if (!moreBtn) { console.log('未找到三点按钮,跳过'); continue; } moreBtn.click(); await delay(500); let menuItems = document.querySelectorAll('.mx-context-menu-item'); const favMenu = Array.from(menuItems).find(m => m.textContent.includes('收藏到歌单') ); if (!favMenu) { console.log('未找到“收藏到歌单”菜单项'); moreBtn.click(); continue; } // 悬停 favMenu.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true, view: window })); await delay(500); // 找到目标歌单 menuItems = document.querySelectorAll('.mx-context-menu-item'); const targetItem = Array.from(menuItems).find(m => m.textContent.includes(playlistName) ); if (!targetItem) { console.log(`未找到【${playlistName}】选项`); moreBtn.click(); continue; } targetItem.click(); console.log(`已收藏到【${playlistName}】`); await delay(300); } console.log('批量收藏完毕'); } /** * 对勾选的歌曲执行“取消收藏” * 即点击三点 -> 找到“取消收藏” -> 点击 */ async function batchCancelFavoriteSelected() { const allSongs = getSongItems(); if (allSongs.length === 0) { alert('没有歌曲行'); return; } // 找勾选 const selectedSongs = Array.from(allSongs).filter(s => { const cb = s.querySelector('.song-select-checkbox'); return cb && cb.checked; }); if (selectedSongs.length === 0) { alert('你还没勾选任何歌曲'); return; } console.log(`准备对 ${selectedSongs.length} 首歌曲执行“取消收藏”...`); for (let i = 0; i < selectedSongs.length; i++) { const song = selectedSongs[i]; console.log(`第 ${i+1} 首: 准备取消收藏`); const moreBtn = song.querySelector('button[class*="arco-btn"][class*="arco-btn-text"][class*="arco-btn-shape-circle"]'); if (!moreBtn) { console.log('未找到三点按钮,跳过'); continue; } moreBtn.click(); await delay(500); const menuItems = document.querySelectorAll('.mx-context-menu-item'); const cancelItem = Array.from(menuItems).find(m => m.textContent.includes(CANCEL_FAV_TEXT) ); if (!cancelItem) { console.log(`未找到“${CANCEL_FAV_TEXT}”选项,此歌曲可能不是已收藏状态`); moreBtn.click(); continue; } cancelItem.click(); console.log('已点击 取消收藏'); await delay(300); } console.log('批量取消收藏操作结束'); } /** * 等待指定元素出现(避免 `main()` 执行时找不到表头) */ async function waitForElement(selector, timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const elem = document.querySelector(selector); if (elem) return elem; await new Promise(resolve => setTimeout(resolve, 200)); } console.warn(`⚠️ 超时: 未能找到元素 ${selector}`); return null; } /** * 主函数:先等待表头元素加载,再执行主要逻辑 */ async function main() { console.log("🔄 执行 main()..."); // 等待表头加载 const headerRow = await waitForElement('.arco-row.h-10.leading-10.px-2[class*="text-$gray"]'); if (!headerRow) { console.warn('🚨 表头未能正确加载,跳过执行 main()'); return; } enhanceHeaderRow(); addCheckboxesToSongs(); addControlPanel(); } /** * 监听 URL 变化,并在目标元素加载后执行 `main()` */ function checkUrlChange() { if (location.href !== lastUrl) { lastUrl = location.href; console.log('🔄 检测到 URL 变化,重新执行脚本...'); // **等待表头加载后再执行 main()** waitForElement('.arco-row.h-10.leading-10.px-2[class*="text-$gray"]').then(header => { if (header) { main(); } }); } setTimeout(checkUrlChange, 1000); } // **首次执行** if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main, { once: true }); } else { main(); } checkUrlChange(); })();