yun-playlist-downloader
Version:
NetEase CloudMusic downloader
217 lines (215 loc) • 6.36 kB
JavaScript
import {
DEFAULT_COOKIE_FILE,
__filename,
baseDebug,
downloadSong,
getAdapter,
getFileName,
readCookie
} from "./chunk-OH6RPAYW.js";
// src/bin.ts
import CliTable from "cli-table3";
import { dl } from "dl-vampire";
import filenamify from "filenamify";
import humanizeDuration from "humanize-duration";
import logSymbols from "log-symbols";
import { createRequire } from "module";
import ms from "ms";
import path from "path";
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
}
}).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
} = 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],
["retry-timeout", retryTimeout],
["retry-times", retryTimes],
["skip", skipExists],
["progress", progress],
["cover", cover],
["cookie", cookie]
);
console.log(table.toString() + "\n");
quality *= 1e3;
retryTimeout = ms(`${retryTimeout} minutes`);
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 (let i of removed) {
console.log(` ${i.singer} - ${i.songName}`);
}
}
console.log(`${logSymbols.info} 可下载 ${keeped.length}/${songs.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,
skipExists,
progress
});
},
concurrency
);
const dur = humanizeDuration(Date.now() - start, { language: "zh_CN" });
console.log("下载完成, 耗时%s", dur);
process.exit(0);
}