认识我的朋友都知道,我是个热爱音乐的博主,值此国庆小长假来临之际,自当整理一下我的Car Music,确保旅途中动次打次不停歇。

以前都是手动整理,从网易云、QQ 音乐下载歌曲,手动解密,然后拷贝到 U 盘。于是,我面临以下重复工作:

  1. 下载歌曲,下载歌词;
  2. 拷贝到 U 盘;
  3. 痛苦的肉眼翻一遍 U 盘里的歌曲,确保“过时”(emm……就是突然不喜欢听的歌,懂得都懂)歌曲被手动移除;

尤其在第三步,很繁琐。于是就有了本篇分享。

我为什么建议你在电脑另存一份 Car Music

诚然,现在车联网很方便,在线听歌很便捷。当仍然无法解决付费歌曲、隧道网络差、自由歌单这些痛点。同时,一般车机上,偶尔会蹦一个“热插拔”异常,导致 U 盘歌曲丢失。(我那尊贵的 CoffeeOS 车机,已经让我体验过 2 次了,真好)

所以我真诚建议你,在电脑备份一份文件。尤其是现在固态硬盘白菜价的时候,别犹豫了!

教程开始

本篇分享脚本基于node.js实现,歌词下载使用开源工具ZonyLrcToolsX。因此开始之前,请先下载安装node.js(过于简单,直接放个链接吧(记得添加到环境变量,不懂 Google 下):Node.js 安装配置

ZonyLrcToolsX 下载地址:ZonyLrcToolsX/releases

将 ZonyLrcToolsX 包解压到电脑本地某个目录,记下这个目录地址。在电脑一个你记得住的位置,创建一个 car_music.txt 文件,内容是下面这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
const fs = require('fs').promises; // 使用 fs.promises 版本
const readline = require('readline');
const path = require('path');

const sourceDirectory = 'E:/car music';
const targetDirectory = 'F:/car music';

const addedList = [];
const deleteList = [];

function printSeparator() {
console.log('---------------------------');
}

// 获取格式化的时间戳
function getFormattedTimestamp(info) {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
if (info) {
return `[${hours}:${minutes}:${seconds} INF]`;
} else {
return `[${hours}:${minutes}:${seconds} WRN]`;
}
}

async function copyFiles() {
for (const file of addedList) {
try {
await fs.copyFile(file, `${targetDirectory}/${path.basename(file)}`);
console.log(`${getFormattedTimestamp(1)} 拷贝成功:${file}`);
} catch (err) {
console.error(`${getFormattedTimestamp(0)} 拷贝失败:${file}`);
}
}
}

async function deleteFiles() {
for (const file of deleteList) {
try {
await fs.unlink(file);
console.log(`${getFormattedTimestamp(1)} 删除成功:${file}`);
} catch (err) {
console.error(`${getFormattedTimestamp(0)} 删除失败:${file}`);
}
}
}

async function confirmCopy() {
printSeparator();
console.log(`${getFormattedTimestamp(1)} 扫描完成,待添加文件列表如下:`);
addedList.forEach((file) => {
console.log(file);
});
printSeparator();

const rl1 = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

return new Promise((resolve) => {
rl1.question('确认帮您拷贝添加到 U 盘吗? (no/yes): ', async (confirmAdd) => {
if (confirmAdd.toLowerCase() === 'n' || confirmAdd.toLowerCase() === 'no') {
printSeparator();
console.log(`${getFormattedTimestamp(1)} 您拒绝了添加文件。`);
resolve();
} else if (confirmAdd.toLowerCase() === 'y' || confirmAdd.toLowerCase() === 'yes') {
printSeparator();
await copyFiles();
resolve();
} else {
printSeparator();
console.log(`${getFormattedTimestamp(0)} 您键入的指令不正确,已跳过本次操作。`);
resolve();
}

// 关闭确认添加的输入流
rl1.close();
});
});
}

async function confirmDelete() {
// 打印 deleteList 并请求确认
if (deleteList.length > 0) {
printSeparator();
console.log(`${getFormattedTimestamp(1)} 待删除文件列表如下:`);
deleteList.forEach((file) => {
console.log(file);
});

const rl2 = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

printSeparator();

return new Promise((resolve) => {
rl2.question('确认帮您删除 U 盘上述文件吗? (no/yes): ', async (confirmDelete) => {
if (confirmDelete.toLowerCase() === 'n' || confirmDelete.toLowerCase() === 'no') {
printSeparator();
console.log(`${getFormattedTimestamp(1)} 您拒绝了删除文件。`);
} else if (confirmDelete.toLowerCase() === 'y' || confirmDelete.toLowerCase() === 'yes') {
printSeparator();
await deleteFiles();
printSeparator();
console.log(`${getFormattedTimestamp(1)} 删除操作已完成。`);
} else {
printSeparator();
console.log(`${getFormattedTimestamp(0)} 您键入的指令不正确,已跳过本次操作。`);
}

// 关闭确认删除的输入流
rl2.close();
resolve();
});
});
} else {
printSeparator();
console.log(`${getFormattedTimestamp(1)} 没有待删除的文件。`);
}
}

async function main() {
try {
const sourceFiles = await fs.readdir(sourceDirectory);
const targetFiles = await fs.readdir(targetDirectory);

for (const fileName of sourceFiles) {
const sourceFilePath = `${sourceDirectory}/${fileName}`;
const targetFilePath = `${targetDirectory}/${fileName}`;

if (!targetFiles.includes(fileName)) {
// 文件不存在于 targetDirectory,将其添加到 addedList
addedList.push(sourceFilePath);
}
}

for (const fileName of targetFiles) {
const sourceFilePath = `${sourceDirectory}/${fileName}`;
const targetFilePath = `${targetDirectory}/${fileName}`;

if (!sourceFiles.includes(fileName)) {
// 文件不存在于 sourceDirectory,将其添加到 deleteList
deleteList.push(targetFilePath);
}
}

await confirmDelete();
// 检查是否有文件需要拷贝
if (addedList.length > 0) {
// 开始确认拷贝
await confirmCopy();
} else {
printSeparator();
console.log(`${getFormattedTimestamp(1)} 没有待添加的文件。`);
}
} catch (err) {
console.error(err);
}
}

// 启动主程序
main();

将其中开头的这两行路径,修改为你自己的。第一个“E:/car music”为“电脑本地备份的音乐目录”,第二个“F:/car music”为“U盘上的音乐目录,注意盘符”:

1
2
const sourceDirectory = 'E:/car music';
const targetDirectory = 'F:/car music';

car_music.txt 文件,重命名为 car_music.js 文件。再创建一个 music_processor.txt 文件,实现鼠标双击执行的功能,可以放在任何位置,比如桌面方便找也可以。内容为:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

cd d:/home/tools/zonyLrcTools
./ZonyLrcTools.Cli.exe download -d "e:\car music" -l -n 2

cd e:/
node ./car_music.js

echo "---------------------------"
echo "脚本执行完成,按 Enter 键退出"
read

将其中的 d:/home/tools/zonyLrcTools 修改为你刚才下载的 zonyLrcToolsX 解压缩的目录。同样的,将其中的e:\car music 改为你电脑本地备份的歌曲目录。保存后,将 music_processor.txt 文件重命名为 music_processor.sh 文件。

1
2
cd e:/
node ./car_music.js

这两部分是进入你刚才保存 car_music.js 的目录,并执行它。因此地址请按你保存的目录修改,我这里放在了 E 盘根目录。

歌曲解密

在网易云、QQ 音乐下载的歌曲,有些是加密的(.ncm / .mflac 之类的格式),可以来这里解密(原作者牛皮):

大功告成

现在插上 U 盘,鼠标双击 music_processor.sh 文件,试试看吧。没插入 U 盘执行会报错,因为找不到对应地址。也可以本地弄两个文件夹,自己测试。
车载 U 盘音乐自动整理工具
推荐下寸铁 《近人可读》专辑,最近好上头。最后,预祝大家假期愉快,下次见~~