👯‍♀️node 实现广场舞视频下载与裁剪,帮我妈下载与剪切广场舞视频

1,315 阅读4分钟

前言

我妈是广场舞大妈,经常让我下载广场舞视频,并且要裁剪出背面演示部分或者分解动作部分,那么问题来了。

🤔我想,既然我要经常做这件事,能否写个 node 来简化一下呢?

功能规划

我妈平时主要是用 51广场舞糖豆广场舞 这两个网站,而且糖豆广场舞还有微信小程序,使用很方便。

所以我这个工具必须能够支持下载这两个网站的视频,其次还要实现裁剪功能,设想下这个使用场景

  1. 在命令行输入某个 51广场舞 或 糖豆广场舞 的链接就能实现下载,下载时显示 loading
  2. 视频下载完成后,在命令行提示输入 开始时间、结束时间,输入完成后开始剪辑视频

需求明确了,开始分析

分析下 51广场舞

随便打开一个 51广场舞 的视频,发现页面就有提供下载按钮,用开发者工具看一下,可以看到按钮的 href 就是视频请求 URL,这就好办了,这里要注意下,这个 URL 不是我们在命令行输入的那个,我们输入的是顶部链接栏的 URL ,这个是我们爬取的目标,是视频的下载链接!

分析下 糖豆广场舞

随便打开一个 糖豆广场舞 的视频,打开开发者工具,发现这里用的是 HTML5 的 video,这个属性的 src 就是视频的请求URL

至此,一切看起来非常顺利,很容易就找到了视频的 URL ,事实上在做糖豆的下载时还卡了一下,具体请往下看

实现

新建一个文件夹,然后 npm 或 yarn 初始化一下

mkdir dance-video-downloader
cd dance-video-downloader

yarn init

新建 index.js,就在这个文件写代码

touch index.js

把用到的模块安装一下,关于这些模块文章下面有说明

yarn add superagent cheerio ora inquirer fluent-ffmpeg

爬取视频 URL

这里使用到两个爬虫的神器,superagentcheerio ,和一个命令行 loading 工具 ora

superagent 其实是一个 http 工具,可用于请求页面内容,cheerio 相当于 node 中的 jq

广场舞地址我们选择在命令行输入,比如 node index http://www.51gcw.com/v/26271.html,可以通过 process.argv[2] 获取到这个 URL

我们先请求到网页内容,然后通过 cheerio 操作 dom 的方式获取到视频的 URL,具体实现代码如下

const superagent = require('superagent')
const cheerio = require('cheerio')
const ora = require('ora')

  function run() {
    const scraping = ora('正在抓取网页...\n').start()

    superagent
      .get(process.argv[2])
      .end((err, res) => {
        if (err) {
          return console.log(err)
        }
        scraping.succeed('已成功抓取到网页\n')

        const downloadLink = getDownloadLink(res.text)
        console.log(downloadLink)
      })
  },
  
  function is51Gcw(url) {
    return url.indexOf('51gcw') > -1
  },

  function isTangDou(url) {
    return url.indexOf('tangdou') > -1
  },

  function getDownloadLink(html) {
    const $ = cheerio.load(html)
    let downloadLink
    if (this.is51Gcw(process.argv[2])) {
      downloadLink = $('.play_xz_mp4 a').eq(1).attr('href')
    } else if (process.argv[2]) {
      downloadLink = $('video').attr('src')
    }
    return downloadLink
  },

测试一下,首先是 51广场舞 的

node index http://www.51gcw.com/v/26271.html 可以看到视频的 URL 打印出来了

再试一下 糖豆广场舞 的,结果却打印出了 undefined

为什么会这样呢?我们打印获取到的网页分析下

  superagent
      .get(process.argv[2])
      .end((err, res) => {
        if (err) {
          return console.log(err)
        }
        scraping.succeed('已成功抓取到网页\n')

        // const downloadLink = getDownloadLink(res.text)
        console.log(res.text)
      })
  },

结果发现,糖豆广场舞的视频是使用插件做的,也即,一开始时,页面并没有 video 这个标签,所以 $('video').attr('src') 必定是获取不到的。

仔细看看这段 HTML内容,发现这个地址就藏在某个对象里,而这段内容其实也就是字符串,所以我决定使用正则表达式来取到这个 URL

改写下获取 URL 的方法

function getDownloadLink(html) {
    const $ = cheerio.load(html)
    let downloadLink
    if (this.is51Gcw(this.url)) {
      downloadLink = $('.play_xz_mp4 a').eq(1).attr('href')
    } else if (this.isTangDou(this.url)) {
      const match = /video:\s?'(https?\:\/\/\S+)'/.exec(html)
      downloadLink = match && match[1]
    }
    return downloadLink
},

