使用Node下载Acfun视频--解析A站视频API、aria2c多线程下载和ffmpeg合并
APACHE-2.0 License
NodeAcfunAAPIaria2cm3u8ffmpeg
aria2c
debaria2``ffmpeg
sudo apt install aria2 ffmpeg
npmacfun-video-cli
czzonet/acfun-video-cli
yarn global add acfun-video-cli
url
acfun-video-cli https://www.acfun.cn/v/ac4621380
...
------
Parse url
ok
------
Parse m3u8
1080p: https://tx-safety-video.acfun.cn/mediacloud/acfun/acfun_video/hls/3HlXWWGOvsJ3D9Vhsn2QzbWPzp9OwtD40Yk9bk8v9t7Khv6leh44hGnw-Qqx9_KP.m3u8?pkey=AALA88Sf3Prmclff8_Ki5E0wlxj0Gam0_NN5bLvhUbCS2_88ypokmdH2Kf1wvzojL4pZJVjDn2m_iRkcrw-4hhRYEn5x01YOyfxYlJ9oOmeMtw4QA_UMZFq5MHQMp7BQZOkIFPPc7oBI0ABtWSSihiKp9WkKUklJibYCStx4Ego_u8MlOMaHONKAAivGjrCsrZap0sO3nuqV5-pThp_LE_WyXImXmfUSFbBkT3vLCWujKw&safety_id=AALdip3SIjwDZfuuv1y8iHA4
ok
------
Download ts videos
09/09 15:53:10 [NOTICE] Downloading 29 item(s)
...
Status Legend:
(OK):download completed.
ok
------
Parse url
ffmpeg version 4.2.4-1ubuntu0.1 Copyright (c) 2000-2020 the FFmpeg developers
...
Stream mapping:
Stream #0:0 -> #0:0 (copy)
Stream #0:1 -> #0:1 (copy)
Press [q] to stop, [?] for help
frame= 3000 fps=0.0 q=-1.0 size= 28160kB time=00:02:01.35 bitrate=1901.0kbits/s speed= 238x frame= 3520 fps=0.0 q=-1.0 Lsize= 33639kB time=00:02:22.36 bitrate=1935.7kbits/s speed= 243x
video:31340kB audio:2223kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.227712%
ok
tsindex.ts
index.ts
async function main() {}
main().then();
nodeNode Api
yarn add -D @types/node
-D
AAPIm3u8url
https://www.acfun.cn/v/ac4621380?quickViewId=videoInfo_new&ajaxpipe=1
https://www.acfun.cn/v/ac4621380
?quickViewId=videoInfo_new&ajaxpipe=1
htmljsonvideoInfo
{"html":"
<script class=\"videoInfo\">\n window.pageInfo = window.videoInfo ={\"currentVideoId\":6291551,
,\"priority\":0}
</script>
<script class=\"videoResource\">\n window.videoResource ={}</script>
<div class='left-column'>\n\n
\
json\\"``\"``"
videoInfo
videoInfo
currentVideoInfo
ksPlayJson
jsonksPlay
ksPlay
ksPlay
adaptationSet[0]
representation
url
m3u8qualityType
1080p720papi.ts
import * as https from "https";
export async function getUrlData(url: string): Promise<string> {
return new Promise((resolve, reject) => {
https
.get(url, (res) => {
if (res === null) {
reject(new Error("[E] No Response."));
}
const { statusCode } = res;
const contentType = res.headers["content-type"];
const allowTypes = [
"application/json; charset=utf-8",
"application/octet-stream",
"application/vnd.apple.mpegurl",
];
let error;
if (statusCode !== 200) {
error = new Error("[E] Response code: " + statusCode);
} else if (
!(contentType !== undefined && allowTypes.includes(contentType))
) {
error = new Error(
"[E] Invalid content-type.\n" +
`Expected one of ${allowTypes} but received ${contentType}`
);
}
if (error) {
res.resume();
reject(error);
}
res.setEncoding("utf8");
let rawData = "";
res.on("data", (chunk) => (rawData += chunk));
res.on("close", () => {
resolve(rawData);
});
})
.on("error", (error) => {
reject(new Error("[E] Https.Get error: " + error));
});
});
}
Node
https
res.resume()
parser.ts
import { getUrlData } from "./api";
export async function parseUrl(videoUrlAddress: string) {
// eg https://www.acfun.cn/v/ac4621380?quickViewId=videoInfo_new&ajaxpipe=1
const urlSuffix = "?quickViewId=videoInfo_new&ajaxpipe=1";
const url = videoUrlAddress + urlSuffix;
const raw: string = await getUrlData(url);
// Split
const strsRemoveHeader = raw.split("window.pageInfo = window.videoInfo =");
const strsRemoveTail = strsRemoveHeader[1].split("</script>");
const strJson = strsRemoveTail[0];
const strJsonEscaped = escapeSpecialChars(strJson);
/** Object videoInfo */
const videoInfo = JSON.parse(strJsonEscaped);
const ksPlayJson = videoInfo.currentVideoInfo.ksPlayJson;
/** Object ksPlay */
const ksPlay = JSON.parse(ksPlayJson);
const representations: any[] = ksPlay.adaptationSet[0].representation;
const urlM3u8s: string[] = representations.map((d) => d.url);
return urlM3u8s;
}
/**
* JSON \\" -> \" ->"
* @param str
*/
function escapeSpecialChars(str: string) {
return str.replace(/\\\\"/g, '\\"').replace(/\\"/g, '"');
}
getUrlData
videoInfoJson
videoInfo
videoInfo``ksPlayJson``ksPlay
ksPlay``representations
index.ts``main
const url = `https://www.acfun.cn/v/ac4621380`;
console.log("\n------\nParse url");
const m3u8Urls = await parseUrl(url);
console.log("ok");
parseUrl
m3uu8m3u81080pm3u8aria2c
parser.ts
export async function parseM3u8(m3u8Url: string) {
const m3u8File = await getUrlData(m3u8Url);
/** ts */
const rawPieces = m3u8File.split(/\n#EXTINF:.{8},\n/);
/** */
const m3u8RelativeLinks = rawPieces.slice(1);
/** */
const patchedTail = m3u8RelativeLinks[m3u8RelativeLinks.length - 1].split(
"\n"
)[0];
m3u8RelativeLinks[m3u8RelativeLinks.length - 1] = patchedTail;
/** m3u8Url */
const m3u8Prefix = m3u8Url.split("/").slice(0, -1).join("/");
const m3u8FullUrls = m3u8RelativeLinks.map((d) => m3u8Prefix + d);
/** aria2curlurl(?url) */
const tsNames = m3u8RelativeLinks.map((d) => d.split("?")[0]);
/** */
let outputFolderName = tsNames[0].slice(0, -9);
/** mp4 */
const outputFileName = outputFolderName + ".mp4";
return {
m3u8FullUrls,
tsNames,
outputFolderName,
outputFileName,
};
}
m3u8Url
aria2c
urlurl(?url)aria2c
sudo apt install aria2
runShell.ts
import { spawn } from "child_process";
/**
* shell
* @param command shell
* @param args shell
* @param options shell
* @description
```ts
readUpdateOutputFromShell("sar", ["-n", "DEV", "1"])
*/ export const runShell = async ( command: string, args: readonly string[], options: ShellOption ) => new Promise((resolve, reject) => { const runpProcess = spawn(command, args, { stdio: "inherit", cwd: options.cwd ? options.cwd : process.cwd(), env: process.env, detached: true, });
/** */
runpProcess.on("close", (code) => {
resolve();
});
});
type ShellOption = { cwd?: string; };
Node
- `spawn`
- `stdio: "inherit"`stdiostderr
- `cwd: options.cwd`
`video.ts`
```ts
import * as fs from "fs";
import * as path from "path";
import { runShell } from "./runShell";
export async function downloadM3u8Videos(
m3u8FullUrls: string[],
outputFolderName: string
) {
//
if (outputFolderName == "") {
throw new Error("[E] Download folder name is empty.");
}
/** _ */
while (fs.existsSync(path.resolve(process.cwd(), outputFolderName))) {
outputFolderName += "_";
if (outputFolderName.length > 100) {
throw new Error(
"[E] Download folder exists and try to rename too many times."
);
}
}
/** */
const outPath = path.resolve(process.cwd(), outputFolderName);
fs.mkdirSync(outPath);
/** */
fs.writeFileSync(path.resolve(outPath, "urls.txt"), m3u8FullUrls.join("\n"));
/** aria2c */
await runShell("aria2c", ["-i", "./urls.txt"], {
cwd: path.resolve(outPath),
});
}
urls.txt
aria2c``urls.txt
-j5index.ts``main
console.log("\n------\nParse m3u8");
const m3u8Url1080p = m3u8Urls[0];
const info = await parseM3u8(m3u8Url1080p);
console.log("ok");
console.log("\n------\nDownload ts videos");
const { m3u8FullUrls, tsNames, outputFolderName, outputFileName } = info;
await downloadM3u8Videos(m3u8FullUrls, outputFolderName);
console.log("ok");
ffmpeg
sudo apt install ffmpeg
ffmpeg
wikiConcatenate FFmpeg
ffmpeg -f concat -safe 0 -i ./files.txt -c copy outputFileName
ffmpeg
ffmpeg-f concat
concat-safe 0
-i ./files.txt
-c copy
outputFileName
files.txt
file /path/xxx1
file /path/xxx2
...
video.ts
export async function mergeVideo(
tsNames: string[],
outputFileName: string,
outputFolderName: string
) {
const outPath = path.resolve(process.cwd(), outputFolderName);
/** file path */
const concatStrs = tsNames.map((d) => `file '${outPath}/${d}'`);
/** */
fs.writeFileSync(path.resolve(outPath, "files.txt"), concatStrs.join("\n"));
debugger;
/** ffmpeg */
await runShell(
"ffmpeg",
[
"-f",
"concat",
"-safe",
"0",
"-i",
"./files.txt",
"-c",
"copy",
outputFileName,
],
{ cwd: path.resolve(outPath) }
);
}
file path
ffmpeg
index.ts``main
console.log("\n------\nMerge video");
await mergeVideo(tsNames, outputFileName, outputFolderName);
console.log("ok");
index.ts
const url = process.argv[2];
console.log("Your input: ", url);
if (typeof url !== "string") {
console.log("[E] Url input required.");
return;
}
if (url.match(/^https:\/\/www\.acfun\.cn\/v\/ac\d+$/) === null) {
console.log(
"[E] Url input invalid.Valid input example: https://www.acfun.cn/v/ac4621380"
);
return;
}
index.ts
import { parseUrl, parseM3u8 } from "./parser";
import { downloadM3u8Videos, mergeVideo } from "./video";
async function main() {
const url = process.argv[2];
console.log("Your input: ", url);
if (typeof url !== "string") {
console.log("[E] Url input required.");
return;
}
if (url.match(/^https:\/\/www\.acfun\.cn\/v\/ac\d+$/) === null) {
console.log(
"[E] Url input invalid.Valid input example: https://www.acfun.cn/v/ac4621380"
);
return;
}
console.log("\n------\nParse url");
const m3u8Urls = await parseUrl(url);
console.log("ok");
console.log("\n------\nParse m3u8");
const m3u8Url1080p = m3u8Urls[0];
console.log("[1080p] ", m3u8Url1080p);
const info = await parseM3u8(m3u8Url1080p);
console.log("ok");
console.log("\n------\nDownload ts videos");
const { m3u8FullUrls, tsNames, outputFolderName, outputFileName } = info;
await downloadM3u8Videos(m3u8FullUrls, outputFolderName);
console.log("ok");
console.log("\n------\nMerge video");
await mergeVideo(tsNames, outputFileName, outputFolderName);
console.log("ok");
}
main().then();
AAPIaria2c
m3u8ffmpeg
NodeAcfun
ffmpeg
m3u8
ffmpeg -i 'https://xxx.m3u8' -c copy output.mp4
This software is distributed under the Apache-2.0 license.
In particular, please be aware that
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Translated to human words:
In case your use of the software forms the basis of copyright infringement, or you use the software for any other illegal purposes, the authors cannot take any responsibility for you.
We only ship the code here, and how you are going to use it is left to your own discretion.