init: git clone https://github.com/iuroc/bilidown.git
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[build]
|
||||
exclude_dir = ["download", "bin"]
|
||||
include_ext = ["go"]
|
||||
@@ -0,0 +1,71 @@
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
# you may remove this if you don't need go generate
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- id: bilidown_windows
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- >-
|
||||
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{ end }}
|
||||
{{- if eq .Arch "386" }}CC=i686-w64-mingw32-gcc{{ end }}
|
||||
goos:
|
||||
- windows
|
||||
ldflags:
|
||||
- -s -w -H windowsgui
|
||||
- id: bilidown_darwin
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=o64-clang
|
||||
goos:
|
||||
- darwin
|
||||
ldflags:
|
||||
- -s -w
|
||||
- id: bilidown_linux
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- >-
|
||||
{{- if eq .Arch "arm64" }}CC=aarch64-linux-gnu-gcc{{ end }}
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags:
|
||||
- -s -w
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- static/**
|
||||
- >-
|
||||
{{- if eq .Os "windows" }}bin/ffmpeg.exe{{- end }}
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
@@ -0,0 +1,146 @@
|
||||
package bilibili_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"bilidown/bilibili"
|
||||
"bilidown/util"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const TEST_SESSDATA = ""
|
||||
const TEST_BVID = "BV1KX4y1V7sA" // 普通合集
|
||||
const TEST_CID = 305542578 // 普通分集
|
||||
const TEST_BVID_HDR = "BV1rp4y1e745" // HDR 合集
|
||||
const TEST_CID_HDR = 244954665 // HDR 分集
|
||||
const TEST_EPID = 835909 // 剧集
|
||||
const TEST_SSID = 48744 // 番剧
|
||||
|
||||
func TestBiliClient(t *testing.T) {
|
||||
client := bilibili.BiliClient{SESSDATA: TEST_SESSDATA}
|
||||
check, err := client.CheckLogin()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if check {
|
||||
fmt.Println("SESSDATA 有效")
|
||||
} else {
|
||||
fmt.Println("SESSDATA 无效")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewQRInfo(t *testing.T) {
|
||||
client := bilibili.BiliClient{}
|
||||
info, err := client.NewQRInfo()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fmt.Printf("二维码信息:%+v", info)
|
||||
}
|
||||
|
||||
func TestGetQRStatus(t *testing.T) {
|
||||
client := bilibili.BiliClient{}
|
||||
qrInfo, err := client.NewQRInfo()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
qr, err := qrcode.New(qrInfo.URL, qrcode.Low)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fmt.Println(qr.ToSmallString(false))
|
||||
|
||||
for {
|
||||
qrStatus, sessdata, err := client.GetQRStatus(qrInfo.QrcodeKey)
|
||||
fmt.Println(qrStatus.Message)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if qrStatus.Code == bilibili.QR_SUCCESS {
|
||||
fmt.Println("登录成功")
|
||||
fmt.Println(sessdata)
|
||||
break
|
||||
} else if qrStatus.Code == bilibili.QR_EXPIRES {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBVInfo(t *testing.T) {
|
||||
client := bilibili.BiliClient{SESSDATA: TEST_SESSDATA}
|
||||
videoInfo, err := client.GetVideoInfo(TEST_BVID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fmt.Printf("%+v", videoInfo)
|
||||
}
|
||||
|
||||
func TestGetSeasonInfo(t *testing.T) {
|
||||
client := bilibili.BiliClient{SESSDATA: TEST_SESSDATA}
|
||||
seasonInfo, err := client.GetSeasonInfo(TEST_EPID, TEST_SSID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fmt.Printf("%+v", seasonInfo)
|
||||
}
|
||||
|
||||
func TestGetPlayInfo(t *testing.T) {
|
||||
client := bilibili.BiliClient{SESSDATA: TEST_SESSDATA}
|
||||
playInfo, err := client.GetPlayInfo(TEST_BVID_HDR, TEST_CID_HDR)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
a, _ := json.Marshal(playInfo.AcceptQuality)
|
||||
b, _ := json.Marshal(playInfo.AcceptDescription)
|
||||
c, _ := json.Marshal(playInfo.SupportFormats)
|
||||
|
||||
fmt.Println(string(a))
|
||||
fmt.Println(string(b))
|
||||
fmt.Println(string(c))
|
||||
}
|
||||
|
||||
func TestSaveSessdata(t *testing.T) {
|
||||
db := util.MustGetDB("../data.db")
|
||||
defer db.Close()
|
||||
err := bilibili.SaveSessdata(db, TEST_SESSDATA)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
fmt.Println("SESSDATA 保存成功")
|
||||
}
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if sessdata != TEST_SESSDATA {
|
||||
t.Error("SESSDATA 读写不一致")
|
||||
} else {
|
||||
fmt.Println("SESSDATA 读取成功")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSessdata(t *testing.T) {
|
||||
db := util.MustGetDB("../data.db")
|
||||
defer db.Close()
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fmt.Println(sessdata)
|
||||
}
|
||||
|
||||
func TestGetPopularVideos(t *testing.T) {
|
||||
client := bilibili.BiliClient{SESSDATA: TEST_SESSDATA}
|
||||
videos, err := client.GetPopularVideos()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fmt.Printf("%+v", videos)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"bilidown/util"
|
||||
)
|
||||
|
||||
type BiliClient struct {
|
||||
SESSDATA string
|
||||
}
|
||||
|
||||
// SimpleGET 简单的 GET 请求
|
||||
func (client *BiliClient) SimpleGET(_url string, params map[string]string) (*http.Response, error) {
|
||||
values := url.Values{}
|
||||
for k, v := range params {
|
||||
values.Set(k, v)
|
||||
}
|
||||
_client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(nil),
|
||||
},
|
||||
}
|
||||
request, err := http.NewRequest("GET", _url+"?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header = client.MakeHeader()
|
||||
return _client.Do(request)
|
||||
}
|
||||
|
||||
// MakeHeader 生成请求头
|
||||
func (client *BiliClient) MakeHeader() http.Header {
|
||||
header := http.Header{}
|
||||
header.Set("Cookie", "SESSDATA="+client.SESSDATA)
|
||||
header.Set("User-Agent", "Mozilla/5.0")
|
||||
header.Set("Referer", "https://www.bilibili.com")
|
||||
return header
|
||||
}
|
||||
|
||||
// CheckLogin 检查是否已经登录
|
||||
func (client *BiliClient) CheckLogin() (bool, error) {
|
||||
response, err := client.SimpleGET("https://api.bilibili.com/x/space/myinfo", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body := BaseResV2{}
|
||||
err = json.NewDecoder(response.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return false, errors.New(body.Message)
|
||||
}
|
||||
return body.Success(), nil
|
||||
}
|
||||
|
||||
// NewQRInfo 获取登录二维码信息
|
||||
func (client *BiliClient) NewQRInfo() (*QRInfo, error) {
|
||||
response, err := client.SimpleGET("https://passport.bilibili.com/x/passport-login/web/qrcode/generate", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body := BaseResV2{}
|
||||
err = json.NewDecoder(response.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return nil, errors.New(body.Message)
|
||||
}
|
||||
qrInfo := QRInfo{}
|
||||
err = json.Unmarshal(body.Data, &qrInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &qrInfo, nil
|
||||
}
|
||||
|
||||
func (client *BiliClient) getWbiKeyRemote() (wbiKey string, err error) {
|
||||
if client.SESSDATA == "" {
|
||||
return "", errors.New("SESSDATA 不能为空")
|
||||
}
|
||||
response, err := client.SimpleGET("https://api.bilibili.com/x/web-interface/nav", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body := BaseResV2{}
|
||||
if err = json.NewDecoder(response.Body).Decode(&body); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var data struct {
|
||||
WbiImg struct {
|
||||
ImgURL string `json:"img_url"`
|
||||
SubURL string `json:"sub_url"`
|
||||
} `json:"wbi_img"`
|
||||
}
|
||||
if err = json.Unmarshal(body.Data, &data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
match := regexp.MustCompile(`/bfs/wbi/([a-z0-9]+)\.`)
|
||||
imgKey := match.FindStringSubmatch(data.WbiImg.ImgURL)[1]
|
||||
subKey := match.FindStringSubmatch(data.WbiImg.SubURL)[1]
|
||||
if imgKey == "" || subKey == "" {
|
||||
return "", errors.New("regexp.MustCompile(`/bfs/wbi/([a-z0-9])\\.`)")
|
||||
}
|
||||
return imgKey + subKey, nil
|
||||
}
|
||||
|
||||
// GetQRStatus 获取二维码状态
|
||||
func (client *BiliClient) GetQRStatus(qrKey string) (qrStatus *QRStatus, sessdata string, err error) {
|
||||
params := map[string]string{
|
||||
"qrcode_key": qrKey,
|
||||
}
|
||||
response, err := client.SimpleGET("https://passport.bilibili.com/x/passport-login/web/qrcode/poll", params)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body := BaseResV2{}
|
||||
err = json.NewDecoder(response.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return nil, "", errors.New(body.Message)
|
||||
}
|
||||
qrStatus = &QRStatus{}
|
||||
err = json.Unmarshal(body.Data, &qrStatus)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if qrStatus.Code != 0 {
|
||||
return qrStatus, "", nil
|
||||
}
|
||||
sessdata, err = GetCookieValue(response.Cookies(), "SESSDATA")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return qrStatus, sessdata, nil
|
||||
}
|
||||
|
||||
// GetCookieValue 获取指定 Name 的 Cookie 值
|
||||
func GetCookieValue(cookies []*http.Cookie, name string) (string, error) {
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == name {
|
||||
return cookie.Value, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("cookie with name " + name + " not found")
|
||||
}
|
||||
|
||||
// SaveSessdata 保存 SESSDATA
|
||||
func SaveSessdata(db *sql.DB, sessdata string) error {
|
||||
util.SqliteLock.Lock()
|
||||
_, err := db.Exec(`INSERT OR REPLACE INTO "field" ("name", "value") VALUES ("sessdata", ?)`, sessdata)
|
||||
util.SqliteLock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSessdata 获取 SESSDATA
|
||||
func GetSessdata(db *sql.DB) (string, error) {
|
||||
util.SqliteLock.Lock()
|
||||
row := db.QueryRow(`SELECT "value" FROM "field" WHERE "name" = "sessdata"`)
|
||||
util.SqliteLock.Unlock()
|
||||
var sessdata string
|
||||
err := row.Scan(&sessdata)
|
||||
return sessdata, err
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"bilidown/common"
|
||||
)
|
||||
|
||||
// BaseRes 来自 Bilibili 的接口响应,Message 字段为 msg
|
||||
type BaseRes struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// BaseRes2 来自 Bilibili 的接口响应,Message 字段为 message 而不是 msg
|
||||
type BaseResV2 struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// BaseResV3 来自 Bilibili 的接口响应,Message 字段为 message 而不是 msg,Data 字段为 result
|
||||
type BaseResV3 struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
|
||||
func (b *BaseRes) Success() bool {
|
||||
return b.Code == 0
|
||||
}
|
||||
|
||||
func (b *BaseResV2) Success() bool {
|
||||
return b.Code == 0
|
||||
}
|
||||
|
||||
func (b *BaseResV3) Success() bool {
|
||||
return b.Code == 0
|
||||
}
|
||||
|
||||
type QRInfo struct {
|
||||
URL string `json:"url"`
|
||||
QrcodeKey string `json:"qrcode_key"`
|
||||
}
|
||||
|
||||
type QRStatus struct {
|
||||
URL string `json:"string"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const (
|
||||
QR_NO_SCAN = 86101 // 未扫码
|
||||
QR_NO_CLICK = 86090 // 已扫码未确认
|
||||
QR_EXPIRES = 86038 // 已过期
|
||||
QR_SUCCESS = 0 // 已确认登录
|
||||
)
|
||||
|
||||
// Dimension 视频分辨率
|
||||
type Dimension struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Rotate int `json:"rotate"`
|
||||
}
|
||||
|
||||
// StaffItem 视频人员
|
||||
type StaffItem struct {
|
||||
Mid int `json:"mid"`
|
||||
Title string `json:"title"`
|
||||
Name string `json:"name"`
|
||||
Face string `json:"face"`
|
||||
}
|
||||
|
||||
// Page 分集信息
|
||||
type Page struct {
|
||||
// 配合 Bvid 用于获取播放地址
|
||||
Cid int `json:"cid"`
|
||||
// 当前分集在合集中的序号,从 1 开始
|
||||
Page int `json:"page"`
|
||||
// 分集标题
|
||||
Part string `json:"part"`
|
||||
// 分集时长
|
||||
Duration int `json:"duration"`
|
||||
// 分集分辨率
|
||||
Dimension `json:"dimension"`
|
||||
}
|
||||
|
||||
// 通过 BVID 获取的视频信息
|
||||
type VideoInfo struct {
|
||||
Bvid string `json:"bvid"`
|
||||
Aid int `json:"aid"`
|
||||
Pic string `json:"pic"`
|
||||
Title string `json:"title"`
|
||||
Pubdate int `json:"pubdate"`
|
||||
Desc string `json:"desc"`
|
||||
Owner struct {
|
||||
Mid int `json:"mid"`
|
||||
Name string `json:"name"`
|
||||
Face string `json:"face"`
|
||||
} `json:"owner"`
|
||||
Dimension `json:"dimension"`
|
||||
Staff []StaffItem `json:"staff"`
|
||||
Pages []Page `json:"pages"`
|
||||
Duration int `json:"duration"`
|
||||
Stat struct {
|
||||
Aid int `json:"aid"`
|
||||
View int `json:"view"` // 播放数量
|
||||
Danmaku int `json:"danmaku"` // 弹幕数量
|
||||
Reply int `json:"reply"` // 评论数量
|
||||
Favorite int `json:"favorite"` // 收藏数量
|
||||
Coin int `json:"coin"` // 投币数量
|
||||
Share int `json:"share"` // 转发数量
|
||||
NowRank int `json:"now_rank"` // 当前排名
|
||||
HisRank int `json:"his_rank"` // 历史最高排名
|
||||
Like int `json:"like"` // 点赞数量
|
||||
Dislike int `json:"dislike"` // 不喜欢
|
||||
} `json:"stat"`
|
||||
UgcSeason struct {
|
||||
Sections []struct {
|
||||
Title string `json:"title"`
|
||||
Episodes []struct {
|
||||
Title string `json:"title"`
|
||||
Pages []Page `json:"pages"`
|
||||
Bvid string `json:"bvid"`
|
||||
} `json:"episodes"`
|
||||
} `json:"sections"`
|
||||
Title string `json:"title"`
|
||||
} `json:"ugc_season"`
|
||||
}
|
||||
|
||||
// EpisodeInBV 通过 BV 获取的视频信息中的合集信息
|
||||
type EpisodeInBV struct {
|
||||
Aid int `json:"aid"`
|
||||
Bvid string `json:"bvid"`
|
||||
Cid int `json:"cid"`
|
||||
Pic string `json:"pic"` // 封面
|
||||
Dimension `json:"dimension"` // 分辨率
|
||||
Duration int `json:"duration"` // 时长
|
||||
EPID int `json:"ep_id"`
|
||||
LongTitle string `json:"long_title"` // 分集完整标题,比如【法外狂徒张三现身!】
|
||||
PubTime int `json:"pub_time"` // 发布时间
|
||||
Title string `json:"title"` // 分集简略标题,比如【1】
|
||||
}
|
||||
|
||||
// Episode 剧集分集信息
|
||||
type Episode struct {
|
||||
Aid int `json:"aid"`
|
||||
Bvid string `json:"bvid"`
|
||||
Cid int `json:"cid"`
|
||||
Cover string `json:"cover"` // 封面
|
||||
Dimension `json:"dimension"` // 分辨率
|
||||
Duration int `json:"duration"` // 时长
|
||||
EPID int `json:"ep_id"`
|
||||
LongTitle string `json:"long_title"` // 分集完整标题,比如【法外狂徒张三现身!】
|
||||
PubTime int `json:"pub_time"` // 发布时间
|
||||
Title string `json:"title"` // 分集简略标题,比如【1】
|
||||
}
|
||||
|
||||
// 通过 EPID 获取的视频信息
|
||||
type SeasonInfo struct {
|
||||
Actors string `json:"actors"` // 演员名单
|
||||
Areas []struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"areas"` // 地区列表
|
||||
Cover string `json:"cover"` // 封面
|
||||
Evaluate string `json:"evaluate"` // 简介
|
||||
Publish struct {
|
||||
IsFinish int `json:"is_finish"` // 是否完结
|
||||
PubTime string `json:"pub_time"` // 发布时间
|
||||
} `json:"publish"`
|
||||
SeasonID int `json:"season_id"` // 剧集编号
|
||||
SeasonTitle string `json:"season_title"` // 剧集标题
|
||||
Stat struct {
|
||||
Coins int `json:"coins"` // 投币数量
|
||||
Danmakus int `json:"danmakus"` // 弹幕数量
|
||||
Favorite int `json:"favorite"` // 收藏数量
|
||||
Favorites int `json:"favorites"` // 追剧数量
|
||||
Likes int `json:"likes"` // 点赞数量
|
||||
Reply int `json:"reply"` // 评论数量
|
||||
Share int `json:"share"` // 分享数量
|
||||
Views int `json:"views"` // 播放数量
|
||||
} `json:"stat"`
|
||||
Styles []string `json:"styles"` // 剧集内容类型,例如 [ "短剧", "奇幻", "搞笑" ]
|
||||
Title string `json:"title"` // 剧集标题
|
||||
Total int `json:"total"` // 总集数
|
||||
|
||||
Episodes []Episode `json:"episodes"` // 分集信息列表
|
||||
|
||||
NewEp struct {
|
||||
Desc string `json:"desc"` // 更新状态文本
|
||||
IsNew int `json:"is_new"` // 是否是连载,0 为完结,1 为连载
|
||||
} `json:"new_ep"`
|
||||
|
||||
Section []struct {
|
||||
Title string `json:"title"`
|
||||
Episodes []Episode `json:"episodes"`
|
||||
} `json:"section"`
|
||||
}
|
||||
|
||||
type PlayInfo struct {
|
||||
AcceptDescription []string `json:"accept_description"`
|
||||
AcceptQuality []common.MediaFormat `json:"accept_quality"`
|
||||
SupportFormats []struct {
|
||||
Quality common.MediaFormat `json:"quality"`
|
||||
Format string `json:"format"`
|
||||
NewDescription string `json:"new_description"`
|
||||
Codecs []string `json:"codecs"`
|
||||
} `json:"support_formats"`
|
||||
Dash *Dash `json:"dash"`
|
||||
}
|
||||
|
||||
type Dash struct {
|
||||
// 视频时长(秒)
|
||||
Duration int `json:"duration"`
|
||||
Video []Media `json:"video"`
|
||||
Audio []Media `json:"audio"`
|
||||
Flac *struct {
|
||||
Audio Media `json:"audio"`
|
||||
} `json:"flac"`
|
||||
}
|
||||
|
||||
type Media struct {
|
||||
ID common.MediaFormat `json:"id"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
BackupURL []string `json:"backupUrl"`
|
||||
Bandwidth int `json:"bandwidth"`
|
||||
MimeType string `json:"mimeType"`
|
||||
Codecs string `json:"codecs"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
FrameRate string `json:"frameRate"`
|
||||
Codecid int `json:"codecid"`
|
||||
}
|
||||
|
||||
type FavList []struct {
|
||||
Title string `json:"title"`
|
||||
Cover string `json:"cover"`
|
||||
Intro string `json:"intro"`
|
||||
Duration int `json:"duration"`
|
||||
Upper struct {
|
||||
Mid int `json:"mid"`
|
||||
Name string `json:"name"`
|
||||
Face string `json:"face"`
|
||||
} `json:"upper"`
|
||||
PubTime int `json:"pubtime"`
|
||||
Bvid string `json:"bvid"`
|
||||
Ugc struct {
|
||||
FirstCid int `json:"first_cid"`
|
||||
} `json:"ugc"`
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetBVInfo 根据 BVID 获取视频信息
|
||||
func (client *BiliClient) GetVideoInfo(bvid string) (*VideoInfo, error) {
|
||||
if client.SESSDATA == "" {
|
||||
return nil, errors.New("SESSDATA 不能为空")
|
||||
}
|
||||
params := map[string]string{"bvid": bvid}
|
||||
response, err := client.SimpleGET("https://api.bilibili.com/x/web-interface/wbi/view", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := BaseResV2{}
|
||||
err = json.NewDecoder(response.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return nil, errors.New(body.Message)
|
||||
}
|
||||
bvInfo := VideoInfo{}
|
||||
err = json.Unmarshal(body.Data, &bvInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bvInfo, nil
|
||||
}
|
||||
|
||||
// GetSeasonInfo 根据 EPID 或 SSID 获取剧集信息
|
||||
func (client *BiliClient) GetSeasonInfo(epid int, ssid int) (*SeasonInfo, error) {
|
||||
if client.SESSDATA == "" {
|
||||
return nil, errors.New("SESSDATA 不能为空")
|
||||
}
|
||||
params := map[string]string{
|
||||
"ep_id": strconv.Itoa(epid),
|
||||
"season_id": strconv.Itoa(ssid),
|
||||
}
|
||||
response, err := client.SimpleGET("https://api.bilibili.com/pgc/view/web/season", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := BaseResV3{}
|
||||
err = json.NewDecoder(response.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return nil, errors.New(body.Message)
|
||||
}
|
||||
seasonInfo := SeasonInfo{}
|
||||
err = json.Unmarshal(body.Result, &seasonInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &seasonInfo, nil
|
||||
}
|
||||
|
||||
// GetPlayInfo 根据 BVID 和 CID 获取视频播放信息
|
||||
func (client *BiliClient) GetPlayInfo(bvid string, cid int) (*PlayInfo, error) {
|
||||
if client.SESSDATA == "" {
|
||||
return nil, errors.New("SESSDATA 不能为空")
|
||||
}
|
||||
params := map[string]string{
|
||||
"bvid": bvid,
|
||||
"cid": strconv.Itoa(cid),
|
||||
"fnval": "4048",
|
||||
"fnver": "0",
|
||||
"fourk": "1",
|
||||
}
|
||||
response, err := client.SimpleGET("https://api.bilibili.com/x/player/playurl", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := BaseResV2{}
|
||||
err = json.NewDecoder(response.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return nil, errors.New(body.Message)
|
||||
}
|
||||
playInfo := PlayInfo{}
|
||||
err = json.Unmarshal(body.Data, &playInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &playInfo, nil
|
||||
}
|
||||
|
||||
func (client *BiliClient) GetPopularVideos() ([]VideoInfo, error) {
|
||||
if client.SESSDATA == "" {
|
||||
return nil, errors.New("SESSDATA 不能为空")
|
||||
}
|
||||
urls := []string{
|
||||
"https://api.bilibili.com/x/web-interface/popular",
|
||||
"https://api.bilibili.com/x/web-interface/popular/precious",
|
||||
"https://api.bilibili.com/x/web-interface/ranking/v2",
|
||||
}
|
||||
response, err := client.SimpleGET(urls[rand.Intn(len(urls))], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body := BaseResV2{}
|
||||
|
||||
err = json.NewDecoder(response.Body).Decode(&body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return nil, errors.New(body.Message)
|
||||
}
|
||||
data := struct {
|
||||
List []VideoInfo `json:"list"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(body.Data, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.List, nil
|
||||
}
|
||||
|
||||
// GetSeasonsArchivesList 获取合集中的第一个视频的 BVID
|
||||
func (client *BiliClient) GetSeasonsArchivesListFirstBvid(mid int, seasonId int) (string, error) {
|
||||
url := "https://api.bilibili.com/x/polymer/web-space/seasons_archives_list"
|
||||
params := map[string]string{
|
||||
"mid": strconv.Itoa(mid),
|
||||
"season_id": strconv.Itoa(seasonId),
|
||||
"page_num": "1",
|
||||
"page_size": "1",
|
||||
}
|
||||
|
||||
response, err := client.SimpleGET(url, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
body := BaseResV2{}
|
||||
if err = json.NewDecoder(response.Body).Decode(&body); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return "", errors.New(body.Message)
|
||||
}
|
||||
var data struct {
|
||||
Archives []struct {
|
||||
Bvid string `json:"bvid"`
|
||||
} `json:"archives"`
|
||||
}
|
||||
if err = json.Unmarshal(body.Data, &data); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
if len(data.Archives) == 0 {
|
||||
return "", errors.New("视频列表为空")
|
||||
}
|
||||
return data.Archives[0].Bvid, nil
|
||||
}
|
||||
|
||||
func (client *BiliClient) GetFavlist(mediaId int) (*FavList, error) {
|
||||
if client.SESSDATA == "" {
|
||||
return nil, errors.New("SESSDATA 不能为空")
|
||||
}
|
||||
page := 0
|
||||
retry := 0
|
||||
allFavList := FavList{}
|
||||
for {
|
||||
favList, hasMore, err := client.GetFavlistByPage(mediaId, page, 40)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
if retry == 5 || strings.HasPrefix(err.Error(), "body.Code not 0") {
|
||||
return nil, err
|
||||
}
|
||||
retry++
|
||||
continue
|
||||
}
|
||||
allFavList = append(allFavList, *favList...)
|
||||
if !hasMore {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
return &allFavList, nil
|
||||
}
|
||||
|
||||
func (client *BiliClient) GetFavlistByPage(mediaId int, page int, pageSize int) (favlist *FavList, hasMore bool, err error) {
|
||||
if client.SESSDATA == "" {
|
||||
return nil, false, errors.New("SESSDATA 不能为空")
|
||||
}
|
||||
response, err := client.SimpleGET("https://api.bilibili.com/x/v3/fav/resource/list", map[string]string{
|
||||
"media_id": strconv.Itoa(mediaId),
|
||||
"pn": strconv.Itoa(page + 1),
|
||||
"ps": strconv.Itoa(pageSize),
|
||||
"order": "mtime",
|
||||
"type": "0",
|
||||
"tid": "0",
|
||||
"platform": "web",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
body := BaseResV2{}
|
||||
if err = json.NewDecoder(response.Body).Decode(&body); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if body.Code != 0 {
|
||||
return nil, false, fmt.Errorf("body.Code not 0, %s", body.Message)
|
||||
}
|
||||
data := struct {
|
||||
Medias FavList `json:"medias"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}{}
|
||||
if err = json.Unmarshal(body.Data, &data); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &data.Medias, data.HasMore, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"bilidown/util"
|
||||
"database/sql"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var MIXIN_KEY_ENC_TAB = []int{
|
||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35,
|
||||
27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13,
|
||||
37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4,
|
||||
22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52,
|
||||
}
|
||||
|
||||
// GetWebKey 获取最新可用的 WbiKey,自动刷新缓存
|
||||
func (client *BiliClient) getWbiKey(db *sql.DB) (wbiKey string, err error) {
|
||||
wbiKey, err = getWbiKeyFromDB(db)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if wbiKey == "" {
|
||||
// 更新数据库中的 WebKey
|
||||
wbiKey, err = client.getWbiKeyRemote()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = saveWbiKey(db, wbiKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return wbiKey, nil
|
||||
}
|
||||
|
||||
func (client *BiliClient) GetMixinKey(db *sql.DB) (string, error) {
|
||||
wbiKey, err := client.getWbiKey(db)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var result string
|
||||
for _, index := range MIXIN_KEY_ENC_TAB {
|
||||
result += string(wbiKey[index])
|
||||
}
|
||||
return result[:32], nil
|
||||
}
|
||||
|
||||
func WbiSign(params map[string]string, mixinKey string) url.Values {
|
||||
values := url.Values{}
|
||||
for key, value := range params {
|
||||
values.Set(key, value)
|
||||
}
|
||||
wts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
values.Set("wts", wts)
|
||||
encodeStr := strings.ReplaceAll(values.Encode(), "+", "%20") + mixinKey
|
||||
w_rid := util.MD5Hash(encodeStr)
|
||||
values.Set("w_rid", w_rid)
|
||||
return values
|
||||
}
|
||||
|
||||
// GetWbiKey 从数据库中获取 wbiKey,如果数据库中不存在记录或记录过期,则返回空字符串但不返回错误
|
||||
func getWbiKeyFromDB(db *sql.DB) (wbiKey string, err error) {
|
||||
fields, err := util.GetFields(db, "wbi_key", "wbi_key_update_at")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 获取上次更新时间的时间戳,单位是秒
|
||||
updateAt, err := strconv.ParseInt(fields["wbi_key_update_at"], 10, 64)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
// 注意 key_update_at 单位是秒,判断上次刷新时间是否超过 1 天
|
||||
if time.Now().Unix()-updateAt > 24*60*60 {
|
||||
return "", nil
|
||||
}
|
||||
return fields["wbi_key"], nil
|
||||
}
|
||||
|
||||
// SaveWbiKey 保存 imgKey 和 subKey 到数据库
|
||||
func saveWbiKey(db *sql.DB, wbiKey string) error {
|
||||
return util.SaveFields(db, [][2]string{
|
||||
{"wbi_key", wbiKey},
|
||||
{"wbi_key_update_at", strconv.FormatInt(time.Now().Unix(), 10)},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package bilibili
|
||||
|
||||
import (
|
||||
"bilidown/util"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetWbiKey(t *testing.T) {
|
||||
db := util.MustGetDB("../data.db")
|
||||
defer db.Close()
|
||||
sessdata, err := GetSessdata(db)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
client := BiliClient{SESSDATA: sessdata}
|
||||
mixinKey, err := client.GetMixinKey(db)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fmt.Println(mixinKey)
|
||||
}
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
fmt.Println(time.Now().Unix())
|
||||
}
|
||||
|
||||
func TestURLEncode(t *testing.T) {
|
||||
str := url.Values{
|
||||
"foo": {"one one four"},
|
||||
"bar": {"五一四"},
|
||||
"baz": {"1919810"},
|
||||
}.Encode()
|
||||
str = strings.ReplaceAll(str, "+", "%20")
|
||||
fmt.Println(str)
|
||||
}
|
||||
|
||||
func TestWbiSign(t *testing.T) {
|
||||
db := util.MustGetDB("../data.db")
|
||||
defer db.Close()
|
||||
sessdata, err := GetSessdata(db)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
client := BiliClient{SESSDATA: sessdata}
|
||||
mixinKey, err := client.GetMixinKey(db)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
newParams := WbiSign(map[string]string{
|
||||
"dm_cover_img_str": "QU5HTEUgKEludGVsLCBJbnRlbChSKSBJcmlzKFIpIFhlIEdyYXBoaWNzICgweDAwMDA5QTQ5KSBEaXJlY3QzRDExIHZzXzVfMCBwc181XzAsIEQzRDExKUdvb2dsZSBJbmMuIChJbnRlbC",
|
||||
"dm_img_inter": `{"ds":[{"t":10,"c":"YmUtcGFnZXItaXRlbSBiZS1wYWdlci1pdGVtLWFjdGl2ZQ","p":[330,110,110],"s":[179,179,358]}],"wh":[5762,6704,10],"of":[318,636,318]}`,
|
||||
"dm_img_list": `[{"x":1847,"y":-1616,"z":0,"timestamp":2486463,"k":70,"type":0},{"x":1972,"y":-1005,"z":24,"timestamp":2486563,"k":120,"type":0},{"x":2564,"y":-638,"z":72,"timestamp":2486665,"k":107,"type":0},{"x":4200,"y":-753,"z":199,"timestamp":2486765,"k":91,"type":0},{"x":4922,"y":-73,"z":426,"timestamp":2486865,"k":106,"type":0},{"x":4656,"y":-512,"z":35,"timestamp":2486965,"k":94,"type":0},{"x":4942,"y":-590,"z":79,"timestamp":2487066,"k":126,"type":0},{"x":5200,"y":-414,"z":123,"timestamp":2487167,"k":60,"type":0},{"x":4916,"y":-820,"z":228,"timestamp":2563922,"k":111,"type":0},{"x":5004,"y":-50,"z":570,"timestamp":2564022,"k":110,"type":0},{"x":4784,"y":330,"z":666,"timestamp":2564122,"k":98,"type":0},{"x":5029,"y":767,"z":956,"timestamp":2564223,"k":115,"type":0},{"x":5327,"y":1400,"z":1307,"timestamp":2564324,"k":107,"type":0},{"x":4598,"y":671,"z":578,"timestamp":2564424,"k":125,"type":0},{"x":5228,"y":1283,"z":1193,"timestamp":2564525,"k":83,"type":0},{"x":5210,"y":932,"z":978,"timestamp":2564625,"k":66,"type":0},{"x":5034,"y":195,"z":484,"timestamp":2564725,"k":112,"type":0},{"x":6496,"y":1467,"z":1849,"timestamp":2564825,"k":83,"type":0},{"x":6592,"y":1387,"z":1852,"timestamp":2564929,"k":122,"type":0},{"x":6001,"y":728,"z":1235,"timestamp":2565030,"k":95,"type":0},{"x":6666,"y":1393,"z":1900,"timestamp":2627309,"k":121,"type":1},{"x":5486,"y":5603,"z":2283,"timestamp":2627842,"k":102,"type":0},{"x":5269,"y":4186,"z":1733,"timestamp":2627947,"k":117,"type":0},{"x":4439,"y":3174,"z":897,"timestamp":2628056,"k":117,"type":0},{"x":5231,"y":3959,"z":1687,"timestamp":2628268,"k":79,"type":0},{"x":6197,"y":4786,"z":2587,"timestamp":2628368,"k":96,"type":0},{"x":4667,"y":3298,"z":724,"timestamp":2628470,"k":91,"type":0},{"x":4658,"y":3436,"z":205,"timestamp":2629433,"k":88,"type":0},{"x":5380,"y":3443,"z":1117,"timestamp":2630390,"k":121,"type":0},{"x":5559,"y":2678,"z":1920,"timestamp":2630492,"k":116,"type":0},{"x":5517,"y":2465,"z":2023,"timestamp":2630634,"k":86,"type":0},{"x":6584,"y":3533,"z":3087,"timestamp":2630782,"k":96,"type":0},{"x":6183,"y":3242,"z":2839,"timestamp":2630892,"k":103,"type":0},{"x":6277,"y":580,"z":1449,"timestamp":2663911,"k":68,"type":0},{"x":7556,"y":3077,"z":3053,"timestamp":2664013,"k":114,"type":0},{"x":5321,"y":1642,"z":1201,"timestamp":2664114,"k":104,"type":0},{"x":5839,"y":2261,"z":1761,"timestamp":2664215,"k":72,"type":0},{"x":6303,"y":2726,"z":2222,"timestamp":2664317,"k":93,"type":0},{"x":7553,"y":4255,"z":3532,"timestamp":2664420,"k":104,"type":0},{"x":7677,"y":4381,"z":3650,"timestamp":2664521,"k":119,"type":0},{"x":8184,"y":4762,"z":4098,"timestamp":2664622,"k":122,"type":0},{"x":4555,"y":740,"z":406,"timestamp":2664722,"k":118,"type":0},{"x":4305,"y":241,"z":75,"timestamp":2664822,"k":105,"type":0},{"x":6373,"y":2218,"z":2117,"timestamp":2664934,"k":110,"type":0},{"x":6079,"y":1757,"z":1680,"timestamp":2665036,"k":90,"type":0},{"x":8158,"y":3462,"z":3524,"timestamp":2665136,"k":100,"type":0},{"x":9312,"y":4205,"z":4531,"timestamp":2665236,"k":124,"type":0},{"x":5306,"y":89,"z":487,"timestamp":2665337,"k":89,"type":0},{"x":9721,"y":4399,"z":4941,"timestamp":2665437,"k":100,"type":0},{"x":9058,"y":3727,"z":4282,"timestamp":2665540,"k":107,"type":0}]`,
|
||||
"dm_img_str": "V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ",
|
||||
"keyword": "",
|
||||
"mid": "596866446",
|
||||
"order": "pubdate",
|
||||
"order_avoided": "true",
|
||||
"platform": "web",
|
||||
"pn": "3",
|
||||
"ps": "30",
|
||||
"tid": "0",
|
||||
"w_webid": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzcG1faWQiOiIwLjAiLCJidXZpZCI6IjBGRTlGRDMxLTZGQTQtRjU2RC1GQjEwLUI4OTg2ODUyRUZDNzUwMDUyaW5mb2MiLCJ1c2VyX2FnZW50IjoiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEzMS4wLjAuMCBTYWZhcmkvNTM3LjM2IEVkZy8xMzEuMC4wLjAiLCJidXZpZF9mcCI6IjY3YzZkNzg2NTgyYzQ2OGYwNjRiZTE5MTk2OWU4ZWE4IiwiYmlsaV90aWNrZXQiOiI4ODA0MDgzYmYzZGQ1NGNkNzI4OWZjZDE0MmIwN2U4ZCIsImNyZWF0ZWRfYXQiOjE3MzI0NzE3MTIsInR0bCI6ODY0MDAsInVybCI6Ii81OTY4NjY0NDYvdmlkZW8_dGlkPTBcdTAwMjZwbj0yXHUwMDI2a2V5d29yZD1cdTAwMjZvcmRlcj1wdWJkYXRlIiwicmVzdWx0Ijoibm9ybWFsIiwiaXNzIjoiZ2FpYSIsImlhdCI6MTczMjQ3MTcxMn0.pTVyhdRcB2VFcXqeYoi3TjnlEFLAXMpNNJK-dW9Gq3vqBL4YldGgZBuGBXXF7Ldwg_vW6TQg6pQCQ7vz357ws2Z9g2-kLIuNmR3j8oMg2zAXAND1q5oJNw0jNnevhLlB8_vOcip0eIJSHRjqbPbNShKLOcSnSfLaiI64EHwjRFAOEPYVw3evLXKB4TFnxSRi1WDSI684TfNrXp0_2yTJvuPheHQmQC1NcUP_P9tqTuRiDy3YfkuR8PlRcQxmKHVs-byObL5WEPqMMQa8b8zidRtzEkbGV7ra8gTgv1HwgpSDi_Y1VNsX2WtNcBPTSwURWc7zREf-RJqruzgcf1ErpA",
|
||||
"web_location": "1550101",
|
||||
}, mixinKey)
|
||||
|
||||
fmt.Println(newParams.Encode())
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func RandomString(length int) string {
|
||||
randomBytes := make([]byte, length)
|
||||
rand.Read(randomBytes)
|
||||
return fmt.Sprintf("%x", randomBytes)[:length]
|
||||
}
|
||||
|
||||
type MediaFormat int
|
||||
@@ -0,0 +1,41 @@
|
||||
module bilidown
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
modernc.org/sqlite v1.33.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
|
||||
github.com/getlantern/errors v1.0.4 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect
|
||||
modernc.org/libc v1.61.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.4 h1:i2iR1M9GKj4WuingpNqJ+XQEw6i6dnAgKAmLj6ZB3X0=
|
||||
github.com/getlantern/errors v1.0.4/go.mod h1:/Foq8jtSDGP8GOXzAjeslsC4Ar/3kB+UiQH+WyV4pzY=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag=
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534/go.mod h1:ZsLfOY6gKQOTyEcPYNA9ws5/XHZQFroxqCOhHjGcs9Y=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||
modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 h1:IYXPPTTjjoSHvUClZIYexDiO7g+4x+XveKT4gCIAwiY=
|
||||
modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
|
||||
modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"bilidown/router"
|
||||
"bilidown/util"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const (
|
||||
HTTP_PORT = 8098 // 限定 HTTP 服务器端口
|
||||
HTTP_HOST = "" // 限定 HTTP 服务器主机
|
||||
VERSION = "v2.1.1" // 软件版本号,将影响托盘标题显示
|
||||
)
|
||||
|
||||
var urlLocal = fmt.Sprintf("http://127.0.0.1:%d", HTTP_PORT)
|
||||
var urlLocalUnix = fmt.Sprintf("%s?___%d", urlLocal, time.Now().UnixMilli())
|
||||
|
||||
func main() {
|
||||
checkFFmpeg()
|
||||
// 启动托盘程序
|
||||
systray.Run(onReady, nil)
|
||||
}
|
||||
|
||||
func onReady() {
|
||||
// 设置托盘图标
|
||||
setIcon()
|
||||
// 设置托盘标题
|
||||
setTitle()
|
||||
// 设置托盘菜单
|
||||
setMenuItem()
|
||||
// 初始化数据表
|
||||
mustInitTables()
|
||||
// 配置和启动 HTTP 服务器
|
||||
mustRunServer()
|
||||
// 调用默认浏览器访问端口
|
||||
time.Sleep(time.Millisecond * 1000)
|
||||
openBrowser(urlLocalUnix)
|
||||
// 保持运行
|
||||
select {}
|
||||
}
|
||||
|
||||
// checkFFmpeg 检测 ffmpeg 的安装情况,如果未安装则打印提示信息。
|
||||
func checkFFmpeg() {
|
||||
if _, err := util.GetFFmpegPath(); err != nil {
|
||||
fmt.Println("🚨 FFmpeg is missing. Install it from https://www.ffmpeg.org/download.html or place it in ./bin, then restart the application.")
|
||||
select {}
|
||||
}
|
||||
}
|
||||
|
||||
// 配置和启动 HTTP 服务器
|
||||
func mustRunServer() {
|
||||
// 前端打包文件
|
||||
http.Handle("/", http.FileServer(http.Dir("static")))
|
||||
// 后端接口服务
|
||||
http.Handle("/api/", http.StripPrefix("/api", router.API()))
|
||||
// 启动 HTTP 服务器
|
||||
go func() {
|
||||
err := http.ListenAndServe(fmt.Sprintf("%s:%d", HTTP_HOST, HTTP_PORT), nil)
|
||||
if err != nil {
|
||||
log.Fatal("http.ListenAndServe:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// openBrowser 调用系统默认浏览器打开指定 URL
|
||||
func openBrowser(url string) {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", url)
|
||||
case "linux":
|
||||
cmd = exec.Command("xdg-open", url)
|
||||
default:
|
||||
log.Printf("openBrowser: %v.", errors.New("unsupported operating system"))
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("openBrowser: %v.", err)
|
||||
}
|
||||
fmt.Printf("Opened in default browser: %s.\n", url)
|
||||
}
|
||||
|
||||
// setIcon 设置托盘图标
|
||||
func setIcon() {
|
||||
var path string
|
||||
if runtime.GOOS == "windows" {
|
||||
path = "static/favicon.ico"
|
||||
} else {
|
||||
path = "static/favicon-32x32.png"
|
||||
}
|
||||
systray.SetIcon(mustReadFile(path))
|
||||
}
|
||||
|
||||
// mustReadFile 返回文件字节内容
|
||||
func mustReadFile(path string) []byte {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatalln("os.ReadFile:", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// setTitle 设置托盘标题和工具提示
|
||||
func setTitle() {
|
||||
title := "Bilidown"
|
||||
tooltip := fmt.Sprintf("%s 视频解析器 %s (port:%d)", title, VERSION, HTTP_PORT)
|
||||
// only available on Mac and Windows.
|
||||
systray.SetTooltip(tooltip)
|
||||
}
|
||||
|
||||
// setMenuItem 设置托盘菜单
|
||||
func setMenuItem() {
|
||||
openBrowserItemText := fmt.Sprintf("打开主界面 (port:%d)", HTTP_PORT)
|
||||
openBrowserItem := systray.AddMenuItem(openBrowserItemText, openBrowserItemText)
|
||||
go func() {
|
||||
for {
|
||||
<-openBrowserItem.ClickedCh
|
||||
openBrowser(urlLocalUnix)
|
||||
}
|
||||
}()
|
||||
|
||||
aboutItemText := "Github 项目主页"
|
||||
aboutItem := systray.AddMenuItem(aboutItemText, aboutItemText)
|
||||
go func() {
|
||||
for {
|
||||
<-aboutItem.ClickedCh
|
||||
openBrowser("https://github.com/iuroc/bilidown")
|
||||
}
|
||||
}()
|
||||
|
||||
exitItemText := "退出应用"
|
||||
exitItem := systray.AddMenuItem(exitItemText, exitItemText)
|
||||
go func() {
|
||||
<-exitItem.ClickedCh
|
||||
log.Printf("Bilidown has exited.")
|
||||
systray.Quit()
|
||||
}()
|
||||
}
|
||||
|
||||
// mustInitTables 初始化数据表
|
||||
func mustInitTables() {
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS "field" (
|
||||
"name" TEXT PRIMARY KEY NOT NULL,
|
||||
"value" TEXT
|
||||
)`); err != nil {
|
||||
log.Fatalln("create table field:", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS "log" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"content" TEXT NOT NULL,
|
||||
"create_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`); err != nil {
|
||||
log.Fatalln("create table log:", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS "task" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"bvid" text NOT NULL,
|
||||
"cid" integer NOT NULL,
|
||||
"format" integer NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"owner" text NOT NULL,
|
||||
"cover" text NOT NULL,
|
||||
"status" text NOT NULL,
|
||||
"folder" text NOT NULL,
|
||||
"duration" integer NOT NULL,
|
||||
"download_type" text NOT NULL DEFAULT 'merge',
|
||||
"create_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`); err != nil {
|
||||
log.Fatalln("create table task:", err)
|
||||
}
|
||||
|
||||
if _, err := util.GetCurrentFolder(db); err != nil {
|
||||
log.Fatalln("util.GetCurrentFolder:", err)
|
||||
}
|
||||
|
||||
if err := initHistoryTask(db); err != nil {
|
||||
log.Fatalln("initHistoryTask:", err)
|
||||
}
|
||||
|
||||
// 添加可能缺失的列(用于数据库迁移)
|
||||
if err := addMissingColumns(db); err != nil {
|
||||
log.Fatalln("addMissingColumns:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// addMissingColumns 添加可能缺失的列(用于数据库迁移)
|
||||
func addMissingColumns(db *sql.DB) error {
|
||||
// 检查download_type列是否存在,如果不存在则添加
|
||||
// SQLite没有直接的方法检查列是否存在,我们尝试添加列并忽略错误
|
||||
// 使用事务确保操作原子性
|
||||
util.SqliteLock.Lock()
|
||||
_, _ = db.Exec(`ALTER TABLE "task" ADD COLUMN "download_type" TEXT DEFAULT 'merge'`)
|
||||
// 将现有记录中的NULL值更新为默认值'merge'
|
||||
_, _ = db.Exec(`UPDATE "task" SET "download_type" = 'merge' WHERE "download_type" IS NULL`)
|
||||
util.SqliteLock.Unlock()
|
||||
|
||||
// 忽略错误,因为列可能已经存在
|
||||
// SQLite错误码为1表示列已存在
|
||||
return nil
|
||||
}
|
||||
|
||||
// initHistoryTask 将上一次程序运行时未完成的任务进度全部变为 error
|
||||
func initHistoryTask(db *sql.DB) error {
|
||||
util.SqliteLock.Lock()
|
||||
_, err := db.Exec(`UPDATE "task" SET "status" = 'error' WHERE "status" IN ('waiting', 'running')`)
|
||||
util.SqliteLock.Unlock()
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"bilidown/bilibili"
|
||||
"bilidown/util"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
func getQRInfo(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
client := bilibili.BiliClient{}
|
||||
qrInfo, err := client.NewQRInfo()
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
imageData, err := qrcode.Encode(qrInfo.URL, qrcode.Medium, 256)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
base64Str := base64.StdEncoding.EncodeToString(imageData)
|
||||
util.Res{
|
||||
Success: true,
|
||||
Message: "获取成功",
|
||||
Data: struct {
|
||||
Key string `json:"key"`
|
||||
Image string `json:"image"`
|
||||
}{
|
||||
Key: qrInfo.QrcodeKey,
|
||||
Image: "data:image/png;base64," + base64Str,
|
||||
}}.Write(w)
|
||||
}
|
||||
|
||||
func checkLogin(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil || sessdata == "" {
|
||||
util.Res{Success: false, Message: "未登录"}.Write(w)
|
||||
return
|
||||
}
|
||||
client := bilibili.BiliClient{SESSDATA: sessdata}
|
||||
check, err := client.CheckLogin()
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
if check {
|
||||
util.Res{Success: true, Message: "登录成功"}.Write(w)
|
||||
} else {
|
||||
util.Res{Success: false, Message: "登录失败"}.Write(w)
|
||||
}
|
||||
}
|
||||
|
||||
// getQRStatus 获取二维码状态
|
||||
func getQRStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ParseForm() != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
|
||||
key := r.FormValue("key")
|
||||
if key == "" {
|
||||
util.Res{Success: false, Message: "key 不能为空"}.Write(w)
|
||||
return
|
||||
}
|
||||
client := bilibili.BiliClient{}
|
||||
qrStatus, sessdata, err := client.GetQRStatus(key)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
if qrStatus.Code != bilibili.QR_SUCCESS {
|
||||
util.Res{Success: false, Message: qrStatus.Message}.Write(w)
|
||||
return
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
err = bilibili.SaveSessdata(db, sessdata)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "登录成功"}.Write(w)
|
||||
}
|
||||
|
||||
func logout(w http.ResponseWriter, r *http.Request) {
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
err := bilibili.SaveSessdata(db, "")
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "退出成功"}.Write(w)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"bilidown/util"
|
||||
"bilidown/util/res_error"
|
||||
)
|
||||
|
||||
func API() *http.ServeMux {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("/getVideoInfo", getVideoInfo)
|
||||
router.HandleFunc("/getSeasonInfo", getSeasonInfo)
|
||||
router.HandleFunc("/getQRInfo", getQRInfo)
|
||||
router.HandleFunc("/getQRStatus", getQRStatus)
|
||||
router.HandleFunc("/checkLogin", checkLogin)
|
||||
router.HandleFunc("/getPlayInfo", getPlayInfo)
|
||||
router.HandleFunc("/createTask", createTask)
|
||||
router.HandleFunc("/getActiveTask", getActiveTask)
|
||||
router.HandleFunc("/getTaskList", getTaskList)
|
||||
router.HandleFunc("/showFile", showFile)
|
||||
router.HandleFunc("/getFields", getFields)
|
||||
router.HandleFunc("/saveFields", saveFields)
|
||||
router.HandleFunc("/logout", logout)
|
||||
router.HandleFunc("/quit", quit)
|
||||
router.HandleFunc("/getPopularVideos", getPopularVideos)
|
||||
router.HandleFunc("/deleteTask", deleteTask)
|
||||
router.HandleFunc("/getRedirectedLocation", getRedirectedLocation)
|
||||
router.HandleFunc("/downloadVideo", downloadVideo)
|
||||
router.HandleFunc("/getSeasonsArchivesListFirstBvid", getSeasonsArchivesListFirstBvid)
|
||||
router.HandleFunc("/getFavList", getFavList)
|
||||
return router
|
||||
}
|
||||
|
||||
func getRedirectedLocation(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ParseForm() != nil {
|
||||
res_error.Send(w, res_error.ParamError)
|
||||
return
|
||||
}
|
||||
url := r.FormValue("url")
|
||||
if !util.IsValidURL(url) {
|
||||
res_error.Send(w, res_error.URLFormatError)
|
||||
return
|
||||
}
|
||||
if location, err := util.GetRedirectedLocation(url); err != nil {
|
||||
res_error.Send(w, res_error.NoLocationError)
|
||||
return
|
||||
} else {
|
||||
util.Res{Success: true, Message: "获取成功", Data: location}.Write(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func quit(w http.ResponseWriter, r *http.Request) {
|
||||
util.Res{Success: true, Message: "退出成功"}.Write(w)
|
||||
go func() {
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func getFields(w http.ResponseWriter, r *http.Request) {
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
|
||||
fields, err := util.GetFields(db, util.FieldUtil{}.AllowSelect()...)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Data: fields}.Write(w)
|
||||
}
|
||||
|
||||
func saveFields(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
util.Res{Success: false, Message: "不支持的请求方法"}.Write(w)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
var body [][2]string
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&body)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
|
||||
fu := util.FieldUtil{}
|
||||
|
||||
for _, d := range body {
|
||||
if !fu.IsAllowUpdate(d[0]) {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("字段 %s 不允许修改", d[0])}.Write(w)
|
||||
return
|
||||
}
|
||||
|
||||
if d[0] == "download_folder" {
|
||||
if _, err := os.Stat(d[1]); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(d[1], os.ModePerm); err != nil {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("目录创建失败:%s", d[1])}.Write(w)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("路径设置失败:%v", err)}.Write(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = util.SaveFields(db, body)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "保存成功"}.Write(w)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"bilidown/task"
|
||||
"bilidown/util"
|
||||
)
|
||||
|
||||
func createTask(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
if r.Method != http.MethodPost {
|
||||
util.Res{Success: false, Message: "不支持的请求方法"}.Write(w)
|
||||
return
|
||||
}
|
||||
var body []task.TaskInDB
|
||||
err := json.NewDecoder(r.Body).Decode(&body)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
for _, item := range body {
|
||||
if !util.CheckBvidFormat(item.Bvid) {
|
||||
util.Res{Success: false, Message: "bvid 格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
if item.Cover == "" || item.Title == "" || item.Owner == "" {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
}
|
||||
|
||||
if !util.IsValidURL(item.Cover) {
|
||||
util.Res{Success: false, Message: "封面链接格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
if !util.IsValidURL(item.Audio) {
|
||||
util.Res{Success: false, Message: "音频链接格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
if !util.IsValidURL(item.Video) {
|
||||
util.Res{Success: false, Message: "视频链接格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
if !util.IsValidFormatCode(item.Format) {
|
||||
util.Res{Success: false, Message: "清晰度代码错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
item.Folder, err = util.GetCurrentFolder(db)
|
||||
item.Status = "waiting"
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("util.GetCurrentFolder: %v.", err)}.Write(w)
|
||||
return
|
||||
}
|
||||
_task := task.Task{TaskInDB: item}
|
||||
_task.Title = util.FilterFileName(_task.Title)
|
||||
err = _task.Create(db)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("_task.Create: %v.", err)}.Write(w)
|
||||
return
|
||||
}
|
||||
go _task.Start()
|
||||
}
|
||||
util.Res{Success: true, Message: "创建成功"}.Write(w)
|
||||
}
|
||||
|
||||
func getActiveTask(w http.ResponseWriter, r *http.Request) {
|
||||
util.Res{Success: true, Data: task.GlobalTaskList}.Write(w)
|
||||
}
|
||||
|
||||
func getTaskList(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
page, err := strconv.Atoi(r.FormValue("page"))
|
||||
if err != nil {
|
||||
page = 0
|
||||
}
|
||||
pageSize, err := strconv.Atoi(r.FormValue("pageSize"))
|
||||
if err != nil {
|
||||
pageSize = 360
|
||||
}
|
||||
tasks, err := task.GetTaskList(db, page, pageSize)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "获取成功", Data: tasks}.Write(w)
|
||||
}
|
||||
|
||||
// showFile 调用 Explorer 查看文件位置
|
||||
func showFile(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
filePath := r.FormValue("filePath")
|
||||
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// 根据操作系统选择命令
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// Windows 使用 explorer
|
||||
cmd = exec.Command("explorer", "/select,", filePath)
|
||||
case "darwin":
|
||||
// macOS 使用 open
|
||||
cmd = exec.Command("open", "-R", filePath)
|
||||
case "linux":
|
||||
// Linux 使用 xdg-open
|
||||
cmd = exec.Command("xdg-open", filePath)
|
||||
default:
|
||||
util.Res{Success: false, Message: "不支持的操作系统"}.Write(w)
|
||||
return
|
||||
}
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "操作成功"}.Write(w)
|
||||
}
|
||||
|
||||
func deleteTask(w http.ResponseWriter, r *http.Request) {
|
||||
taskIDStr := r.FormValue("id")
|
||||
taskID, err := strconv.Atoi(taskIDStr)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
|
||||
_task, err := task.GetTask(db, taskID)
|
||||
if err == sql.ErrNoRows {
|
||||
util.Res{Success: true, Message: "数据库中没有该条记录,所以本次操作被忽略,可以算作成功。"}.Write(w)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("task.GetTask: %v", err)}.Write(w)
|
||||
return
|
||||
}
|
||||
filePath := _task.FilePath()
|
||||
err = os.Remove(filePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("文件删除失败 os.Remove: %v", err)}.Write(w)
|
||||
return
|
||||
}
|
||||
|
||||
err = task.DeleteTask(db, taskID)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("task.DeleteTask: %v", err)}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "删除成功"}.Write(w)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"bilidown/bilibili"
|
||||
"bilidown/util"
|
||||
"bilidown/util/res_error"
|
||||
)
|
||||
|
||||
// getVideoInfo 通过 BV 号获取视频信息
|
||||
func getVideoInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ParseForm() != nil {
|
||||
res_error.Send(w, res_error.ParamError)
|
||||
return
|
||||
}
|
||||
bvid := r.FormValue("bvid")
|
||||
if !util.CheckBvidFormat(bvid) {
|
||||
res_error.Send(w, res_error.BvidFormatError)
|
||||
return
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil || sessdata == "" {
|
||||
res_error.Send(w, res_error.NotLogin)
|
||||
return
|
||||
}
|
||||
client := bilibili.BiliClient{SESSDATA: sessdata}
|
||||
videoInfo, err := client.GetVideoInfo(bvid)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "获取成功", Data: videoInfo}.Write(w)
|
||||
}
|
||||
|
||||
// getSeasonInfo 通过 EP 号或 SS 号获取视频信息
|
||||
func getSeasonInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ParseForm() != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
var epid int
|
||||
epid, err := strconv.Atoi(r.FormValue("epid"))
|
||||
if r.FormValue("epid") != "" && err != nil {
|
||||
util.Res{Success: false, Message: "epid 格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
var ssid int
|
||||
if epid == 0 {
|
||||
ssid, err = strconv.Atoi(r.FormValue("ssid"))
|
||||
if r.FormValue("ssid") != "" && err != nil {
|
||||
util.Res{Success: false, Message: "ssid 格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil || sessdata == "" {
|
||||
res_error.Send(w, res_error.NotLogin)
|
||||
return
|
||||
}
|
||||
|
||||
client := bilibili.BiliClient{SESSDATA: sessdata}
|
||||
seasonInfo, err := client.GetSeasonInfo(epid, ssid)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "获取成功", Data: seasonInfo}.Write(w)
|
||||
}
|
||||
|
||||
// getPlayInfo 通过 BVID 和 CID 获取视频播放信息
|
||||
func getPlayInfo(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ParseForm() != nil {
|
||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
|
||||
bvid := r.FormValue("bvid")
|
||||
if !util.CheckBvidFormat(bvid) {
|
||||
util.Res{Success: false, Message: "bvid 格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
cid, err := strconv.Atoi(r.FormValue("cid"))
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: "cid 格式错误"}.Write(w)
|
||||
return
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil || sessdata == "" {
|
||||
res_error.Send(w, res_error.NotLogin)
|
||||
return
|
||||
}
|
||||
client := bilibili.BiliClient{SESSDATA: sessdata}
|
||||
playInfo, err := client.GetPlayInfo(bvid, cid)
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: fmt.Sprintf("client.GetPlayInfo: %v", err)}.Write(w)
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "获取成功", Data: playInfo}.Write(w)
|
||||
}
|
||||
|
||||
func getPopularVideos(w http.ResponseWriter, r *http.Request) {
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil || sessdata == "" {
|
||||
res_error.Send(w, res_error.NotLogin)
|
||||
return
|
||||
}
|
||||
|
||||
client := bilibili.BiliClient{SESSDATA: sessdata}
|
||||
videos, err := client.GetPopularVideos()
|
||||
if err != nil {
|
||||
util.Res{Success: false, Message: err.Error()}.Write(w)
|
||||
return
|
||||
}
|
||||
bvidList := make([]string, 0)
|
||||
for _, v := range videos {
|
||||
bvidList = append(bvidList, v.Bvid)
|
||||
}
|
||||
util.Res{Success: true, Message: "获取成功", Data: bvidList}.Write(w)
|
||||
}
|
||||
|
||||
var downloadVideo = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Query().Get("path")
|
||||
safePath := filepath.Clean(path)
|
||||
safePath = strings.ReplaceAll(safePath, "\\", "/")
|
||||
http.ServeFile(w, r, safePath)
|
||||
})
|
||||
|
||||
var getSeasonsArchivesListFirstBvid = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var mid int
|
||||
var seasonId int
|
||||
var err error
|
||||
if mid, err = strconv.Atoi(r.URL.Query().Get("mid")); err != nil {
|
||||
res_error.Send(w, res_error.MidFormatError)
|
||||
return
|
||||
}
|
||||
if seasonId, err = strconv.Atoi(r.URL.Query().Get("seasonId")); err != nil {
|
||||
res_error.Send(w, res_error.SeasonIdFormatError)
|
||||
return
|
||||
}
|
||||
client := bilibili.BiliClient{}
|
||||
bvid, err := client.GetSeasonsArchivesListFirstBvid(mid, seasonId)
|
||||
if err != nil {
|
||||
res_error.Send(w, fmt.Sprintf("client.GetSeasonsArchivesList: %v", err))
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "获取成功", Data: bvid}.Write(w)
|
||||
})
|
||||
|
||||
var getFavList = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mediaId, err := strconv.Atoi(r.URL.Query().Get("mediaId"))
|
||||
if err != nil {
|
||||
res_error.Send(w, res_error.ParamError)
|
||||
return
|
||||
}
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil || sessdata == "" {
|
||||
res_error.Send(w, res_error.NotLogin)
|
||||
return
|
||||
}
|
||||
client := bilibili.BiliClient{SESSDATA: sessdata}
|
||||
favList, err := client.GetFavlist(mediaId)
|
||||
if err != nil {
|
||||
res_error.Send(w, err.Error())
|
||||
return
|
||||
}
|
||||
util.Res{Success: true, Message: "获取成功", Data: favList}.Write(w)
|
||||
})
|
||||
@@ -0,0 +1,501 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"bilidown/bilibili"
|
||||
"bilidown/common"
|
||||
"bilidown/util"
|
||||
)
|
||||
|
||||
// TaskInitOption 创建任务时需要从 POST 请求获取的参数
|
||||
type TaskInitOption struct {
|
||||
Bvid string `json:"bvid"`
|
||||
Cid int `json:"cid"`
|
||||
Format common.MediaFormat `json:"format"`
|
||||
Title string `json:"title"`
|
||||
Owner string `json:"owner"`
|
||||
Cover string `json:"cover"`
|
||||
Status TaskStatus `json:"status"`
|
||||
Folder string `json:"folder"`
|
||||
Audio string `json:"audio"`
|
||||
Video string `json:"video"`
|
||||
Duration int `json:"duration"`
|
||||
DownloadType string `json:"downloadType"`
|
||||
}
|
||||
|
||||
// TaskInDB 任务数据库中的数据
|
||||
type TaskInDB struct {
|
||||
TaskInitOption
|
||||
ID int64 `json:"id"`
|
||||
CreateAt time.Time `json:"createAt"`
|
||||
}
|
||||
|
||||
func (task *TaskInDB) FilePath() string {
|
||||
ext := ".mp4"
|
||||
if task.DownloadType == "audio" {
|
||||
ext = ".m4a"
|
||||
}
|
||||
return filepath.Join(task.Folder,
|
||||
fmt.Sprintf("%s %s%s", task.Title,
|
||||
strings.Replace(base64.StdEncoding.EncodeToString([]byte(strconv.FormatInt(task.ID, 10))), "=", "", -1),
|
||||
ext,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// done | waiting | running | error
|
||||
type TaskStatus string
|
||||
|
||||
type Task struct {
|
||||
TaskInDB
|
||||
AudioProgress float64 `json:"audioProgress"`
|
||||
VideoProgress float64 `json:"videoProgress"`
|
||||
MergeProgress float64 `json:"mergeProgress"`
|
||||
}
|
||||
|
||||
var GlobalTaskList = []*Task{}
|
||||
var GlobalTaskMux = &sync.Mutex{}
|
||||
var GlobalDownloadSem = util.NewSemaphore(3)
|
||||
var GlobalMergeSem = util.NewSemaphore(3)
|
||||
|
||||
func (task *Task) Create(db *sql.DB) error {
|
||||
util.SqliteLock.Lock()
|
||||
result, err := db.Exec(`INSERT INTO "task" ("bvid", "cid", "format", "title", "owner", "cover", "status", "folder", "duration", "download_type")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
task.Bvid,
|
||||
task.Cid,
|
||||
task.Format,
|
||||
task.Title,
|
||||
task.Owner,
|
||||
task.Cover,
|
||||
task.Status,
|
||||
task.Folder,
|
||||
task.Duration,
|
||||
task.DownloadType,
|
||||
)
|
||||
util.SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task.ID, err = result.LastInsertId()
|
||||
task.CreateAt = time.Now()
|
||||
return err
|
||||
}
|
||||
|
||||
// Create 创建任务,并将任务加入全局任务列表
|
||||
func (task *Task) Start() {
|
||||
if task.DownloadType == "" {
|
||||
task.DownloadType = "merge"
|
||||
}
|
||||
GlobalTaskMux.Lock()
|
||||
GlobalTaskList = append(GlobalTaskList, task)
|
||||
GlobalTaskMux.Unlock()
|
||||
db := util.MustGetDB()
|
||||
defer db.Close()
|
||||
sessdata, err := bilibili.GetSessdata(db)
|
||||
if err != nil {
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("bilibili.GetSessdata: %v", err))
|
||||
return
|
||||
}
|
||||
client := &bilibili.BiliClient{SESSDATA: sessdata}
|
||||
|
||||
GlobalDownloadSem.Acquire()
|
||||
task.UpdateStatus(db, "running")
|
||||
|
||||
if task.DownloadType == "audio" {
|
||||
// 仅音频模式:只下载音频,重命名音频文件为输出文件
|
||||
err = DownloadMedia(client, task.Audio, task, "audio")
|
||||
if err != nil {
|
||||
GlobalDownloadSem.Release()
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err))
|
||||
return
|
||||
}
|
||||
GlobalDownloadSem.Release()
|
||||
outputPath := task.TaskInDB.FilePath()
|
||||
audioPath := filepath.Join(task.Folder, strconv.FormatInt(task.ID, 10)+".audio")
|
||||
err = os.Rename(audioPath, outputPath)
|
||||
if err != nil {
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("os.Rename: %v", err))
|
||||
return
|
||||
}
|
||||
// 添加元数据
|
||||
if err := task.addMetadata(outputPath); err != nil {
|
||||
log.Printf("添加元数据失败 (任务ID: %d): %v", task.ID, err)
|
||||
}
|
||||
task.UpdateStatus(db, "done")
|
||||
return
|
||||
} else if task.DownloadType == "video" {
|
||||
// 仅视频模式:只下载视频,重命名视频文件为输出文件
|
||||
err = DownloadMedia(client, task.Video, task, "video")
|
||||
if err != nil {
|
||||
GlobalDownloadSem.Release()
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err))
|
||||
return
|
||||
}
|
||||
GlobalDownloadSem.Release()
|
||||
outputPath := task.TaskInDB.FilePath()
|
||||
videoPath := filepath.Join(task.Folder, strconv.FormatInt(task.ID, 10)+".video")
|
||||
err = os.Rename(videoPath, outputPath)
|
||||
if err != nil {
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("os.Rename: %v", err))
|
||||
return
|
||||
}
|
||||
// 添加元数据
|
||||
if err := task.addMetadata(outputPath); err != nil {
|
||||
log.Printf("添加元数据失败 (任务ID: %d): %v", task.ID, err)
|
||||
}
|
||||
task.UpdateStatus(db, "done")
|
||||
return
|
||||
} else {
|
||||
// 合并模式:下载音频和视频,然后合并
|
||||
err = DownloadMedia(client, task.Audio, task, "audio")
|
||||
if err != nil {
|
||||
GlobalDownloadSem.Release()
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err))
|
||||
return
|
||||
}
|
||||
err = DownloadMedia(client, task.Video, task, "video")
|
||||
if err != nil {
|
||||
GlobalDownloadSem.Release()
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("DownloadMedia: %v", err))
|
||||
return
|
||||
}
|
||||
GlobalDownloadSem.Release()
|
||||
|
||||
outputPath := task.TaskInDB.FilePath()
|
||||
videoPath := filepath.Join(task.Folder, strconv.FormatInt(task.ID, 10)+".video")
|
||||
audioPath := filepath.Join(task.Folder, strconv.FormatInt(task.ID, 10)+".audio")
|
||||
GlobalMergeSem.Acquire()
|
||||
err = task.MergeMedia(outputPath, videoPath, audioPath)
|
||||
if err != nil {
|
||||
GlobalMergeSem.Release()
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("task.MergeMedia: %v", err))
|
||||
return
|
||||
}
|
||||
err = os.Remove(videoPath)
|
||||
if err != nil {
|
||||
GlobalMergeSem.Release()
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("os.Remove: %v", err))
|
||||
return
|
||||
}
|
||||
err = os.Remove(audioPath)
|
||||
if err != nil {
|
||||
GlobalMergeSem.Release()
|
||||
task.UpdateStatus(db, "error", fmt.Errorf("os.Remove: %v", err))
|
||||
return
|
||||
}
|
||||
GlobalMergeSem.Release()
|
||||
// 添加元数据
|
||||
if err := task.addMetadata(outputPath); err != nil {
|
||||
log.Printf("添加元数据失败 (任务ID: %d): %v", task.ID, err)
|
||||
}
|
||||
task.UpdateStatus(db, "done")
|
||||
}
|
||||
}
|
||||
|
||||
// 合并音视频
|
||||
func (task *Task) MergeMedia(outputPath string, inputPaths ...string) error {
|
||||
inputs := []string{}
|
||||
for _, path := range inputPaths {
|
||||
inputs = append(inputs, "-i", path)
|
||||
}
|
||||
|
||||
ffmpegPath, err := util.GetFFmpegPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command(ffmpegPath, append(inputs, "-c:v", "copy", "-c:a", "copy", "-progress", "pipe:1", "-strict", "-2", outputPath)...)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
|
||||
progress := newProgressBar(int64(task.Duration))
|
||||
outTimeRegex := regexp.MustCompile(`out_time_ms=(\d+)`) // 毫秒
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
match := outTimeRegex.FindStringSubmatch(line)
|
||||
if len(match) == 2 {
|
||||
outTime, err := strconv.ParseInt(match[1], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
progress.current = outTime / 1000000
|
||||
task.MergeProgress = progress.percent()
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
task.MergeProgress = 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetVideoURL(medias []bilibili.Media, format common.MediaFormat) (string, error) {
|
||||
for _, code := range []int{12, 7, 13} {
|
||||
for _, item := range medias {
|
||||
if item.ID == format && item.Codecid == code {
|
||||
return item.BaseURL, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("未找到对应视频分辨率格式")
|
||||
}
|
||||
|
||||
func GetAudioURL(dash *bilibili.Dash) string {
|
||||
if dash.Flac != nil {
|
||||
return dash.Flac.Audio.BaseURL
|
||||
}
|
||||
var maxAudioID common.MediaFormat
|
||||
var audioURL string
|
||||
for _, item := range dash.Audio {
|
||||
if item.ID > maxAudioID {
|
||||
maxAudioID = item.ID
|
||||
audioURL = item.BaseURL
|
||||
}
|
||||
}
|
||||
return audioURL
|
||||
}
|
||||
|
||||
func (task *Task) UpdateStatus(db *sql.DB, status TaskStatus, errs ...error) error {
|
||||
util.SqliteLock.Lock()
|
||||
_, err := db.Exec(`UPDATE "task" SET "status" = ? WHERE "id" = ?`, status, task.ID)
|
||||
util.SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
err = util.CreateLog(db, fmt.Sprintf("Task-%d-Error: %v", task.ID, err))
|
||||
if err != nil {
|
||||
log.Fatalln("CreateLog:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
task.Status = status
|
||||
return err
|
||||
}
|
||||
|
||||
func DownloadMedia(client *bilibili.BiliClient, _url string, task *Task, mediaType string) error {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
resp, err = client.SimpleGET(_url, nil)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := strconv.FormatInt(task.ID, 10) + "." + mediaType
|
||||
filepath := filepath.Join(task.Folder, filename)
|
||||
|
||||
progress := newProgressBar(resp.ContentLength)
|
||||
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
reader := io.TeeReader(resp.Body, file)
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
progress.add(n)
|
||||
GlobalTaskMux.Lock()
|
||||
if mediaType == "video" {
|
||||
task.VideoProgress = progress.percent()
|
||||
} else {
|
||||
task.AudioProgress = progress.percent()
|
||||
}
|
||||
GlobalTaskMux.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type progressBar struct {
|
||||
total int64
|
||||
current int64
|
||||
}
|
||||
|
||||
func (p *progressBar) add(n int) {
|
||||
p.current += int64(n)
|
||||
}
|
||||
|
||||
func (p *progressBar) percent() float64 {
|
||||
return float64(p.current) / float64(p.total)
|
||||
}
|
||||
|
||||
func newProgressBar(total int64) *progressBar {
|
||||
return &progressBar{
|
||||
total: total,
|
||||
}
|
||||
}
|
||||
|
||||
func GetTaskList(db *sql.DB, page int, pageSize int) ([]TaskInDB, error) {
|
||||
tasks := []TaskInDB{}
|
||||
util.SqliteLock.Lock()
|
||||
rows, err := db.Query(`SELECT
|
||||
"id", "bvid", "cid", "format", "title",
|
||||
"owner", "cover", "status", "folder", "duration", "download_type", "create_at"
|
||||
FROM "task" ORDER BY "id" DESC LIMIT ?, ?`,
|
||||
page*pageSize, pageSize,
|
||||
)
|
||||
util.SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createAt := ""
|
||||
|
||||
for rows.Next() {
|
||||
task := TaskInDB{}
|
||||
err = rows.Scan(
|
||||
&task.ID,
|
||||
&task.Bvid,
|
||||
&task.Cid,
|
||||
&task.Format,
|
||||
&task.Title,
|
||||
&task.Owner,
|
||||
&task.Cover,
|
||||
&task.Status,
|
||||
&task.Folder,
|
||||
&task.Duration,
|
||||
&task.DownloadType,
|
||||
&createAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task.CreateAt, err = time.Parse("2006-01-02 15:04:05", createAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func DeleteTask(db *sql.DB, taskID int) error {
|
||||
util.SqliteLock.Lock()
|
||||
_, err := db.Exec(`DELETE FROM "task" WHERE "id" = ?`, taskID)
|
||||
util.SqliteLock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func GetTask(db *sql.DB, taskID int) (*TaskInDB, error) {
|
||||
task := TaskInDB{}
|
||||
createAt := ""
|
||||
util.SqliteLock.Lock()
|
||||
err := db.QueryRow(`SELECT
|
||||
"id", "bvid", "cid", "format", "title",
|
||||
"owner", "cover", "status", "folder", "duration", "download_type", "create_at"
|
||||
FROM "task" WHERE "id" = ?`,
|
||||
taskID,
|
||||
).Scan(
|
||||
&task.ID,
|
||||
&task.Bvid,
|
||||
&task.Cid,
|
||||
&task.Format,
|
||||
&task.Title,
|
||||
&task.Owner,
|
||||
&task.Cover,
|
||||
&task.Status,
|
||||
&task.Folder,
|
||||
&task.Duration,
|
||||
&task.DownloadType,
|
||||
&createAt,
|
||||
)
|
||||
util.SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.CreateAt, err = time.Parse("2006-01-02 15:04:05", createAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
// addMetadata 使用 ffmpeg 给输出文件添加元数据(description 和 artist)
|
||||
func (task *Task) addMetadata(filePath string) error {
|
||||
ffmpegPath, err := util.GetFFmpegPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc := task.Bvid
|
||||
if desc == "" {
|
||||
desc = ""
|
||||
}
|
||||
|
||||
author := task.Owner
|
||||
|
||||
// 临时文件加上 .mp4 扩展名
|
||||
tempPath := filePath + ".tmp.mp4"
|
||||
|
||||
// 使用双引号包裹文件路径,避免特殊字符
|
||||
cmd := exec.Command(ffmpegPath,
|
||||
"-i", filePath,
|
||||
"-metadata", "description="+desc,
|
||||
"-metadata", "artist="+author,
|
||||
"-codec", "copy",
|
||||
"-y",
|
||||
tempPath,
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("ffmpeg添加元数据失败: %v, 输出: %s", err, string(output))
|
||||
}
|
||||
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
return fmt.Errorf("删除原文件失败: %v", err)
|
||||
}
|
||||
if err := os.Rename(tempPath, filePath); err != nil {
|
||||
return fmt.Errorf("重命名临时文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFFMPEG(t *testing.T) {
|
||||
cmd := exec.Command("ffmpeg", "-i", `E:\bilidown\27.video`, "-i", `E:\bilidown\27.audio`, "-c:v", "copy", "-c:a", "copy", "-progress", "pipe:1", `E:\bilidown\27.mp4`)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
fmt.Println(">>>", scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("done")
|
||||
}
|
||||
|
||||
func TestNum(t *testing.T) {
|
||||
t.Log(int64(100999) / 1000)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func CreateLog(db *sql.DB, content string) error {
|
||||
SqliteLock.Lock()
|
||||
_, err := db.Exec(`INSERT INTO "log" ("content") VALUES (?)`, content)
|
||||
SqliteLock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func GetFields(db *sql.DB, names ...string) (map[string]string, error) {
|
||||
|
||||
if len(names) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
placeholders := make([]string, len(names))
|
||||
for i := 0; i < len(names); i++ {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
query := fmt.Sprintf(`SELECT "name", "value" FROM "field" WHERE "name" IN (%s)`, strings.Join(placeholders, ","))
|
||||
|
||||
values := make([]interface{}, len(names))
|
||||
for i := 0; i < len(names); i++ {
|
||||
values[i] = names[i]
|
||||
}
|
||||
SqliteLock.Lock()
|
||||
row, err := db.Query(query, values...)
|
||||
SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer row.Close()
|
||||
var name, value string
|
||||
fields := make(map[string]string)
|
||||
for row.Next() {
|
||||
if err := row.Scan(&name, &value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields[name] = value
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func SaveFields(db *sql.DB, data [][2]string) error {
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
|
||||
stmt, err := tx.Prepare(`INSERT OR REPLACE INTO "field" ("name", "value") VALUES (?, ?)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, d := range data {
|
||||
SqliteLock.Lock()
|
||||
_, err = stmt.Exec(d[0], d[1])
|
||||
SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentFolder 获取数据库中的下载保存路径,如果不存在则将默认路径保存到数据库
|
||||
func GetCurrentFolder(db *sql.DB) (string, error) {
|
||||
var folder string
|
||||
SqliteLock.Lock()
|
||||
err := db.QueryRow(`SELECT "value" FROM "field" WHERE "name" = 'download_folder'`).Scan(&folder)
|
||||
SqliteLock.Unlock()
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
folder, err = GetDefaultDownloadFolder()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.MkdirAll(folder, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = SaveDownloadFolder(db, folder)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return folder, nil
|
||||
}
|
||||
err = os.MkdirAll(folder, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
// SaveDownloadFolder 保存下载路径,不存在则自动创建
|
||||
func SaveDownloadFolder(db *sql.DB, downloadFolder string) error {
|
||||
_, err := os.Stat(downloadFolder)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(downloadFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
SqliteLock.Lock()
|
||||
_, err = db.Exec(`INSERT OR REPLACE INTO "field" ("name", "value") VALUES ('download_folder', ?)`, downloadFolder)
|
||||
SqliteLock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
var SqliteLock sync.Mutex
|
||||
|
||||
func MustGetDB(path ...string) *sql.DB {
|
||||
pathStr := ""
|
||||
if len(path) == 0 {
|
||||
pathStr = "./data.db"
|
||||
} else if len(path) > 1 {
|
||||
log.Fatalln(errors.New("len(path) <= 1"))
|
||||
} else {
|
||||
pathStr = path[0]
|
||||
}
|
||||
db, err := sql.Open("sqlite", pathStr)
|
||||
if err != nil {
|
||||
log.Fatalln("sql.Open:", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package util
|
||||
|
||||
type FieldUtil struct{}
|
||||
|
||||
func (f FieldUtil) AllowSelect() []string {
|
||||
return []string{
|
||||
"download_folder",
|
||||
}
|
||||
}
|
||||
|
||||
func (f FieldUtil) AllowUpdate() []string {
|
||||
return []string{
|
||||
"download_folder",
|
||||
}
|
||||
}
|
||||
|
||||
func (f FieldUtil) IsAllow(allFields []string, names ...string) bool {
|
||||
allowedFields := make(map[string]struct{})
|
||||
for _, field := range allFields {
|
||||
allowedFields[field] = struct{}{}
|
||||
}
|
||||
for _, name := range names {
|
||||
if _, exists := allowedFields[name]; !exists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f FieldUtil) IsAllowSelect(names ...string) bool {
|
||||
return f.IsAllow(f.AllowSelect(), names...)
|
||||
}
|
||||
|
||||
func (f FieldUtil) IsAllowUpdate(names ...string) bool {
|
||||
return f.IsAllow(f.AllowUpdate(), names...)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package res_error
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bilidown/util"
|
||||
)
|
||||
|
||||
// Send 发送异常响应
|
||||
func Send(w http.ResponseWriter, message string) {
|
||||
util.Res{Message: message, Success: false}.Write(w)
|
||||
}
|
||||
|
||||
const (
|
||||
BvidFormatError = "错误的 Bvid 格式"
|
||||
URLFormatError = "错误的 URL 格式"
|
||||
MidFormatError = "错误的 Mid 格式"
|
||||
SeasonIdFormatError = "错误的 SeasonId 格式"
|
||||
ParamError = "参数错误"
|
||||
MethodNotAllowError = "不允许的请求方式"
|
||||
NoLocationError = "无重定向目标地址"
|
||||
FileNotFountError = "文件不存在"
|
||||
FileTypeNotAllowError = "不允许的文件类型"
|
||||
SystemError = "系统错误"
|
||||
NotLogin = "未登录"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 统一的 JSON 响应结构
|
||||
type Res struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
func (r Res) Write(w http.ResponseWriter) {
|
||||
bs, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
w.Write([]byte(`{"success":false,"message":"系统错误","data":null}`))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(bs)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package util
|
||||
|
||||
import "sync"
|
||||
|
||||
type Semaphore struct {
|
||||
ch chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewSemaphore(concurrency int) *Semaphore {
|
||||
return &Semaphore{
|
||||
ch: make(chan struct{}, concurrency),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Semaphore) Acquire() {
|
||||
s.ch <- struct{}{}
|
||||
s.wg.Add(1)
|
||||
}
|
||||
|
||||
func (s *Semaphore) Release() {
|
||||
<-s.ch
|
||||
s.wg.Done()
|
||||
}
|
||||
|
||||
func (s *Semaphore) Wait() {
|
||||
s.wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"bilidown/common"
|
||||
)
|
||||
|
||||
func CheckBvidFormat(bvid string) bool {
|
||||
return regexp.MustCompile("^BV1[a-zA-Z0-9]+").MatchString(bvid)
|
||||
}
|
||||
|
||||
// GetDefaultDownloadFolder 获取默认下载路径
|
||||
func GetDefaultDownloadFolder() (string, error) {
|
||||
return filepath.Abs("./download")
|
||||
}
|
||||
|
||||
func IsNumber(str string) bool {
|
||||
_, err := strconv.Atoi(str)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsValidURL 判断字符串是否为合法的URL
|
||||
func IsValidURL(u string) bool {
|
||||
_, err := url.ParseRequestURI(u)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsValidFormatCode 判断格式码是否合法
|
||||
func IsValidFormatCode(format common.MediaFormat) bool {
|
||||
allowed := []common.MediaFormat{6, 16, 32, 64, 74, 80, 112, 116, 120, 125, 126, 127}
|
||||
for _, v := range allowed {
|
||||
if v == format {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FilterFileName 过滤字符串中的特殊字符,使其允许作为文件名。
|
||||
func FilterFileName(fileName string) string {
|
||||
return regexp.MustCompile(`[\\/:*?"<>|\n]`).ReplaceAllString(fileName, "")
|
||||
}
|
||||
|
||||
// GetFFmpegPath 获取可用的 FFmpeg 执行路径。
|
||||
func GetFFmpegPath() (string, error) {
|
||||
if err := exec.Command("ffmpeg", "-version").Run(); err == nil {
|
||||
return "ffmpeg", nil
|
||||
}
|
||||
if err := exec.Command("bin/ffmpeg", "-version").Run(); err == nil {
|
||||
return "bin/ffmpeg", nil
|
||||
}
|
||||
return "", errors.New("ffmpeg not found")
|
||||
}
|
||||
|
||||
// GetRedirectedLocation 获取响应头中的 Location,不会自动跟随重定向。
|
||||
func GetRedirectedLocation(url string) (string, error) {
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
request, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if locationURL, err := response.Location(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return locationURL.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func MD5Hash(str string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(str))
|
||||
hash := hasher.Sum(nil)
|
||||
hashString := hex.EncodeToString(hash)
|
||||
return hashString
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"bilidown/common"
|
||||
"bilidown/util"
|
||||
)
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
for i := 4; i < 10; i++ {
|
||||
for j := 0; j < 3; j++ {
|
||||
str := common.RandomString(i)
|
||||
t.Log(str)
|
||||
}
|
||||
t.Log("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRedirectedLocation(t *testing.T) {
|
||||
os.Setenv("https_proxy", "http://192.168.1.5:9000")
|
||||
if location, err := util.GetRedirectedLocation("https://b23.tv/Ga6sbzT"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
fmt.Println(location)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user