yun-playlist-downloader
Version:
NetEase CloudMusic downloader
229 lines (227 loc) • 6.91 kB
JavaScript
import {
DEFAULT_COOKIE_FILE,
__filename,
baseDebug,
downloadSong,
getAdapter,
getFileName,
readCookie
} from "./chunk-YZICK6CX.js";
// src/cli.ts
import { createRequire } from "module";
import path from "path";
import CliTable from "cli-table3";
import { dl } from "dl-vampire";
import { delay } from "es-toolkit";
import filenamify from "filenamify";
import humanizeDuration from "humanize-duration";
import logSymbols from "log-symbols";
import ms from "ms";
import pmap from "promise.map";
import rcFactory from "rc";
import updateNotifier from "update-notifier";
import { hideBin } from "yargs/helpers";
import yargs from "yargs/yargs";
import { z, ZodError } from "zod";
import { fromError } from "zod-validation-error";
var debug = baseDebug.extend("cli");
var _require = createRequire(__filename);
var rawPackageJson = _require("../package.json");
var { version } = rawPackageJson;
updateNotifier({ pkg: rawPackageJson }).notify();
var DEFAULT_FORMAT = ":name/:singer - :songName.:ext";
if (process.argv.some((s) => s.match(/(dj)?radio/))) {
DEFAULT_FORMAT = ":name/:programDate 第:programOrder期 - :songName.:ext";
}
var config = rcFactory("yun", {
"concurrency": 5,
"format": DEFAULT_FORMAT,
"quality": 999,
"retry-timeout": 3,
// 3 mins
"retry-times": 3,
// 3 times
"skip": true,
"progress": true
});
yargs(hideBin(process.argv)).scriptName("yun").command(
"$0 <url>",
"网易云音乐 歌单/专辑 下载器",
// builder
(yargs2) => {
return yargs2.usage("Usage: $0 <url> [options]").positional("url", {
describe: "歌单/专辑/电台的链接 or 歌单 ID",
type: "string",
demandOption: true
// 直接运行 yun 飘红
}).alias({
h: "help",
v: "version",
c: "concurrency",
f: "format",
q: "quality",
s: "skip",
p: "progress"
}).options({
concurrency: {
desc: "同时下载数量",
type: "number",
default: 5
},
format: {
desc: "文件格式",
type: "string",
default: DEFAULT_FORMAT
},
quality: {
desc: "音质, 默认 999k 即最大码率, 可选 128/192/320",
type: "number",
default: 999,
choices: [128, 192, 320, 999]
},
retryTimeout: {
desc: "下载超时(分)",
type: "number",
default: 3
},
retryTimes: {
desc: "下载重试次数",
type: "number",
default: 3
},
skip: {
desc: "对于已存在文件且大小合适则跳过",
type: "boolean",
default: true
},
progress: {
desc: "是否显示进度条",
type: "boolean",
default: true
},
cover: {
desc: "下载封面",
type: "boolean",
default: false
},
cookie: {
desc: "cookie文件",
type: "string",
default: DEFAULT_COOKIE_FILE
},
skipTrial: {
desc: "跳过试听歌曲",
type: "boolean",
default: false
}
}).config(config).example(`$0 'https://music.163.com/#/playlist?id=7392714527'`, "下载歌单").example("$0 7392714527", "使用 id 下载歌单").example("$0 -c 10 <url>", "10首同时下载").example('$0 -f ":singer - :songName.:ext" <url>', '下载文件名为 "歌手 - 歌名"').epilog("帮助 & 文档: https://github.com/magicdawn/yun-playlist-downloader");
},
async (parsed) => {
try {
await defaultCommandAction(parsed);
} catch (e) {
if (e instanceof ZodError) {
const validationError = fromError(e);
console.error(validationError.toString());
throw validationError;
} else {
throw e;
}
}
}
).version(version).help().parse();
async function defaultCommandAction(options) {
let {
concurrency,
format,
quality,
retryTimeout,
retryTimes,
skip: skipExists,
progress,
cover,
cookie,
skipTrial
} = options;
let url = z.union([z.string().url(), z.string().regex(/^\d+$/)], {
errorMap: () => ({ message: "url 参数错误: 支持 url 或 歌单ID" })
}).parse(options.url);
const table = new CliTable({
head: [`${logSymbols.info} 当前参数`, "值", "备注"]
});
table.push(
["concurrency", concurrency, "同时下载数量"],
["format", format, "文件名模版"],
["quality", quality, "音质(kbps)"],
["retry-timeout", retryTimeout, "下载超时(分)"],
["retry-times", retryTimes, "下载重试(次)"],
["skip", skipExists, "跳过已下载正常文件"],
["progress", progress, "显示进度条"],
["cover", cover, "下载封面"],
["cookie", cookie, "cookie文件"],
["skip-trial", skipTrial, "跳过试听歌曲"]
);
console.log(`${table.toString()}
`);
quality *= 1e3;
retryTimeout = ms(`${retryTimeout}m`);
readCookie(cookie);
if (url && /^\d+$/.test(url)) {
url = `https://music.163.com/#/playlist?id=${url}`;
}
const start = Date.now();
const adapter = getAdapter(url);
const name = await adapter.getTitle() || "";
console.log(`${logSymbols.info} 正在下载「${name}」,请稍候...`);
if (cover) {
const coverUrl = await adapter.getCover();
if (!coverUrl) {
console.log(`${logSymbols.warning} [cover]: 没有找到封面`);
} else {
const coverExt = path.extname(coverUrl) || ".jpg";
const coverFile = `${filenamify(name)}/cover${coverExt}`;
await dl({ url: coverUrl, file: coverFile });
console.log(`${logSymbols.success} [cover]: 封面已下载 ${coverFile}`);
}
}
const songs = await adapter.getSongs(quality);
debug("songs : %j", songs);
const removed = songs.filter((x) => !x.url);
const keeped = songs.filter((x) => x.url);
if (removed.length) {
console.log(`${logSymbols.warning} [版权受限] 不可下载 ${removed.length}/${songs.length}`);
for (const i of removed) {
console.log(` ${i.singer} - ${i.songName}`);
}
}
const freeTrialCount = keeped.filter((x) => x.isFreeTrial).length;
console.log(`${logSymbols.info} 可下载 ${keeped.length}/${songs.length}, 试听 ${freeTrialCount}/${keeped.length}`);
const len = keeped.length.toString().length;
keeped.forEach((item, index) => {
item.rawIndex = index;
item.index = String(index + 1).padStart(len, "0");
});
await pmap(
keeped,
(song) => {
const file = getFileName({ format, song, url, name });
return downloadSong({
url: song.url,
file,
song,
totalLength: keeped.length,
retryTimeout,
retryTimes,
progress,
skipExists,
skipTrial
});
},
concurrency
);
await delay(100);
const dur = humanizeDuration(Date.now() - start, { language: "zh_CN" });
console.log("下载完成, 耗时%s", dur);
process.exit(0);
}