acfun-video-cli

使用Node下载Acfun视频--解析A站视频API、aria2c多线程下载和ffmpeg合并

APACHE-2.0 License

Downloads
7
Stars
6
Committers
1

v3.0.0

  • win ok
  • webpack

v2.0.0

  • winmaclinuxaria2cffmpeg200MB
  • rxjs

NodeAcfun--AAPIaria2cffmpeg

NodeAcfunAAPIaria2cm3u8ffmpeg

aria2cdebaria2``ffmpeg

sudo apt install aria2 ffmpeg

npmacfun-video-cliczzonet/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

AAPI

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

  • scriptjsonvideoInfo
  • json\json\\"``\"``"

videoInfo

  • videoInfo
    • currentVideoInfo
      • ksPlayJsonjsonksPlay

ksPlay

  • ksPlay
    • adaptationSet[0]
      • representation
        • urlm3u8
        • qualityType1080p720p

api.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
  • url
  • Promise
  • get
  • res.resume()
  • utf8
  • Get

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
  • url
  • url
  • videoInfoJson
  • Json
  • JsonvideoInfo
  • 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");
  • parseUrlm3uu8

aria2cm3u8

m3u81080pm3u8aria2c

m3u8

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
  • m3u8
  • ts
  • m3u8Url
  • aria2curlurl(?url)
  • mp4

aria2c

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-j5

index.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");
  • 1080p
  • ts

ffmpeg

ffmpeg

sudo apt install ffmpeg

ffmpegwikiConcatenate FFmpeg

ffmpeg -f concat -safe 0 -i ./files.txt -c copy outputFileName
  • ffmpegffmpeg
  • -f concatconcat
  • -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) }
  );
}
  • ts
  • file path
  • ffmpeg

index.ts``main

  console.log("\n------\nMerge video");
  await mergeVideo(tsNames, outputFileName, outputFolderName);
  console.log("ok");

url

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;
  }
  • url

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();

AAPIaria2cm3u8ffmpegNodeAcfun

ffmpegm3u8

ffmpeg -i 'https://xxx.m3u8' -c copy output.mp4

~czzonet/acfun-video-cli

  1. ffmpeg Documentation
  2. FFmpeg Formats Documentation
  3. Concatenate FFmpeg
  4. aria2c(1) aria2 1.35.0 documentation
  5. HTTP | Node.js v14.9.0 Documentation

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.

Package Rankings
Top 22.16% on Npmjs.org