// ==UserScript==
// @name 网易云音乐列表导出为CSV文件
// @namespace undefined
// @version 0.0.5
// @description 导出当前页网易云音乐列表为CSV文件
// @author allen smith, aspen138
// @match *://music.163.com/*
// @license MIT
// @icon https://s1.music.126.net/style/favicon.ico
// @require https://update.greasyfork.org/scripts/27254/174357/clipboardjs.js
// @require https://update.greasyfork.org/scripts/482500/1297545/Sortable%20JS.js
// @run-at document-end
// @grant none
// ==/UserScript==
(function () {
'use strict';
// 检测页面
let htm = document.getElementsByClassName('f-oh');
if (htm.length === 0) {
return;
}
// 创建dom节点
function createDocument(txt) {
const template = `<div class='childdom'>${txt}</div>`;
let doc = new DOMParser().parseFromString(template, 'text/html');
let div = doc.querySelector('.childdom');
return div;
}
// 检测文档变动
let doc = document.getElementById('g_mymusic');
let _body = document.body;
let clipboard, btn, spli, interId, waitTimeoutId, wait;
let ckdiv;
let check1, check2, check3, check4, check5;
let sortdiv;
console.log("doc=", doc);
function DOMSubtreeModifiedEventHandler() {
console.log("DOMSubtreeModified event trigged.");
//查找列表动画
wait = document.getElementById('wait-animation');
if (wait) _body.removeChild(wait);
wait = document.createElement("span");
wait.id = 'wait-animation';
wait.setAttribute('style', 'display:inline-block;position:absolute;right:50px;top:100px;padding:3px 5px;border:1px solid lightgray;background-color:white;color:black;border-radius:5px;font-size:14px;');
_body.appendChild(wait);
wait.innerHTML = '导出:没有合适的列表';
//检测列表
console.log("find m-table elements", document.getElementsByClassName('m-table'));
let list = document.getElementsByClassName('m-table')[0];
console.log("if clause before, list=", list);
if (!list) {
btn = document.getElementById('export-btn');
spli = document.getElementById('export-spli');
if (btn) _body.removeChild(btn);
if (spli) _body.removeChild(spli);
return;
}
console.log("if clause after, list=", list);
_body.removeChild(wait);
//创建按钮
btn = null;
spli = null;
btn = document.getElementById('export-btn');
spli = document.getElementById('export-spli');
if (!spli) {
spli = document.createElement("input");
spli.id = 'export-spli';
spli.className = 'export-spli';
spli.setAttribute('placeholder', '自定义分隔符(默认 -- )');
spli.setAttribute('style', 'display:inline-block;position:absolute;right:50px;top:100px;padding:3px 5px;border:1px solid lightgray;background-color:white;color:black;border-radius:5px;font-size:14px;');
_body.appendChild(spli);
}
if (!btn) {
btn = document.createElement("button");
btn.id = 'export-btn';
btn.className = 'export-btn';
btn.innerText = '导出列表';
btn.setAttribute('style', 'display:inline-block;position:absolute;right:50px;top:229px;padding:3px 5px;border:1px solid lightgray;background-color:white;color:black;border-radius:5px;font-size:14px;');
_body.appendChild(btn);
}
// 选择列
if (!ckdiv) {
ckdiv = document.createElement("div");
ckdiv.id = 'ckdiv';
ckdiv.className = 'export-ck';
ckdiv.setAttribute('style', 'display:inline-block;position:absolute;right:50px;top:128px;padding:3px 5px;border:1px solid lightgray;background-color:white;color:black;border-radius:5px;font-size:14px;');
_body.appendChild(ckdiv);
}
// 排序
if (!sortdiv) {
// sortdiv = document.createElement("div");
// sortdiv.id = 'sortdiv';
// sortdiv.className = 'sortdiv';
// sortdiv.setAttribute('style', 'display:inline-block;position:absolute;right:50px;top:156px;padding:3px 5px;border:1px solid lightgray;background-color:white;color:black;border-radius:5px;font-size:14px;');
let divstr = `<div id="sortdivbox" style="display:inline-block;position:absolute;right:50px;top:159px;padding:3px 5px;border:1px solid lightgray;background-color:white;color:black;border-radius:5px;font-size:14px;"><div>拖动以排序</div><div id="sortdiv" style="margin:10px 0px;cursor:pointer;"><span style="margin:0 4px;padding:4px 8px;border-radius:3px;border:1px solid lightgray">歌名</span><span style="margin:0 4px;padding:4px 8px;border-radius:3px;border:1px solid lightgray">歌手</span><span style="margin:0 4px;padding:4px 8px;border-radius:3px;border:1px solid lightgray">专辑</span><span style="margin:0 4px;padding:4px 8px;border-radius:3px;border:1px solid lightgray">时长</span><span style="margin:0 4px;padding:4px 8px;border-radius:3px;border:1px solid lightgray">链接</span></div></div>`
_body.appendChild(createDocument(divstr));
sortdiv = new Sortable(document.querySelector('#sortdiv'))
}
let ckbuilder = function (id, label, uncheck, readonly) {
let tmpid = 'ck_' + id;
let ckbox = document.createElement("input");
ckbox.id = tmpid
ckbox.setAttribute('type', 'checkbox');
ckbox.setAttribute('style', 'vertical-align: middle;margin-top: -2px;');
if (!uncheck) ckbox.checked = true;
if (!!readonly) ckbox.setAttribute("disabled", "disabled");
ckdiv.appendChild(ckbox);
let ckspn = document.createElement("label");
ckspn.setAttribute('for', tmpid);
ckspn.innerHTML = ' ' + label;
ckdiv.appendChild(ckspn);
return ckbox;
}
if (!check1) {
check1 = ckbuilder("ck01", "歌名 ", false, true);
}
if (!check2) {
check2 = ckbuilder("ck02", "歌手 ");
}
if (!check3) {
check3 = ckbuilder("ck03", "专辑 ", true);
}
if (!check4) {
check4 = ckbuilder("ck04", "时长 ", true);
}
if (!check5) {
check5 = ckbuilder("ck05", "链接", true);
}
// 创建剪贴板
if (clipboard) clipboard.destroy();
// Example JavaScript with Clipboard.js and CSV download functionality
clipboard = new Clipboard('.export-btn', {
text: function (trigger) {
// 导出列表
btn.innerText = '正在导出 ...';
let result = '';
let csvContent = ''; // Initialize CSV content
let listBody = list.getElementsByTagName('tbody')[0];
let rows = listBody.getElementsByTagName('tr');
// Initialize CSV headers based on selected fields
let headers = [];
document.querySelectorAll('#sortdiv span').forEach(item => {
let type = item.innerText;
switch (type) {
case "歌名":
headers.push('歌名');
break;
case "歌手":
if (check2.checked) headers.push('歌手');
break;
case "专辑":
if (check3.checked) headers.push('专辑');
break;
case "时长":
if (check4.checked) headers.push('时长');
break;
case "链接":
if (check5.checked) headers.push('链接');
break;
}
});
csvContent += headers.join(',') + '\r\n'; // Add headers to CSV
for (let i = 0; i < rows.length; i++) {
let ele = rows[i];
let cells = ele.getElementsByTagName('td');
let name = cells[1].getElementsByTagName('b')[0].getAttribute('title')
.replace(/<div class="soil">[\s\S\n]*?<\/div>/g, "")
.replace(/ /g, " ")
.replace(/&/g, "&");
let link = `https://music.163.com/#${cells[1].getElementsByTagName('a')[0].getAttribute('href')}`;
let time = cells[2].querySelector('.u-dur').innerText;
let artist = cells[3].getElementsByTagName('span')[0].getAttribute('title')
.replace(/<div class="soil">[\s\S\n]*?<\/div>/g, "")
.replace(/ /g, " ")
.replace(/&/g, "&");
let album = cells[4].getElementsByTagName('a')[0].getAttribute('title')
.replace(/<div class="soil">[\s\S\n]*?<\/div>/g, "")
.replace(/ /g, " ")
.replace(/&/g, "&");
let spliChar = spli.value;
if (!spliChar) spliChar = ' -- ';
let isFirst = true;
let row = [];
document.querySelectorAll('#sortdiv span').forEach(item => {
let type = item.innerText;
let tempSplit;
if (isFirst) {
tempSplit = () => { isFirst = false; return ""; }
} else {
tempSplit = () => spliChar;
}
switch (type) {
case "歌名":
row.push(`"${name}"`); // Enclose in quotes to handle commas
break;
case "歌手":
if (check2.checked) {
row.push(`"${artist}"`);
}
break;
case "专辑":
if (check3.checked) {
row.push(`"${album}"`);
}
break;
case "时长":
if (check4.checked) {
row.push(`"${time}"`);
}
break;
case "链接":
if (check5.checked) {
row.push(`"${link}"`);
}
break;
}
});
result += row.join(spliChar) + '\r\n'; // For clipboard
csvContent += row.join(',') + '\r\n'; // For CSV
}
// 提示动画
btn.innerText = '已复制到剪贴板 =';
let count = 6;
clearInterval(interId);
interId = setInterval(function () {
count--;
if (count > 0) {
btn.innerText = '已复制到剪贴板 ' + waitAnimationChar(count);
}
else {
btn.innerText = '导出列表';
clearInterval(interId);
}
}, 300);
// 输出到控制台
console.log(result);
// 输出到剪贴板
trigger.setAttribute('aria-label', result);
// Store CSV content in a data attribute for later use
trigger.dataset.csv = csvContent;
return trigger.getAttribute('aria-label');
}
});
// Listen for the successful copy event to trigger CSV download
clipboard.on('success', function (e) {
// Retrieve CSV content from data attribute
let csvContent = e.trigger.dataset.csv;
// Encode CSV content
//前面加的那个uFEFF是utf-8 BOM的头,目的是把utf-8的文件变成utf-8 BOM,让微软的excel打开csv文件后不会乱码.
let encodedUri = 'data:text/csv;charset=utf-8,' + '\uFEFF' +encodeURIComponent(csvContent);
// Create a link to download the encoded URI
let link = document.createElement("a");
link.setAttribute("href", encodedUri);
let timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
link.setAttribute("download", `网易云音乐-export-${timestamp}.csv`);
document.body.appendChild(link); // Required for Firefox
link.click();
document.body.removeChild(link);
console.log("CSV file has been downloaded.");
});
}
doc.addEventListener('DOMSubtreeModified', DOMSubtreeModifiedEventHandler);
setTimeout(() => { DOMSubtreeModifiedEventHandler(); }, 3 * 1000);
//字符动画
let waitAnimationChar = function (n) {
let temp = n % 3;
if (temp === 0) return '#';
else if (temp == 1) return '$';
else if (temp == 2) return '+';
};
})();