ok,现在可以取到 URL 了

下载视频

superagent 其实就是一个 http 工具,所以直接用它下载即可

我们在取到 URL 后,传到 downloadVideo 进行下载,代码如下

  const fs = require('fs')
  const DOWNLOAD_PATH = 'gcw.mp4'
  function downloadVideo(downloadLink) {
    console.log(`${downloadLink}\n`)
    if (!downloadLink) {
      console.log('获取下载链接失败')
      return
    }
    const downloading = ora('正在下载视频...\n').start()

    const file = fs.createWriteStream(DOWNLOAD_PATH)
    file.on('close', () => {
      downloading.succeed('已成功下载视频\n')

      // this.cutVideo()
    })

    superagent
      .get(downloadLink)
      .pipe(file)
}

测试一下,成功下载到视频

裁剪视频

视频下载完成后,我们要实现裁剪视频,这里用到两个工具,

一个是 Inquirer 命令行交互工具,这里用于提问开始时间和结束时间

而裁剪视频我使用的是 node-fluent-ffmpeg,这个其实是用 node 调用 ffmpeg,所以电脑要安装 ffmpeg,这个也是我平常使用的工具,安利下,功能很强大,可以使用命令进行视频格式转换,图片转视频,切割视频等等,很适合程序猿使用😎

查阅下 node-fluent-ffmpeg 文档,发现它只提供了设置开始时间的方法 setStartTime(),没有提供设置结束时间的方法,只能用 setDuration() 传秒数来设置你要裁剪的时长(秒)

举个例子来说,如果我要裁剪 00:01:03 到 00:02:05 这部分,那我要取得开始时间 00:01:03 然后用 00:02:05 - 00:01:03 = 62(秒) 取得裁剪时长,计算裁剪时长这么不方便的操作肯定不能让用户来做,所以这里我还是让用户输入开始时间和结束时间,然后在代码中计算时长

相关代码如下

const ffmpeg = require('fluent-ffmpeg')
const inquirer = require('inquirer');

/**
* HH:mm:ss 转换成秒数
* @param {string} hms 时间,格式为HH:mm:ss
*/
function hmsToSeconds(hms) {
    const hmsArr = hms.split(':')
    return (+hmsArr[0]) * 60 * 60 + (+hmsArr[1]) * 60 + (+hmsArr[2])
},

/**
* 秒数转换成 HH:mm:ss
* @param {number}} seconds 秒数
*/
function secondsToHms(seconds) {
    const date = new Date(null)
    date.setSeconds(seconds)
    return date.toISOString().substr(11, 8)
}
  
const CUT_RESULT_PATH = 'cut_gcw.mp4'
function cutVideo() {
    inquirer.prompt([
      {
        type: 'confirm',
        name: 'needCut',
        message: '是否需要裁剪?',
        default: true
      },
      {
        type: 'input',
        name: 'startTime',
        message: '请输入开始时间, 默认为 00:00:00 (HH:mm:ss)',
        default: '00:00:00',
        when: ({ needCut }) => needCut
      },
      {
        type: 'input',
        name: 'endTime',
        message: '请输入结束时间, 默认为视频结束时间 (HH:mm:ss)',
        when: ({ needCut }) => needCut
      }
    ]).then(({ needCut, startTime, endTime }) => {
      if (!needCut) {
        process.exit()
      }

      ffmpeg
        .ffprobe(DOWNLOAD_PATH, (err, metadata) => {
          const videoDuration = metadata.format.duration
          const startSecond = utils.hmsToSeconds(startTime)
          const endSecond = endTime ? utils.hmsToSeconds(endTime) : videoDuration
          const cutDuration = endSecond - startSecond

          console.log(`\n开始时间:${startTime}`)
          console.log(`结束时间:${endTime}`)
          console.log(`开始时间(s):${startSecond}`)
          console.log(`结束时间(s):${endSecond}`)
          console.log(`裁剪后时长(s):${cutDuration}\n`)

          const cutting = ora('正在裁剪视频...\n').start()
          ffmpeg(DOWNLOAD_PATH)
            .setStartTime(startTime)
            .setDuration(cutDuration)
            .saveToFile(CUT_RESULT_PATH)
            .on('end', function () {
              cutting.succeed(`已成功裁剪视频,输出为 ${CUT_RESULT_PATH} `)
            })
        })

    })
  }

开发完成

至此,开发完成了,可以用单体模式封装一下,使得代码优雅一点😂,完整的代码如下,也可在github 上查看

const fs = require('fs')
const superagent = require('superagent')
const cheerio = require('cheerio')
const ora = require('ora')
const inquirer = require('inquirer');
const ffmpeg = require('fluent-ffmpeg')

