node 實現廣場舞視頻下載與裁剪,幫我媽下載廣場舞視頻並剪輯

前言

我媽是廣場舞大媽,常常讓我下載廣場舞視頻,而且要裁剪出背面演示部分或者分解動做部分,那麼問題就來了。html

🤔我想,既然我要常常作這件事,可否寫個 node 來簡化一下呢?node

功能規劃

我媽平時主要是用 51廣場舞糖豆廣場舞 這兩個網站,並且糖豆廣場舞還有微信小程序。git

因此我這個工具必須可以下載這兩個網站的視頻,其次還要實現裁剪功能,設想下這個使用場景github

  1. 能夠在命令行輸入某個 51廣場舞 或 糖豆廣場舞 的連接就能實現下載,而且在下載時命令行顯示 下載中loading
  2. 視頻下載完成後,在命令行提示輸入 開始時間、結束時間,輸入完成後開始剪輯視頻

好,需求明確了,開始分析正則表達式

分析下 51廣場舞

隨便打開一個 51廣場舞 的視頻,發現頁面就有提供下載按鈕,咱們用開發者工具看一下,能夠看到按鈕的 href 就是視頻請求 URL,這就好辦了,這裏要注意下,這個 URL 不是咱們在命令行輸入的那個,咱們輸入的是頂部連接欄的 URL ,這個是咱們爬取的目標!npm

分析下 糖豆廣場舞

隨便打開一個 糖豆廣場舞 的視頻,打開開發者工具,發現這裏用的是 HTML5 的 video,這個屬性的 src 就是視頻的請求URL小程序

至此,一切看起來很是順利,咱們很容易就找到了視頻的 URL ,事實上在作糖豆的下載時還卡了一下,具體請往下看

實現

新建一個文件夾,而後 npm 或 yarn 初始化一下微信小程序

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

yarn init
複製代碼

新建 index.js,咱們就在這個文件寫代碼api

touch index.js
複製代碼

把用到的模塊安裝一下,具體文章下面有解說bash

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(),沒有 setEndTime(),只能用 setDuration() 傳秒數來設置你要裁剪的時長(秒)

可是我總不能輸入開始時間,而後再計算出到結束時間的秒數,再輸入這個秒數吧,因此這裏我仍是讓用戶輸入結束時間,我在代碼用 ffprobe 獲取到視頻總長度,計算出開始到結束的秒數,這裏用到了兩個時間轉換的工具方法

咱們在下載完成後,調用 cutVideo 方法,以下

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
          endTime = endTime || utils.secondsToHms(videoDuration) // 設置默認時間爲視頻結束時間
          const startSecond = utils.hmsToSeconds(startTime)
          const endSecond = utils.hmsToSeconds(endTime)
          const cutDuration = (videoDuration - startSecond) - (videoDuration - endSecond)

          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
          endTime = endTime || utils.secondsToHms(videoDuration)
          const startSecond = utils.hmsToSeconds(startTime)
          const endSecond = utils.hmsToSeconds(endTime)
          const cutDuration = (videoDuration - startSecond) - (videoDuration - endSecond)

          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… 而後輸入命令

在等待下載的過程當中,去看看背面演示的開始時間和結束時間,下完後輸入

而後等待剪切完成!

比起之前效率提高了很多!🎉

相關文章
相關標籤/搜索