diff --git a/.github/RELEASE-TEMPLATE.md b/.github/RELEASE-TEMPLATE.md new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8610549 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +on: + push: + # Sequence of patterns matched against refs/tags + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 +#on: +# push: +# branches: [ "master" ] +# pull_request: +# branches: [ "master" ] +name: Latest Release + +jobs: + build: + name: GoReleaser build + runs-on: ubuntu-latest + + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + with: + fetch-depth: 0 # See: https://goreleaser.com/ci/actions/ + + - name: Set up Go 1.21.4 + uses: actions/setup-go@v2 + with: + go-version: 1.21.4 + id: go + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --clean +# env: +# GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 20f4db1..7fcc5c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,9 @@ -vendor -test +downloads .idea -done -/BS4/PHUB/data/ -/BS4/XM/data/ -GB/ -testfile -.fleet -report.md -Solution.sh -__pycache__ -users.txt +.DS_Store +vendor *.mp4 *.m4s +entry.json *.log -*.zip -*.rpm -*.deb -*.blockmap -*.exe -*.jpg -*.png -*.mp3 -*.amr -*.html -.DS_Store -#忽略产生的中间文件 -info.txt -list.txt -build -dist -AVMerger4Win64.exe -AVMerger4Win32.exe -AVMerger4Raspi -AVMerger4Mac -AVMerger4M1 -AVMerger4Linux32 -AVMerger4Linux64 -AVMerger4Android -AVMerger4Raspi64 -*.json -*.xml -*.m4s -report.txt \ No newline at end of file +*.db \ No newline at end of file diff --git a/conf.ini b/conf.ini new file mode 100644 index 0000000..cb94116 --- /dev/null +++ b/conf.ini @@ -0,0 +1,7 @@ +[merge] +;生成文件保存的位置 +prefix = /mnt/d/git/AVmerger/downloads +;视频源文件目录 +src = /mnt/d/git/AVmerger/downloads +[log] +level = Debug \ No newline at end of file diff --git a/example.json b/example.json new file mode 100644 index 0000000..9e6bcd4 --- /dev/null +++ b/example.json @@ -0,0 +1,46 @@ +{ + "media_type": 2, + "has_dash_audio": true, + "is_completed": true, + "total_bytes": 47675288, + "downloaded_bytes": 47675288, + "title": "蜡笔小新【人物志】特殊的一期:春日部防卫队——最强战队", + "type_tag": "80", + "cover": "http:\/\/i0.hdslb.com\/bfs\/archive\/55b5d45ee6857820890e536e8823ead726bc0cbb.jpg", + "video_quality": 80, + "prefered_video_quality": 80, + "guessed_total_bytes": 0, + "total_time_milli": 1058550, + "danmaku_count": 387, + "time_update_stamp": 1702664375269, + "time_create_stamp": 1702664199018, + "can_play_in_advance": true, + "interrupt_transform_temp_file": false, + "quality_pithy_description": "1080P", + "quality_superscript": "", + "cache_version_code": 7590200, + "preferred_audio_quality": 0, + "audio_quality": 0, + "avid": 229337132, + "spid": 0, + "seasion_id": 0, + "bvid": "", + "owner_id": 630727239, + "owner_name": "茂人小头头", + "page_data": { + "cid": 1148286376, + "page": 1, + "from": "vupload", + "part": "盘点《蜡笔小新》最强组织的发展史!永远的春日部防卫队!永远的友谊!", + "link": "", + "rich_vid": "", + "vid": "", + "has_alias": false, + "tid": 253, + "width": 1920, + "height": 1080, + "rotate": 0, + "download_title": "", + "download_subtitle": "" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 08d42c9..ba28c01 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,19 @@ module github.com/zhangyiming748/AVmerger -go 1.19 +go 1.21 require ( - github.com/zhangyiming748/replace v0.0.8 - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 + github.com/klauspost/cpuid/v2 v2.2.6 + github.com/zhangyiming748/GetFileInfo v0.0.39 + gorm.io/driver/sqlite v1.5.4 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.19 // indirect + github.com/zhangyiming748/GetAllFolder v0.0.20 // indirect + github.com/zhangyiming748/filetype v0.0.1 // indirect + golang.org/x/sys v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index aba9c42..202a5ca 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,20 @@ -github.com/zhangyiming748/replace v0.0.8 h1:MeFF+j0tq5Yy/u8VjjH5g2dxd5EOsFxSyckbZsnvbaQ= -github.com/zhangyiming748/replace v0.0.8/go.mod h1:R3otYn+0qxaHOVaz0LafMwoDjhTexJijrLgfcDyOYCQ= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/zhangyiming748/GetAllFolder v0.0.20 h1:1HR+wKCP6lYIUXXpxcmp4ba1U8n6uz5QSTgvVVqZkEE= +github.com/zhangyiming748/GetAllFolder v0.0.20/go.mod h1:uhdqD8XPoOOVj9ywdbAh0CkGaM7kJpfQEiwPjk9NSD4= +github.com/zhangyiming748/GetFileInfo v0.0.39 h1:8JN5QC7dJGJ9QQUiGri7eo5c5FiwTg1wShtrSVkSDQE= +github.com/zhangyiming748/GetFileInfo v0.0.39/go.mod h1:D76EP151kuBY59SWz0nU3XdyDEPPUyFZxHq0cJCgvj8= +github.com/zhangyiming748/filetype v0.0.1 h1:C7a49LHKzqiYAiw/u2lTmPU6gP6Nwb6/sSDBmBMoJ/k= +github.com/zhangyiming748/filetype v0.0.1/go.mod h1:DFFw06VgsQVSriUKe95IC+HKaY4u6hCCbXJE1L1HNUk= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..50a2cab --- /dev/null +++ b/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "github.com/zhangyiming748/AVmerger/merge" + "github.com/zhangyiming748/AVmerger/sql" + "github.com/zhangyiming748/AVmerger/util" + "io" + "log/slog" + "os" +) + +func init() { + setLog() + sql.SetEngine() +} +func main() { + src := util.GetVal("merge", "src") + bilibilihd := "/sdcard/Android/data/tv.danmaku.bilibilihd" + bilibili := "/sdcard/Android/data/tv.danmaku.bili" + if existFolder(bilibilihd) { + src = bilibilihd + } else if existFolder(bilibili) { + src = bilibili + } + merge.Merge(src) +} + +/* +设置程序运行的日志等级 +*/ +func setLog() { + var opt slog.HandlerOptions + level := util.GetVal("log", "level") + switch level { + case "Debug": + opt = slog.HandlerOptions{ // 自定义option + AddSource: true, + Level: slog.LevelDebug, // slog 默认日志级别是 info + } + case "Info": + opt = slog.HandlerOptions{ // 自定义option + AddSource: true, + Level: slog.LevelInfo, // slog 默认日志级别是 info + } + case "Warn": + opt = slog.HandlerOptions{ // 自定义option + AddSource: true, + Level: slog.LevelWarn, // slog 默认日志级别是 info + } + case "Err": + opt = slog.HandlerOptions{ // 自定义option + AddSource: true, + Level: slog.LevelError, // slog 默认日志级别是 info + } + default: + slog.Warn("需要正确设置环境变量 Debug,Info,Warn or Err") + slog.Debug("默认使用Debug等级") + opt = slog.HandlerOptions{ // 自定义option + AddSource: true, + Level: slog.LevelDebug, // slog 默认日志级别是 info + } + } + file := "AVmerge.log" + logf, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0770) + if err != nil { + panic(err) + } + logger := slog.New(slog.NewJSONHandler(io.MultiWriter(logf, os.Stdout), &opt)) + slog.SetDefault(logger) +} + +func existFolder(f string) bool { + _, err := os.Stat(f) + + if os.IsNotExist(err) { + fmt.Println("文件夹不存在") + return false + } else if err == nil { + fmt.Println("文件夹存在") + return true + } else { + fmt.Println("发生错误:", err) + return false + } +} diff --git a/merge.go b/merge.go deleted file mode 100644 index 102605a..0000000 --- a/merge.go +++ /dev/null @@ -1,186 +0,0 @@ -package AVmerger - -import ( - "encoding/json" - "fmt" - "golang.org/x/exp/slog" - "os" - "strings" -) - -// 最终生成前的结构体 -type Info struct { - Video string // 视频文件的绝对路径 - Audio string // 音频文件的绝对路径 - Name string // 最终文件的全名 - Del string // 标记删除的目录 -} -type Entry struct { - MediaType int `json:"media_type"` - HasDashAudio bool `json:"has_dash_audio"` - IsCompleted bool `json:"is_completed"` - TotalBytes int `json:"total_bytes"` - DownloadedBytes int `json:"downloaded_bytes"` - Title string `json:"title"` - TypeTag string `json:"type_tag"` - Cover string `json:"cover"` - VideoQuality int `json:"video_quality"` - PreferedVideoQuality int `json:"prefered_video_quality"` - GuessedTotalBytes int `json:"guessed_total_bytes"` - TotalTimeMilli int `json:"total_time_milli"` - DanmakuCount int `json:"danmaku_count"` - TimeUpdateStamp int64 `json:"time_update_stamp"` - TimeCreateStamp int64 `json:"time_create_stamp"` - CanPlayInAdvance bool `json:"can_play_in_advance"` - InterruptTransformTempFile bool `json:"interrupt_transform_temp_file"` - QualityPithyDescription string `json:"quality_pithy_description"` - QualitySuperscript string `json:"quality_superscript"` - CacheVersionCode int `json:"cache_version_code"` - PreferredAudioQuality int `json:"preferred_audio_quality"` - AudioQuality int `json:"audio_quality"` - Avid int `json:"avid"` - Spid int `json:"spid"` - SeasionId int `json:"seasion_id"` - Bvid string `json:"bvid"` - OwnerId int `json:"owner_id"` - OwnerName string `json:"owner_name"` - OwnerAvatar string `json:"owner_avatar"` - PageData struct { - Cid int `json:"cid"` - Page int `json:"page"` - From string `json:"from"` - Part string `json:"part"` - Link string `json:"link"` - RichVid string `json:"rich_vid"` - Vid string `json:"vid"` - HasAlias bool `json:"has_alias"` - Weblink string `json:"weblink"` - Offsite string `json:"offsite"` - Tid int `json:"tid"` - Width int `json:"width"` - Height int `json:"height"` - Rotate int `json:"rotate"` - DownloadTitle string `json:"download_title"` - DownloadSubtitle string `json:"download_subtitle"` - } `json:"page_data"` -} - -// todo 目录使用序数词 -// /Users/zen/Github/AVmerger/file 包括单p和多p -func AllIn(root string) { - infos := get(root) - slog.Debug("解析后", slog.Any("返回的视频", infos)) - for i, info := range *infos { - slog.Info(fmt.Sprintf("正在合并第 %d/%d 个视频\n", i+1, len(*infos))) - avc(root, info) - } -} -func AllInH265(root string) { - infos := get(root) - slog.Debug("解析后", slog.Any("返回的视频", infos)) - for i, info := range *infos { - slog.Info(fmt.Sprintf("正在合并第 %d/%d 个视频", i+1, len(*infos))) - hevc(root, info) - } -} - -func get(root string) *[]Info { - var infos []Info - vs, err := getChildDir(root) - if err != nil { - slog.Warn("错误", slog.Any("读取视频根目录", err)) - return nil - } - for _, v := range vs { - rootv := strings.Join([]string{root, v.Name()}, string(os.PathSeparator)) - p, err := getChildDir(rootv) - if err != nil { - slog.Warn("错误", slog.Any("读取视频根目录", err)) - return nil - } - for _, entry := range p { - rootvp := strings.Join([]string{rootv, entry.Name()}, string(os.PathSeparator)) - // log.Info.Println(rootvp) - entry := strings.Join([]string{rootvp, "entry.json"}, string(os.PathSeparator)) - j, err := os.ReadFile(entry) - if err != nil { - slog.Warn("错误", slog.Any("读取entry.json文件", err)) - return nil - } - var name Entry - err = json.Unmarshal(j, &name) - if err != nil { - slog.Warn("错误", slog.Any("解析entry.json文件", err)) - return nil - } - avs, err := getChildDir(rootvp) - if err != nil { - slog.Warn("错误", slog.Any("读取分p视频目录", err)) - return nil - } - for _, av := range avs { - audio := strings.Join([]string{rootvp, av.Name(), "audio.m4s"}, string(os.PathSeparator)) - video := strings.Join([]string{rootvp, av.Name(), "video.m4s"}, string(os.PathSeparator)) - info := Info{ - Video: strings.Replace(video, " ", "", -1), - Audio: strings.Replace(audio, " ", "", -1), - Name: strings.Replace(strings.Join([]string{name.Title, name.PageData.Part}, ""), "|", "", -1), - Del: rootvp, - } - slog.Debug("一个完整视频的基本信息", slog.Any("视频", info.Video), slog.Any("音频", info.Audio), slog.Any("文件名", info.Name), slog.Any("删除后不会影响其他视频的目录", info.Del)) - infos = append(infos, info) - } - } - } - return &infos -} - -/* -获取子目录 -*/ -func getChildDir(dir string) ([]os.DirEntry, error) { - var cDir []os.DirEntry - readDir, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - for _, child := range readDir { - if strings.HasPrefix(child.Name(), ".") { - slog.Info("跳过隐藏文件夹", slog.Any("文件名", child.Name())) - continue - } - if !child.IsDir() { - // log.Info.Printf("跳过文件:%v\n", child.Name()) - slog.Info("跳过文件", slog.Any("文件名", child.Name())) - continue - } - cDir = append(cDir, child) - } - return cDir, err -} - -// todo 我有一个业务是对字符串中指定重复字符去重,我看网上都是利用map实现,所以自己写了一个,但是感觉不够优雅,你有更好的方式吗 -/* -s: 原字符串 -dup: 需要被去重的字符 -*/ -func Duplicate(s string, dup byte) string { - sb := []byte(s) - var nb []byte - for i := 0; i < len(sb); i++ { - if i == 0 { - // 如果是第一个字符,直接原样写入字节数组 - nb = append(nb, sb[i]) - } else { - // 如果不是第一个字符 - if sb[i] == dup && sb[i-1] == dup { - //如果本身和前一个字符都是dup则跳过 - continue - } else { - //否则写入新字节数组 - nb = append(nb, sb[i]) - } - } - } - return string(nb) -} diff --git a/merge/merge.go b/merge/merge.go new file mode 100644 index 0000000..ee354ed --- /dev/null +++ b/merge/merge.go @@ -0,0 +1,239 @@ +package merge + +import ( + "encoding/json" + "fmt" + "github.com/zhangyiming748/AVmerger/replace" + "github.com/zhangyiming748/AVmerger/sql" + "github.com/zhangyiming748/AVmerger/util" + "github.com/zhangyiming748/GetFileInfo" + "github.com/zhangyiming748/GetFileInfo/mediaInfo" + "log/slog" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "time" +) + +type Entry struct { + MediaType int `json:"media_type"` + HasDashAudio bool `json:"has_dash_audio"` + IsCompleted bool `json:"is_completed"` + TotalBytes int `json:"total_bytes"` + DownloadedBytes int `json:"downloaded_bytes"` + Title string `json:"title"` + TypeTag string `json:"type_tag"` + Cover string `json:"cover"` + VideoQuality int `json:"video_quality"` + PreferedVideoQuality int `json:"prefered_video_quality"` + GuessedTotalBytes int `json:"guessed_total_bytes"` + TotalTimeMilli int `json:"total_time_milli"` + DanmakuCount int `json:"danmaku_count"` + TimeUpdateStamp int64 `json:"time_update_stamp"` + TimeCreateStamp int64 `json:"time_create_stamp"` + CanPlayInAdvance bool `json:"can_play_in_advance"` + InterruptTransformTempFile bool `json:"interrupt_transform_temp_file"` + QualityPithyDescription string `json:"quality_pithy_description"` + QualitySuperscript string `json:"quality_superscript"` + CacheVersionCode int `json:"cache_version_code"` + PreferredAudioQuality int `json:"preferred_audio_quality"` + AudioQuality int `json:"audio_quality"` + Avid int `json:"avid"` + Spid int `json:"spid"` + SeasionId int `json:"seasion_id"` + Bvid string `json:"bvid"` + OwnerId int `json:"owner_id"` + OwnerName string `json:"owner_name"` + OwnerAvatar string `json:"owner_avatar"` + PageData struct { + Cid int `json:"cid"` + Page int `json:"page"` + From string `json:"from"` + Part string `json:"part"` + Link string `json:"link"` + RichVid string `json:"rich_vid"` + Vid string `json:"vid"` + HasAlias bool `json:"has_alias"` + Weblink string `json:"weblink"` + Offsite string `json:"offsite"` + Tid int `json:"tid"` + Width int `json:"width"` + Height int `json:"height"` + Rotate int `json:"rotate"` + DownloadTitle string `json:"download_title"` + DownloadSubtitle string `json:"download_subtitle"` + } `json:"page_data"` +} + +func Merge(rootPath string) { + roots := getall(rootPath) + slog.Debug("根目录", slog.Any("roots", roots)) + for _, root := range roots { + slog.Info("1", slog.String("1", root)) + secs := getall(root) + for _, sec := range secs { + slog.Info("2", slog.String("2", sec)) + entry := strings.Join([]string{sec, "entry.json"}, string(os.PathSeparator)) + name := getName(entry) + name = CutName(name) + slog.Info("entry", slog.String("获取到的文件名", name)) + thirds := getall(sec) + for _, third := range thirds { + slog.Info("3", slog.String("3", third)) + video := strings.Join([]string{third, "video.m4s"}, string(os.PathSeparator)) + audio := strings.Join([]string{third, "audio.m4s"}, string(os.PathSeparator)) + fname := strings.Join([]string{name, "mp4"}, ".") + prefix := util.GetVal("merge", "prefix") + if isExist(prefix) { + aim := strings.Join([]string{prefix, "bili"}, string(os.PathSeparator)) + os.Mkdir(aim, 0777) + fname = strings.Join([]string{aim, fname}, string(os.PathSeparator)) + } else { + slog.Warn("目标文件夹不存在,退出") + os.Exit(-1) + } + if isFileExist(fname) { + perfix := strings.Replace(fname, ".mp4", "", 1) + middle := strings.Join([]string{perfix, time.Now().Format("20060102")}, "-") + fname = strings.Join([]string{middle, "mp4"}, ".") + } + slog.Info("命令执行前最终名称", slog.String("文件名", fname), slog.String("视频", video), slog.String("音频", audio)) + vInfo := GetFileInfo.GetFileInfo(video) + mi, ok := vInfo.MediaInfo.(mediaInfo.VideoInfo) + if ok { + slog.Debug("断言视频mediainfo结构体成功", slog.Any("MediainfoVideo结构体", mi)) + } else { + slog.Warn("断言视频mediainfo结构体失败") + } + slog.Info("WARNING", slog.String("vTAG", mi.VideoCodecID)) + cmd := exec.Command("ffmpeg", "-i", video, "-i", audio, "-c:v", "copy", "-c:a", "copy", "-ac", "1", "-tag:v", "hvc1", fname) + if mi.VideoCodecID == "avc1" { + cmd = exec.Command("ffmpeg", "-i", video, "-i", audio, "-c:v", "copy", "-c:a", "copy", "-ac", "1", fname) + } + err := util.ExecCommand(cmd) + if err != nil { + slog.Warn("哔哩哔哩合成出错", slog.Any("错误原文", err), slog.Any("命令原文", fmt.Sprint(cmd))) + continue + } + if err = os.RemoveAll(sec); err != nil { + slog.Debug("删除失败", slog.String("目录名", sec), slog.Any("错误原文", err)) + return + } else { + slog.Debug("删除成功", slog.String("目录名", sec)) + } + } + } + } +} + +func isDir(path string) bool { + fileInfo, _ := os.Stat(path) + if fileInfo.IsDir() { + return true + } else { + return false + } +} + +func getall(rootPath string) (realFolders []string) { + folders, _ := os.ReadDir(rootPath) + for _, folder := range folders { + folderPath := strings.Join([]string{rootPath, folder.Name()}, string(os.PathSeparator)) + if isDir(folderPath) { + realFolders = append(realFolders, folderPath) + } + } + return realFolders +} + +/* +解析并返回文件名和entry原始文件 +*/ +func getName(jackson string) (name string) { + var entry Entry + file, err := os.ReadFile(jackson) + if err != nil { + return + } + err = json.Unmarshal(file, &entry) + + record := new(sql.Bili) + record.Title = entry.Title + record.Cover = strings.Replace(entry.Cover, "\\/", "//", -1) + record.CreatedAt = sql.S2T(strconv.FormatInt(entry.TimeCreateStamp, 10)) + record.UpdatedAt = sql.S2T(strconv.FormatInt(entry.TimeUpdateStamp, 10)) + record.Owner = entry.OwnerName + record.PartName = entry.PageData.Part + record.Original = string(file) + record.SetOne() + + if err != nil { + return + } + name = strings.Join([]string{entry.PageData.Part, entry.Title}, "") + name = replace.ForFileName(name) + slog.Debug("解析之后拼接", slog.String("名称", name)) + return name +} + +/* +判断路径是否存在 +*/ +func isExist(path string) bool { + if _, err := os.Stat(path); err == nil { + fmt.Println("路径存在") + return true + } else if os.IsNotExist(err) { + fmt.Println("路径不存在") + return false + } else { + fmt.Println("发生错误:", err) + return false + } +} + +/* +判断文件是否存在 +*/ +func isFileExist(fp string) bool { + if _, err := os.Stat(fp); os.IsNotExist(err) { + return false + } else { + return true + } +} + +/* +截取合理长度的标题 +*/ +func CutName(before string) (after string) { + for i, char := range before { + slog.Debug(fmt.Sprintf("第%d个字符:%v", i+1, string(char))) + if i >= 124 { + slog.Debug("截取124之前的完整字符") + break + } else { + after = strings.Join([]string{after, string(char)}, "") + } + } + slog.Debug("截取后", slog.String("before", before), slog.String("after", after)) + return after +} +func kindesOfPrefix() string { + switch runtime.GOOS { + case "linux": + if uname, _ := exec.Command("uname", "-a").CombinedOutput(); strings.Contains(string(uname), "microsoft") { + return "/mnt/c/Users/zen/Videos" + } + case "windows": + return "" + case "darwin": + case "android": + return "/sdcard/Movies" + default: + os.Exit(-1) + } + return "" +} diff --git a/multi.json b/multi.json deleted file mode 100755 index e5535e1..0000000 --- a/multi.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "media_type": 2, - "has_dash_audio": true, - "is_completed": true, - "total_bytes": 23817994, - "downloaded_bytes": 23817994, - "title": "凤凰传奇现场合集", - "type_tag": "64", - "cover": "http:\/\/i0.hdslb.com\/bfs\/archive\/ada7a362df15d204f92c793417fa41431589ca28.jpg", - "video_quality": 64, - "prefered_video_quality": 64, - "guessed_total_bytes": 0, - "total_time_milli": 220844, - "danmaku_count": 3, - "time_update_stamp": 1657368233823, - "time_create_stamp": 1657362530333, - "can_play_in_advance": true, - "interrupt_transform_temp_file": false, - "quality_pithy_description": "720P", - "quality_superscript": "", - "cache_version_code": 6750300, - "preferred_audio_quality": 0, - "audio_quality": 0, - "avid": 470012166, - "spid": 0, - "seasion_id": 0, - "bvid": "BV1kT41157wN", - "owner_id": 207855349, - "owner_name": "弹指红颜笑", - "owner_avatar": "http:\/\/i1.hdslb.com\/bfs\/face\/663fd7bf08f97daf59763040ae3115ba998c06d9.jpg", - "page_data": { - "cid": 748654562, - "page": 1, - "from": "vupload", - "part": "凤凰传奇_-_郎的诱惑(2013_CCTV3_七夕大型直播)", - "link": "", - "rich_vid": "", - "vid": "", - "has_alias": false, - "weblink": "", - "offsite": "", - "tid": 29, - "width": 1920, - "height": 1080, - "rotate": 0, - "download_title": "视频已缓存完成", - "download_subtitle": "凤凰传奇现场合集 凤凰传奇_-_郎的诱惑(2013_CCTV3_七夕大型直播)" - } -} \ No newline at end of file diff --git a/replace/filename.go b/replace/filename.go new file mode 100644 index 0000000..8650f6f --- /dev/null +++ b/replace/filename.go @@ -0,0 +1,74 @@ +package replace + +import ( + "log/slog" + "regexp" + "strings" +) + +//func ForFileName(str string) string { +// str = strings.Replace(str, "。", ".", -1) +// str = strings.Replace(str, ",", ",", -1) +// str = strings.Replace(str, "《", "(", -1) +// str = strings.Replace(str, "》", ")", -1) +// str = strings.Replace(str, "【", "(", -1) +// str = strings.Replace(str, "】", ")", -1) +// str = strings.Replace(str, "(", "(", -1) +// str = strings.Replace(str, ")", ")", -1) +// str = strings.Replace(str, "「", "(", -1) +// str = strings.Replace(str, "」", ")", -1) +// str = strings.Replace(str, "+", "_", -1) +// str = strings.Replace(str, "`", "", -1) +// str = strings.Replace(str, " ", "", -1) +// str = strings.Replace(str, "\u00A0", "", -1) +// str = strings.Replace(str, "\u0000", "", -1) +// str = strings.Replace(str, "·", "", -1) +// str = strings.Replace(str, "\uE000", "", -1) +// str = strings.Replace(str, "\u000D", "", -1) +// str = strings.Replace(str, "、", "", -1) +// //str = strings.Replace(str, "/", "", -1) +// str = strings.Replace(str, "!", "", -1) +// str = strings.Replace(str, "|", "", -1) +// str = strings.Replace(str, "|", "", -1) +// str = strings.Replace(str, ":", "", -1) +// str = strings.Replace(str, " ", "", -1) +// str = strings.Replace(str, "&", "", -1) +// str = strings.Replace(str, "?", "", -1) +// str = strings.Replace(str, "(", "", -1) +// str = strings.Replace(str, ")", "", -1) +// str = strings.Replace(str, "-", "", -1) +// str = strings.Replace(str, " ", "", -1) +// str = strings.Replace(str, "“", "", -1) +// str = strings.Replace(str, "”", "", -1) +// str = strings.Replace(str, "--", "", -1) +// str = strings.Replace(str, "_", "", -1) +// str = strings.Replace(str, ":", "", -1) +// return str +//} + +/* +仅保留文件名中的 数字 字母 和 中文 +*/ +func ForFileName(name string) string { + nStr := "" + for _, v := range name { + if Effective(string(v)) { + // fmt.Printf("%d\t有效%v\n", i, string(v)) + nStr = strings.Join([]string{nStr, string(v)}, "") + } + } + slog.Debug("正则表达式匹配数字字母汉字", slog.String("文件名", nStr)) + return nStr +} +func Effective(s string) bool { + if s == " " { + return true + } + num := regexp.MustCompile(`\d`) // 匹配任意一个数字 + letter := regexp.MustCompile(`[a-zA-Z]`) // 匹配任意一个字母 + char := regexp.MustCompile(`[\p{Han}]`) // 匹配任意一个汉字 + if num.MatchString(s) || letter.MatchString(s) || char.MatchString(s) { + return true + } + return false +} diff --git a/replace/tty.go b/replace/tty.go new file mode 100644 index 0000000..5ef2fbe --- /dev/null +++ b/replace/tty.go @@ -0,0 +1,13 @@ +package replace + +import "strings" + +/* +终端输出内容转换为可以保存到mysql的字段 +*/ +func TTY2Mysql(tty string) string { + tty = strings.Replace(tty, " ", " ", -1) + tty = strings.Replace(tty, "\u0000", " ", -1) + tty = strings.Replace(tty, "\n", " ", -1) + return tty +} diff --git a/replace/unit_test.go b/replace/unit_test.go new file mode 100644 index 0000000..7daa5cb --- /dev/null +++ b/replace/unit_test.go @@ -0,0 +1,11 @@ +package replace + +import ( + "testing" +) + +func TestForFileName(t *testing.T) { + str := "Hello, 世界!123abc!@#$%^&*()_+" + ret := ForFileName(str) + t.Log(ret) +} diff --git a/singel.json b/singel.json deleted file mode 100644 index 439d912..0000000 --- a/singel.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "media_type": 2, - "has_dash_audio": true, - "is_completed": true, - "total_bytes": 86672476, - "downloaded_bytes": 86672476, - "title": "0170522 (Stellar) (720P_HD)", - "type_tag": "64", - "cover": "http:\/\/i0.hdslb.com\/bfs\/archive\/d0ac7dd0f46bb193849069fc267610f2ff092a4b.jpg", - "video_quality": 64, - "prefered_video_quality": 80, - "guessed_total_bytes": 0, - "total_time_milli": 371356, - "danmaku_count": 1, - "time_update_stamp": 1659894305208, - "time_create_stamp": 1659894279968, - "can_play_in_advance": true, - "interrupt_transform_temp_file": false, - "quality_pithy_description": "720P", - "quality_superscript": "", - "cache_version_code": 6750300, - "preferred_audio_quality": 0, - "audio_quality": 0, - "avid": 216576312, - "spid": 0, - "seasion_id": 0, - "bvid": "BV16a411N7of", - "owner_id": 550915477, - "owner_name": "白小黑视频", - "page_data": { - "cid": 789896058, - "page": 1, - "from": "vupload", - "has_alias": false, - "tid": 0, - "width": 1280, - "height": 720, - "rotate": 0 - } -} \ No newline at end of file diff --git a/sql/bili.go b/sql/bili.go new file mode 100644 index 0000000..3ca3ecd --- /dev/null +++ b/sql/bili.go @@ -0,0 +1,35 @@ +package sql + +import ( + "fmt" + "gorm.io/gorm" + "strconv" + "time" +) + +type Bili struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Cover string `gorm:"cover,type=string;comment:视频封面"` + Title string `gorm:"title;comment:视频标题"` + Owner string `gorm:"owner;comment:视频作者"` + PartName string `gorm:"part_name;comment:分卷标题'"` + Original string `gorm:"original;comment:原始json"` + CreatedAt time.Time `gorm:"created_at;comment:视频创建时间"` + UpdatedAt time.Time `gorm:"updated_at;comment:视频上传时间"` +} + +var layout = "2006-01-02 15:04:05.000000000 +0800" + +func S2T(timestampStr string) time.Time { + timestampStr = "1702664199073" + timestampInt, _ := strconv.ParseInt(timestampStr, 10, 64) + t := time.Unix(timestampInt/1000, 0) + formattedTime := t.Format("2006-01-02 15:04:05.000000000 +0800") + fmt.Println(formattedTime) + return t +} + +func (b *Bili) SetOne() *gorm.DB { + return GetEngine().Create(&b) +} diff --git a/sql/lite.go b/sql/lite.go new file mode 100644 index 0000000..2fb5c98 --- /dev/null +++ b/sql/lite.go @@ -0,0 +1,39 @@ +package sql + +import ( + "fmt" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +func SetEngine() { + db, _ = gorm.Open(sqlite.Open("merge.db"), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Warn), + }) + // 迁移 schema + err := db.AutoMigrate(Bili{}) + if err != nil { + panic("创建数据库错误") + return + } + // Create + //db.Create(&Product{Code: "D42", Price: 100}) + // Read + //var product Product + //db.First(&product, 1) // 根据整型主键查找 + //db.First(&product, "code = ?", "D42") // 查找 code 字段值为 D42 的记录 + // Update - 将 product 的 price 更新为 200 + //db.Model(&product).Update("Price", 200) + // Update - 更新多个字段 + //db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // 仅更新非零值字段 + //db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"}) + // Delete - 删除 product + //db.Delete(&product, 1) + fmt.Println(db) +} +func GetEngine() *gorm.DB { + return db +} diff --git a/sql/unit_test.go b/sql/unit_test.go new file mode 100644 index 0000000..81dce82 --- /dev/null +++ b/sql/unit_test.go @@ -0,0 +1,12 @@ +package sql + +import "testing" + +func TestInit(t *testing.T) { + + SetEngine() +} + +func TestS2t(t *testing.T) { + S2T("3") +} diff --git a/util/cmd.go b/util/cmd.go new file mode 100644 index 0000000..afef368 --- /dev/null +++ b/util/cmd.go @@ -0,0 +1,58 @@ +package util + +import ( + "fmt" + "log/slog" + "os" + "os/exec" + "strings" +) + +func ExecCommand(c *exec.Cmd) (e error) { + defer func() { + if err := recover(); err != nil { + slog.Warn("命令运行出现错误", slog.String("命令原文", fmt.Sprint(c)), slog.Any("错误原文", err)) + os.Exit(-1) + } + }() + slog.Info("开始执行命令", slog.String("命令原文", fmt.Sprint(c))) + if level := GetVal("log", "level"); level == "Debug" { + stdout, err := c.StdoutPipe() + c.Stderr = c.Stdout + if err != nil { + slog.Warn("连接Stdout产生错误", slog.String("命令原文", fmt.Sprint(c)), slog.String("错误原文", fmt.Sprint(err))) + return err + } + if err = c.Start(); err != nil { + slog.Warn("启动cmd命令产生错误", slog.String("命令原文", fmt.Sprint(c)), slog.String("错误原文", fmt.Sprint(err))) + return err + } + for { + tmp := make([]byte, 1024) + _, err := stdout.Read(tmp) + t := string(tmp) + t = strings.Replace(t, "\u0000", "", -1) + fmt.Println(t) + if err != nil { + break + } + } + if err = c.Wait(); err != nil { + slog.Warn("命令执行中产生错误", slog.String("命令原文", fmt.Sprint(c)), slog.String("错误原文", fmt.Sprint(err))) + return err + } + } else { + if output, err := c.CombinedOutput(); err != nil { + slog.Warn("命令执行中产生错误", slog.String("命令原文", fmt.Sprint(c)), slog.String("错误原文", fmt.Sprint(err))) + return err + } else { + // 这是一段永远不可能被运行的代码 + slog.Debug("命令执行完毕", slog.String("输出", string(output))) + } + } + if exit := GetExitStatus(); exit { + slog.Debug("命令端获取到退出状态,命令结束后退出", slog.Bool("信号值", exit), slog.String("最后一条命令", fmt.Sprint(c))) + os.Exit(0) + } + return nil +} diff --git a/util/conf.go b/util/conf.go new file mode 100644 index 0000000..0efbed8 --- /dev/null +++ b/util/conf.go @@ -0,0 +1,50 @@ +package util + +import ( + "errors" + "fmt" + "github.com/zhangyiming748/AVmerger/util/goini" + "log/slog" +) + +const confPath = "./conf.ini" + +var ( + conf *goini.Config +) + +/* +* + - 初始化 + init函数的主要作用: + 初始化不能采用初始化表达式初始化的变量。 + 程序运行前的注册。 + 实现sync.Once功能。 + 其他 +*/ +func init() { + initConfig() +} + +func initConfig() { + conf = goini.SetConfig(confPath) + slog.Debug("读取配置文件", slog.String("文件名", confPath)) +} + +/** + * 根据键获取值 + */ +func GetVal(section, name string) string { + val, _ := conf.GetValue(section, name) + return val +} + +func SetVal(section, key, value string) error { + if err := conf.SetValue(section, key, value); err { + slog.Debug("修改配置文件成功") + return nil + } else { + slog.Warn("修改配置文件失败") + return errors.New(fmt.Sprintf("修改配置文件失败\tsextion:%s\tkey:%s\tvalue:%s\n", section, key, value)) + } +} diff --git a/util/duplicate.go b/util/duplicate.go new file mode 100644 index 0000000..38fd550 --- /dev/null +++ b/util/duplicate.go @@ -0,0 +1,16 @@ +package util + +/* +根据输入的字符串切片返回去重的字符串切片 +*/ +func DuplicateBySlice(elements []string) (dup []string) { + m := make(map[string]bool) + for _, element := range elements { + if _, ok := m[element]; !ok { // 如果元素不在map中,则添加到result和map中 + dup = append(dup, element) + m[element] = true + } + } + + return dup +} diff --git a/util/goini/conf.go b/util/goini/conf.go new file mode 100644 index 0000000..c7b803f --- /dev/null +++ b/util/goini/conf.go @@ -0,0 +1,154 @@ +package goini + +import ( + "bufio" + "errors" + "fmt" + "io" + "log/slog" + "os" + "strings" +) + +type Config struct { + filepath string //your ini file path directory+file + conflist []map[string]map[string]string //configuration information slice +} + +// Create an empty configuration file +func SetConfig(filepath string) *Config { + slog.Debug("读取配置文件", slog.String("文件名", filepath)) + c := new(Config) + c.filepath = filepath + + return c +} + +// To obtain corresponding value of the key values +func (c *Config) GetValue(section, name string) (string, error) { + c.ReadList() + conf := c.ReadList() + for _, v := range conf { + for key, value := range v { + if key == section { + return value[name], nil + } + } + } + notFound := errors.New("no value") + return "", notFound +} + +// Set the corresponding value of the key value, if not add, if there is a key change +func (c *Config) SetValue(section, key, value string) bool { + c.ReadList() + data := c.conflist + var ok bool + var index = make(map[int]bool) + var conf = make(map[string]map[string]string) + for i, v := range data { + _, ok = v[section] + index[i] = ok + } + + i, ok := func(m map[int]bool) (i int, v bool) { + for i, v := range m { + if v == true { + return i, true + } + } + return 0, false + }(index) + + if ok { + c.conflist[i][section][key] = value + return true + } else { + conf[section] = make(map[string]string) + conf[section][key] = value + c.conflist = append(c.conflist, conf) + return true + } + + return false +} + +// Delete the corresponding key values +func (c *Config) DeleteValue(section, name string) bool { + c.ReadList() + data := c.conflist + for i, v := range data { + for key, _ := range v { + if key == section { + delete(c.conflist[i][key], name) + return true + } + } + } + return false +} + +// List all the configuration file +func (c *Config) ReadList() []map[string]map[string]string { + + file, err := os.Open(c.filepath) + if err != nil { + CheckErr(err) + } + defer file.Close() + var data map[string]map[string]string + var section string + buf := bufio.NewReader(file) + for { + l, err := buf.ReadString('\n') + line := strings.TrimSpace(l) + if err != nil { + if err != io.EOF { + CheckErr(err) + } + if len(line) == 0 { + break + } + } + switch { + case len(line) == 0: + case string(line[0]) == "#": //增加配置文件备注 + case line[0] == '[' && line[len(line)-1] == ']': + section = strings.TrimSpace(line[1 : len(line)-1]) + data = make(map[string]map[string]string) + data[section] = make(map[string]string) + default: + i := strings.IndexAny(line, "=") + if i == -1 { + continue + } + value := strings.TrimSpace(line[i+1 : len(line)]) + data[section][strings.TrimSpace(line[0:i])] = value + if c.uniquappend(section) == true { + c.conflist = append(c.conflist, data) + } + } + + } + + return c.conflist +} + +func CheckErr(err error) string { + if err != nil { + return fmt.Sprintf("Error is :'%s'", err.Error()) + } + return "Notfound this error" +} + +// Ban repeated appended to the slice method +func (c *Config) uniquappend(conf string) bool { + for _, v := range c.conflist { + for k, _ := range v { + if k == conf { + return false + } + } + } + return true +} diff --git a/util/randon.go b/util/randon.go new file mode 100644 index 0000000..b61b4ca --- /dev/null +++ b/util/randon.go @@ -0,0 +1,19 @@ +package util + +import ( + "log/slog" + "math/rand" + "time" +) + +func RandomWithSeed() { + rand.Seed(time.Now().Unix()) + a := rand.Intn(2000) + seed := rand.New(rand.NewSource(time.Now().Unix())) + b := seed.Intn(2000) + if a == b { + slog.Info("生成的随机数", slog.Int("a", a), slog.Int("b", b)) + } else { + slog.Info("不相等") + } +}