const utils = {
  /**
   * HH:mm:ss 转换成秒数
   * @param {string} hms 时间,格式为HH:mm:ss
   */
  hmsToSeconds(hms) {
    const hmsArr = hms.split(':')

    return (+hmsArr[0]) * 60 * 60 + (+hmsArr[1]) * 60 + (+hmsArr[2])
  },

  /**
   * 秒数转换成 HH:mm:ss
   * @param {number}} seconds 秒数
   */
  secondsToHms(seconds) {
    const date = new Date(null)
    date.setSeconds(seconds)
    return date.toISOString().substr(11, 8)
  }
}

const downloader = {
  url: process.argv[2],
  VIDEO_URL_REG: /video:\s?'(https?\:\/\/\S+)'/,
  DOWNLOAD_PATH: 'gcw.mp4',
  CUT_RESULT_PATH: 'gcw_cut.mp4',

  run() {
    if (!this.url) {
      console.log('请输入 51广场舞 或 糖豆广场舞 地址')
      return
    }

    const scraping = ora('正在抓取网页...\n').start()

    superagent
      .get(this.url)
      .end((err, res) => {
        if (err) {
          return console.log(err)
        }
        scraping.succeed('已成功抓取到网页\n')

        const downloadLink = this.getDownloadLink(res.text)
        this.downloadVideo(downloadLink)
      })
  },

  is51Gcw(url) {
    return url.indexOf('51gcw') > -1
  },

  isTangDou(url) {
    return url.indexOf('tangdou') > -1
  },

  getDownloadLink(html) {
    const $ = cheerio.load(html)
    let downloadLink
    if (this.is51Gcw(this.url)) {
      downloadLink = $('.play_xz_mp4 a').eq(1).attr('href')
    } else if (this.isTangDou(this.url)) {
      const match = this.VIDEO_URL_REG.exec(html)
      downloadLink = match && match[1]
    }
    return downloadLink
  },

  downloadVideo(downloadLink) {
    console.log(`${downloadLink}\n`)
    if (!downloadLink) {
      console.log('获取下载链接失败')
      return
    }
    const downloading = ora('正在下载视频...\n').start()

    const file = fs.createWriteStream(this.DOWNLOAD_PATH)
    file.on('close', () => {
      downloading.succeed('已成功下载视频\n')

      this.cutVideo()
    })

    superagent
      .get(downloadLink)
      .pipe(file)
  },

  cutVideo() {
    inquirer.prompt([
      {
        type: 'confirm',
        name: 'needCut',
        message: '是否需要裁剪?',
        default: true
      },
      {
        type: 'input',
        name: 'startTime',
        message: '请输入开始时间, 默认为 00:00:00 (HH:mm:ss)',
        default: '00:00:00',
        when: ({ needCut }) => needCut
      },
      {
        type: 'input',
        name: 'endTime',
        message: '请输入结束时间, 默认为视频结束时间 (HH:mm:ss)',
        when: ({ needCut }) => needCut
      }
    ]).then(({ needCut, startTime, endTime }) => {
      if (!needCut) {
        process.exit()
      }

      ffmpeg
        .ffprobe(this.DOWNLOAD_PATH, (err, metadata) => {
          const videoDuration = metadata.format.duration
          const startSecond = utils.hmsToSeconds(startTime)
          const endSecond = endTime ? utils.hmsToSeconds(endTime) : videoDuration
          const cutDuration = endSecond - startSecond

          console.log(`\n开始时间:${startTime}`)
          console.log(`结束时间:${endTime}`)
          console.log(`开始时间(s):${startSecond}`)
          console.log(`结束时间(s):${endSecond}`)
          console.log(`裁剪后时长(s):${cutDuration}\n`)

          const cutting = ora('正在裁剪视频...\n').start()
          ffmpeg(this.DOWNLOAD_PATH)
            .setStartTime(startTime)
            .setDuration(cutDuration)
            .saveToFile(this.CUT_RESULT_PATH)
            .on('end', () => {
              cutting.succeed(`已成功裁剪视频,输出为 ${this.CUT_RESULT_PATH} `)
            })
        })

    })
  }
}

downloader.run()

试用一下

收到任务

杨丽萍广场舞 醉人的花香

只要背面演示部分

安排

找到这个广场舞:www.51gcw.com/v/35697.htm… 然后输入命令

在等待下载的过程中,去看看背面演示的开始时间和结束时间,下完后输入

然后等待剪切完成!

比起以前效率提升了不少!🎉

后续计划

后面可将下载与裁剪分离,做成两个命令,这样也可以单独使用裁剪功能,可以剪其他视频