前言

最近发现一些比较有意思的视频,于是便想着将视频下载下来,虽然B站客户端有提供缓存,但是会在视频中加入一些不必要的元素

因此就敲了一个爬取工具

Dev

大概的流程是这样的

  • 通过 API 拿到视频源地址
  • 下载 flv 到服务器 (需要处理防盗链
  • 通过 ffmpeg 将 flv 转成 mp4
  • 再将 mp4 写入 response 进行回传

ffmpeg 需要提前安装好 https://ffmpeg.org/

保存视频时的命名使用到了B站的标题,因此可能会存在名字包含特殊字符的情况,比如说 '/' 会被识别成目录,对于这种情况则需要将其转换成其它字符

且不支持空格(空格大概是因为 ffmpeg 的缘故,因为win下正常,win和linux下的 ffmpeg 貌似也有所区别 💦

Go实现

package main

import (
	"github.com/W1llyu/ourjson"
	"github.com/kataras/iris/v12"
	log "github.com/sirupsen/logrus"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

// CidApi 通过此API获取cid
var CidApi = "https://api.bilibili.com/x/player/pagelist"

// VideoUrlApi 通过此API获取视频Url,格式为flv
var VideoUrlApi = "https://api.bilibili.com/x/player/playurl"

// VideoInfoApi 视频信息
var VideoInfoApi = "https://api.bilibili.com/x/web-interface/view"

// RootPath 项目路径
var RootPath, _ = filepath.Abs(path.Dir(os.Args[0]))

// BiliVideoStorageOriginal 存放原始视频 - .flv
var BiliVideoStorageOriginal = RootPath + "/BiliVideoStorageOriginal/"

// BiliVideoStorage 存放转格式后的视频 - .mp4
var BiliVideoStorage = RootPath + "/BiliVideoStorage/"

type VideoInfo struct {
	Title string
	Auth  string
	Uid   string
}

func BiliVideo(ctx iris.Context) {
	defer ErrorHandle(ctx)
	//pwd := ctx.URLParam("pwd")
	//if pwd != "147526" {
	//	panic("密码有误喵!")
	//}

	// BV号
	bvCode := ctx.URLParam("bv")
	if bvCode == "" {
		panic("BV号有误喵!")
	}

	log.Warning("开始下载喵 - " + bvCode)
	cid := GetCid(bvCode)
	if cid == "" {
		panic("BV号有误喵!")
	}
	url := GetVideoUrl(bvCode, cid)
	if url == "" {
		panic("BV号下无视频喵!")
	}

	videoInfo := GetVideoInfo(bvCode)
	log.Info("【视频标题】 " + videoInfo.Title)
	log.Info("【视频作者】 " + videoInfo.Auth)
	log.Info("【作者UID】 " + videoInfo.Uid)

	client := &http.Client{}
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Add("Referer", "https://www.bilibili.com/")
	resp, _ := client.Do(req)
	body, _ := ioutil.ReadAll(resp.Body)

	timeStr := strconv.FormatInt(time.Now().Unix(), 10)
	// 创建flv文件
	fileName := videoInfo.Title + "【" + videoInfo.Auth + "】" + "【" + videoInfo.Uid + "】" + "【" + timeStr + "】"
	originalPath := BiliVideoStorageOriginal + fileName + ".flv"
	original, _ := os.Create(originalPath)
	err := original.Close()
	if err != nil {
		return
	}

	// 写入
	originalFile, _ := os.OpenFile(originalPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
	defer func(originalFile *os.File) {
		err := originalFile.Close()
		if err != nil {

		}
	}(originalFile)
	_, err = originalFile.Write(body)
	if err != nil {
		return
	}

	filePath := BiliVideoStorage + fileName + ".mp4"
	// 使用ffmpeg转换格式
	// win
	cmd := exec.Command("ffmpeg", "-i", originalPath, filePath)
	// linux
	// cmd := exec.Command("/bin/sh", "-c", "ffmpeg -i " +originalPath+ " -c:v copy -c:a copy "+filePath)
	//执行
	err = cmd.Run()
	if err != nil {
		return
	}

	// 返回.mp4
	file, err := os.Open(filePath)
	defer func(file *os.File) {
		err := file.Close()
		if err != nil {

		}
	}(file)
	fileContent, _ := ioutil.ReadAll(file)
	ctx.ContentType("video/mp4")
	ctx.Header("Content-Disposition", "attachment; filename="+videoInfo.Title+".mp4")
	_, err = ctx.Write(fileContent)
	if err != nil {
		return
	}

	log.Info("下载完成喵!")
	log.Warning("============= ✨ ============")
}

func GetCid(code string) string {
	api := CidApi + "?bvid=" + code + "&jsonp=jsonp"
	res, _ := http.Get(api)
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {

		}
	}(res.Body)
	body, _ := ioutil.ReadAll(res.Body)
	jsonStr := string(body)
	jsonObject, _ := ourjson.ParseObject(jsonStr)

	data := jsonObject.GetJsonArray("data")
	videoData := data.GetJsonObject(0)
	cid, _ := videoData.GetInt("cid")
	return strconv.Itoa(cid)
}

func GetVideoInfo(code string) VideoInfo {
	var info VideoInfo
	api := VideoInfoApi + "?bvid=" + code
	res, _ := http.Get(api)
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {

		}
	}(res.Body)
	body, _ := ioutil.ReadAll(res.Body)
	jsonStr := string(body)
	jsonObject, _ := ourjson.ParseObject(jsonStr)

	data := jsonObject.GetJsonObject("data")
	info.Title, _ = data.GetString("title")
	if strings.Contains(info.Title, "\\") || strings.Contains(info.Title, "/") {
		info.Title = strings.ReplaceAll(info.Title, "\\", "💢")
		info.Title = strings.ReplaceAll(info.Title, "/", "💢")
	}
	owner := data.GetJsonObject("owner")
	info.Auth, _ = owner.GetString("name")
	uid, _ := owner.GetInt("mid")
	info.Uid = strconv.Itoa(uid)
	return info
}

func GetVideoUrl(bvCode, cid string) string {
	api := VideoUrlApi + "?avid=&cid=" + cid + "&bvid=" + bvCode + "&qn=120&type=&otype=json"
	res, _ := http.Get(api)
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {

		}
	}(res.Body)
	body, _ := ioutil.ReadAll(res.Body)
	jsonStr := string(body)
	jsonObject, _ := ourjson.ParseObject(jsonStr)
	data := jsonObject.GetJsonObject("data")
	durl := data.GetJsonArray("durl")
	durl0 := durl.GetJsonObject(0)
	url, _ := durl0.GetString("url")
	return url
}

Ex - ploooosion!