diff --git a/README.md b/README.md index 9fd63c8..a6d5887 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,25 @@ ## 功能特性 -- **图片 / 视频双支持**:扫描当前目录(递归),自动识别图片与视频文件 +- **图片 / 视频 / 音频**:扫描当前目录(递归),自动识别图片、视频与音频文件 - **标准分辨率归类**:720P、1080P、2K、2.5K、影院 2K、DCI 4K、8K 及带鱼屏(UltraWide 2K/3K/4K)等标准规格单独建目录 - **非标分辨率归类**:按像素区间自动归入 1K ~ 9K / 普通 / 原图(原画质)目录 - **方向区分**:横屏、竖屏、等比(正方形)分开整理,GIF 单独处理 +- **音频归类**:音频文件单独归入「音频」目录 - **异常兜底**:读取失败、不支持的格式自动归入「读取异常」「已忽略」目录 - **自动整理**:无需人工干预,扫描 → 分析 → 移动一次完成,跨磁盘自动降级为复制后删除 -- **零依赖安装**:视频信息读取使用内嵌的 `ffprobe.exe`(Windows x64),无需额外安装 ffmpeg +- **全格式支持**:视频/音频以内嵌的 `ffprobe.exe`(Windows x64)作为最终判定依据,**凡是 ffmpeg 支持的格式都能识别**,不再依赖扩展名白名单 +- **媒体信息入库**:自动将文件的 MD5 与完整媒体信息(类型/宽高/时长/编码/容器等)存入根目录下的 `media-info.db`(SQLite,xorm 驱动),支持按字段查询与去重 ## 支持的格式 | 类型 | 格式 | |------|------| -| 图片 | JPG / JPEG / JPE / JFIF / PNG / GIF / BMP / WEBP / PSB,PSD 单独归入「图片_PSD」 | -| 视频 | MP4 / AVI / MOV / MKV / MPG / M4V / WMV / WEBM / FLV / TS / RM / RMVB / 3GP / 3G2 | +| 图片 | JPG / JPEG / JPE / JFIF / PNG / GIF / BMP / WEBP / PSB,PSD 单独归入「图片_PSD」;Go 无法解码的图片格式(如 TIFF)由 ffprobe 兜底 | +| 视频 | 凡 ffmpeg 支持的容器/编码均可识别(MP4 / MKV / AVI / MOV / TS / FLV / WMV / RM / RMVB / WEBM / M2TS 等),以 ffprobe 探测结果为准 | +| 音频 | 凡 ffmpeg 支持的音频(MP3 / WAV / FLAC / AAC / M4A / OGG / WMA 等)归入「音频」目录 | -> 判断依据为文件 MIME 信息 + 扩展名双重识别,扩展名大小写不敏感。 +> 判断依据:图片按文件 MIME + 扩展名识别(扩展名大小写不敏感);视频/音频由 ffprobe 读取媒体流判定,支持范围与 ffmpeg 一致。 ## 构建编译 @@ -72,7 +75,7 @@ kenaito-media-picker.exe {uid}-图片_已忽略 ← 不支持格式的图片 ``` -### 视频 +### 视频 / 音频 ```text {uid}-视频_横屏_720P / 1080P / 2DKP / 2D5KP / 4KP / 4KHP / 8KP @@ -81,17 +84,18 @@ kenaito-media-picker.exe {uid}-视频_竖屏_1k ~ 9k / 普通 / 原画质 {uid}-视频_等比_1k ~ 9k / 普通 / 原画质 {uid}-视频_横屏_GIF / 视频_竖屏_GIF / 视频_等比_GIF -读取异常 ← 无法读取分辨率的视频 -已忽略 ← 不支持的视频格式 +音频 ← 所有音频文件(ffmpeg 可识别的格式) +视频_读取异常 ← mime 为视频但 ffprobe 读取失败 +已忽略 ← 非媒体文件或无法识别 ``` -> 视频时长与分辨率通过内嵌 `ffprobe.exe` 读取(处理时会临时释放,完成后自动删除)。 +> 视频时长与分辨率通过内嵌 `ffprobe.exe` 读取(处理时会临时释放,完成后自动删除);凡 ffmpeg 支持的视频/音频格式均可识别。 ## 目录结构 ```text -├── main.go # 程序入口(扫描 → 过滤 → 图片/视频处理) -├── core/ # 核心逻辑:文件扫描、MIME 过滤、图片/视频分组与移动 +├── main.go # 程序入口(扫描 → 过滤 → 图片/媒体处理) +├── core/ # 核心逻辑:文件扫描、MIME 过滤、图片/视频/音频分组与移动 ├── util/ # 工具:MIME 识别、图片尺寸读取、ffprobe 封装、文件移动 ├── vars/ # 全局状态 + 内嵌 ffprobe.exe(go:embed) ├── doc/ # 构建依赖(ffmpeg / upx 压缩包) @@ -101,9 +105,9 @@ kenaito-media-picker.exe ## 注意事项 - 默认使用**移动**模式,原文件会从源目录移走;同名文件跨磁盘时自动降级为复制 + 删除 -- 每次运行生成独立的 UUID 目录,重复运行不会相互覆盖 +- 已整理文件全部移动到 `{当前目录名}-picker` 白名单目录内,重复运行会跳过该目录,并将新文件并入已有的分类目录 - GIF 图片单独归入「图片_GIF」;GIF 视频按方向归入「视频_*_GIF」 -- 视频处理依赖 Windows 平台的 `ffprobe.exe`(已内嵌),非 Windows 平台需自行调整 +- 视频/音频处理依赖 Windows 平台的 `ffprobe.exe`(已内嵌),非 Windows 平台需自行调整 ## License diff --git a/bug20260813.md b/bug20260813.md deleted file mode 100644 index 02dab14..0000000 --- a/bug20260813.md +++ /dev/null @@ -1,89 +0,0 @@ -# 项目审查记录 2026-08-13 - -> 审查方式:静态代码审查(`go build` / `go vet` 均通过,此文件不含编译级错误) -> 审查范围:全部 `.go` 源码 - ---- - -## 一、严重 Bug - -### 1. WEBP / BMP / PSB 永远进「读取异常」文件夹(专用分支是死代码) - -- **位置**:`util/image.go:9-19`、`core/handle_image.go:22,35,46` -- **问题**:`supportImageTypes` 中包含了 `.bmp`、`.webp`、`.psb`,导致 `handle_image.go:22` 的通用分支总是先命中,调用 `image.DecodeConfig`(`main.go:6-8` 只注册了 png/jpeg/gif 解码器),对 webp/bmp 必然失败,文件全部落入「读取异常」目录。 -- **影响**:`handle_image.go:35`(webp)、`:46`(bmp)的专用处理分支永远不会执行,与 README 承诺支持的格式不符。 -- **修复方向**:从 `supportImageTypes` 中移除 `.bmp` / `.webp` / `.psb`,让专用分支生效;或在通用分支前先特判这三类格式。 - -### 2. PSD 从未被识别为图片,`ImagePsdList` 恒为空 - -- **位置**:`core/file_scanner.go:70-79`、`core/handle_image.go:57` -- **问题**:PSD 文件的 MIME 为 `application/vnd.adobe.photoshop`(不包含 `image/` 前缀),且 `.psd` 不在 `supportImageTypes` 中,因此在 `DoFilter` 阶段就被丢弃,永远不会进入 `GlobalImagePathList`。 -- **影响**:`handle_image.go:57` 的 PSD 分支不可达,README 声称的「PSD 单独归入图片_PSD」功能实际失效。 -- **修复方向**:在过滤阶段对 `.psd` / `application/vnd.adobe.photoshop` 单独识别并入图片列表。 - -### 3. 同名文件移动时数据丢失风险(已处理) - -- **位置**:`util/video.go:152-169`(`moveFileToDir`) -- **问题**:目标文件名只取 `BaseName`。不同子目录下的同名文件(如 `a/photo.jpg`、`b/photo.jpg`)会被移动到/复制到同一目标:Unix 下 `os.Rename` 静默覆盖,跨盘 fallback 的 `copyFile`(`os.Create`)也会截断覆盖目标。 -- **影响**:真实数据丢失风险,且无任何告警。 -- **修复方向**:目标名冲突时追加序号或 hash,确保不覆盖已有文件。 - -### 4. BMP 自顶向下格式(负高度)导致解析错乱(已处理) - -- **位置**:`core/bmp.go:59`、`core/handle_image.go:96-98` -- **问题**:`ReadBmpImage` 直接返回 `int32` 宽高,负高度被 `fmt.Sprintf("%d-%d")` 拼成 `"640--480"`,`strings.Split("-")` 后 `width2Height[1]` 为空串,`String2int` 返回 0,图片被错误归类。 -- **影响**:此类 BMP 图片分辨率判定错误(高度变 0,全部按横屏普通处理)。 -- **修复方向**:读取时对高度取绝对值,或改用不易混淆的分隔符/结构化存储。 - -### 5. 重复运行会重新整理已整理过的目录(已处理) - -- **位置**:`core/file_scanner.go:20`、`main.go` -- **问题**:`filepath.Walk` 扫描整个根目录。第二次运行时,上次生成的 `{uid}-图片_*`、`{uid}-视频_*` 等目录会被再次扫描,其中已分类的文件会被再次移动,形成无限嵌套/重复归类。 -- **影响**:重复执行程序导致目录结构越来越乱,违背 README「重复运行不会相互覆盖」的描述。 -- **修复方向**:扫描时跳过已生成的 `{uid}-` 前缀目录,或提供过滤白名单。 - ---- - -## 二、次要问题 - -### 6. ffprobe 输出解析脆弱 - -- **位置**:`util/video.go:53-101` -- **问题**:对多视频流(常见于 MKV)的多行输出处理靠猜测(`\r\n\r\n\r\n\r\n` split、`N/AxN/A` 字符串替换等),且不校验 0 值分辨率,解析出错时归类到「普通」而非「读取异常」。 - -### 7. 视频「读取异常 / 已忽略」目录命名不一致 - -- **位置**:`core/handle_video.go:56,62` -- **问题**:这两个目录没有 uid 前缀(如 `读取异常`、`已忽略`),与其它 `{uid}-视频_*` 目录风格不统一,且与图片侧带 uid 的命名方式不一致。 - -### 8. 死代码 / 未使用数据 - -- **位置**:`util/file.go:47-112`、`vars/video.go`、`util/file.go:17-21`、`vars/global.go:5` -- **问题**:`VideoPath2DurationMap` / `VideoPath2WidthHeightTagMap` 赋值后从未使用(重命名逻辑已被注释掉);`util/file.go` 中保留大段注释代码;`GetFileDirectory`、`GlobalFilePath2FileNameMap` 未被使用。 - -### 9. `doPickImageFile` 空判断误报 - -- **位置**:`core/handle_image.go:91` -- **问题**:判断的是全局 `vars.ImagePath2WidthHeightMap` 而非入参 `imagePath2WidthHeightMap`;当所有图片都是错误/忽略时,会误打印「当前目录下没有扫描到图片文件」。 - -### 10. 目录创建失败未处理 - -- **位置**:`core/handle_image.go`(各 `move*Image` 函数)、`core/handle_video.go`(各 `move*Video` 函数) -- **问题**:多数 `util.CreateDir` 的返回值被忽略,若目录创建失败仍继续执行 `DoMoveFileToDir`,文件移动会静默失败。 - -### 11. 临时 ffprobe.exe 清理不保证 - -- **位置**:`core/handle_video.go:13-24,68-71` -- **问题**:通过流程末尾手动 `os.Remove` 清理,若中途异常(panic / 提前 return)会残留 `ffprobe.exe`;下次运行该文件会被扫描当作普通文件。建议 `defer` 清理。 - ---- - -## 三、优化建议 - -1. **修复严重 bug 1/2 的关键**:调整 `supportImageTypes` 列表 / 过滤逻辑,确保 webp、bmp、psb、psd 走各自正确的分支。 -2. **重复运行防护**:扫描时跳过已生成的 `{uid}-` 目录。 -3. **合并 ffprobe 调用**:分辨率 + 时长可用一次 `-show_streams -show_format` 取回,每视频省一次子进程创建。 -4. **消除重复代码**:图片/视频各有约 60 个几乎相同的分组 + 移动函数,可用「分组名 → 列表」映射表驱动,大幅减小编码量。 -5. **使用 `filepath.Join`**:代替 `rootDir + separator + ...` 字符串拼接,避免路径分隔符与平台问题。 -6. **`moveFileToDir` 增加同名去重**:冲突时追加序号/hash,杜绝覆盖。 -7. **清理死代码**:删除注释掉的 rename 逻辑、未使用的 map 与函数。 diff --git a/core/bmp.go b/core/bmp.go index 45cd99a..0613bcb 100644 --- a/core/bmp.go +++ b/core/bmp.go @@ -3,6 +3,7 @@ package core import ( "encoding/binary" "fmt" + "io" "os" ) @@ -85,3 +86,29 @@ func absInt32(v int32) int32 { } return v } + +// ReadPsdImage 读取PSD/PSB格式的图片信息(两者共享相同的8BPS文件头) +func ReadPsdImage(filePath string) (err error, width int, height int) { + file, err := os.Open(filePath) + if err != nil { + fmt.Printf("=== Failed to open file: %v\n", err) + return err, 0, 0 + } + defer file.Close() + + var header [26]byte + if _, err := io.ReadFull(file, header[:]); err != nil { + fmt.Printf("=== Failed to read PSD/PSB header: %v\n", err) + return err, 0, 0 + } + if string(header[0:4]) != "8BPS" { + return fmt.Errorf("not a valid PSD/PSB file: magic %q, expected \"8BPS\"", string(header[0:4])), 0, 0 + } + version := binary.BigEndian.Uint16(header[4:6]) + if version != 1 && version != 2 { + return fmt.Errorf("unsupported PSD/PSB version: %d", version), 0, 0 + } + height = int(binary.BigEndian.Uint32(header[14:18])) + width = int(binary.BigEndian.Uint32(header[18:22])) + return nil, width, height +} diff --git a/core/file_scanner.go b/core/file_scanner.go index 9890b1d..e39900e 100644 --- a/core/file_scanner.go +++ b/core/file_scanner.go @@ -3,6 +3,7 @@ package core import ( "fmt" "github.com/redmask-hb/GoSimplePrint/goPrint" + "kenaito-media-picker/store" "kenaito-media-picker/util" "kenaito-media-picker/vars" "log" @@ -55,29 +56,21 @@ func (FileScanner) DoFilter() { for _, globalFilePath := range vars.GlobalFilePathList { fileMime := vars.GlobalFilePath2MimeInfoMap[globalFilePath] count = count + 1 - if strings.Contains(fileMime, "video/") { // 视频 - vars.GlobalVideoPathList = append(vars.GlobalVideoPathList, globalFilePath) - bar.PrintBar(util.CalcPercentage(count, total)) - continue - } - // mime格式为application/octet-stream的视频 + // 图片 ext := path.Ext(globalFilePath) - if util.IsSupportVideo(ext) { - vars.GlobalVideoPathList = append(vars.GlobalVideoPathList, globalFilePath) - bar.PrintBar(util.CalcPercentage(count, total)) - continue - } - if strings.Contains(fileMime, "image/") { // 图片 + if strings.Contains(fileMime, "image/") || util.IsSupportImage(ext) || strings.EqualFold(ext, ".psd") { vars.GlobalImagePathList = append(vars.GlobalImagePathList, globalFilePath) bar.PrintBar(util.CalcPercentage(count, total)) continue } - if util.IsSupportImage(ext) { - vars.GlobalImagePathList = append(vars.GlobalImagePathList, globalFilePath) + // 其余作为媒体候选,由ffprobe最终判定(视频/音频/不支持), + // 只要ffmpeg支持的格式都会被识别,不再依赖扩展名白名单 + if util.IsPotentialMedia(fileMime) { + vars.GlobalMediaPathList = append(vars.GlobalMediaPathList, globalFilePath) bar.PrintBar(util.CalcPercentage(count, total)) continue } - // 其他的文件不处理 + // 明显的非媒体文件(代码/文档/压缩包等)直接不处理 } bar.PrintEnd("=== Finish") } @@ -91,6 +84,9 @@ func visit(currentPath string, info os.FileInfo, err error) error { return filepath.SkipDir // 跳过白名单目录(已整理文件的输出目录) } if !info.IsDir() { + if filepath.Base(currentPath) == store.DBFileName { + return nil // 跳过媒体信息数据库文件 + } vars.GlobalFilePathList = append(vars.GlobalFilePathList, currentPath) // filename, include ext vars.GlobalFilePath2FileNameMap[currentPath] = filepath.Base(currentPath) diff --git a/core/handle_image.go b/core/handle_image.go index b04f956..254949f 100644 --- a/core/handle_image.go +++ b/core/handle_image.go @@ -18,26 +18,15 @@ func DoHandleImage(pickerDir string) { ignoreCount := 0 // 忽略数 for _, imageFilePath := range vars.GlobalImagePathList { suffix := vars.GlobalFilePath2FileExtMap[imageFilePath] - if util.IsSupportImage(suffix) { - err, width, height := util.ReadImageInfo(imageFilePath) - if err == nil { - successCount = successCount + 1 - vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", width, height) - fmt.Printf("=== Image总数: %d, 已读取Info: %d, 成功数: %d, 失败数: %d \n", total, successCount+errorCount+ignoreCount, successCount, errorCount) - } else { - errorCount = errorCount + 1 - vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath) - fmt.Printf("=== 异常图片: %s \n", imageFilePath) - } - continue - } if strings.EqualFold(suffix, ".webp") { // 特殊文件处理, webp为网络常用图片格式 webpErr, webpWidth, webpHeight := ReadWebpImage(imageFilePath) if webpErr == nil { vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", webpWidth, webpHeight) + saveMediaRecord(imageFilePath, "image", webpWidth, webpHeight, 0, "", "", "") successCount = successCount + 1 } else { errorCount = errorCount + 1 + vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath) fmt.Printf("=== 异常图片: %s \n", imageFilePath) } continue @@ -46,9 +35,11 @@ func DoHandleImage(pickerDir string) { bpmErr, bmpWidth, bmpHeight := ReadBmpImage(imageFilePath) if bpmErr == nil { vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", bmpWidth, bmpHeight) + saveMediaRecord(imageFilePath, "image", int(bmpWidth), int(bmpHeight), 0, "", "", "") successCount = successCount + 1 } else { errorCount = errorCount + 1 + vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath) fmt.Printf("=== 异常图片: %s \n", imageFilePath) } continue @@ -58,6 +49,46 @@ func DoHandleImage(pickerDir string) { successCount = successCount + 1 continue } + if strings.EqualFold(suffix, ".psb") { // 特殊文件处理, psb与psd共享文件头 + psbErr, psbWidth, psbHeight := ReadPsdImage(imageFilePath) + if psbErr == nil { + vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", psbWidth, psbHeight) + saveMediaRecord(imageFilePath, "image", psbWidth, psbHeight, 0, "", "", "") + successCount = successCount + 1 + } else { + errorCount = errorCount + 1 + vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath) + fmt.Printf("=== 异常图片: %s \n", imageFilePath) + } + continue + } + if util.IsSupportImage(suffix) { + err, width, height := util.ReadImageInfo(imageFilePath) + if err == nil { + successCount = successCount + 1 + vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", width, height) + saveMediaRecord(imageFilePath, "image", width, height, 0, "", "", "") + fmt.Printf("=== Image总数: %d, 已读取Info: %d, 成功数: %d, 失败数: %d \n", total, successCount+errorCount+ignoreCount, successCount, errorCount) + } else if util.CheckFileIsExist(util.FFprobeExePath()) { + // Go标准库解码失败,尝试用ffprobe兜底(ffmpeg支持的图片格式更广,如tiff/svg等) + if probe, perr := util.ProbeMediaFile(util.FFprobeExePath(), imageFilePath); perr == nil { + if w, h := probe.VideoResolution(); w > 0 && h > 0 { + successCount = successCount + 1 + vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", w, h) + saveMediaRecord(imageFilePath, "image", w, h, 0, probe.VideoCodec(), probe.FormatName(), probe.RawJSON()) + continue + } + } + errorCount = errorCount + 1 + vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath) + fmt.Printf("=== 异常图片: %s \n", imageFilePath) + } else { + errorCount = errorCount + 1 + vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath) + fmt.Printf("=== 异常图片: %s \n", imageFilePath) + } + continue + } // 其他的直接先忽略吧, 爱改改, 不改拉倒 ignoreCount = ignoreCount + 1 vars.ImageIgnorePathList = append(vars.ImageIgnorePathList, imageFilePath) diff --git a/core/handle_video.go b/core/handle_video.go index 20929fd..a1d0733 100644 --- a/core/handle_video.go +++ b/core/handle_video.go @@ -6,57 +6,60 @@ import ( "kenaito-media-picker/vars" "os" "strings" - "time" ) -func DoHandleVideo(pickerDir string) { - // 释放ffprobe - readerFileName := "./ffprobe.exe" - if util.CheckFileIsExist(readerFileName) { - _ = os.Remove(readerFileName) - } - err := util.WriteByteArraysToFile(vars.VideoDecodeToolWin64, readerFileName) - if err != nil { - fmt.Println("=== 释放解码器失败, 5秒后将自动退出", err) - time.Sleep(time.Second * 5) - return - } - total := len(vars.GlobalVideoPathList) // 总数 - successCount := 0 // 成功数 - errorCount := 0 // 失败数 - ignoreCount := 0 // 忽略数 - for _, videoFilePath := range vars.GlobalVideoPathList { - suffix := vars.GlobalFilePath2FileExtMap[videoFilePath] - if util.IsSupportVideo(suffix) { - width, height, err := util.ReadVideoWidthHeight(videoFilePath) - if err == nil { - successCount = successCount + 1 - vars.VideoPath2WidthHeightMap[videoFilePath] = fmt.Sprintf("%d-%d", width, height) - vars.VideoPath2WidthHeightTagMap[videoFilePath] = fmt.Sprintf("[%dx%d]", width, height) - fmt.Printf("=== Video总数: %d, 已读取Info: %d, 成功数: %d, 失败数: %d \n", total, successCount+errorCount+ignoreCount, successCount, errorCount) - duration := util.ReadVideoDuration(videoFilePath) - if duration == 0 { - vars.VideoPath2DurationMap[videoFilePath] = "0H0M0S" - } else { - vars.VideoPath2DurationMap[videoFilePath] = util.SecondsToHms(duration) - } - } else { +// DoHandleMedia 处理所有媒体候选文件:ffprobe判定视频/音频/不支持 +func DoHandleMedia(pickerDir string) { + total := len(vars.GlobalMediaPathList) + videoCount := 0 // 视频数 + audioCount := 0 // 音频数 + errorCount := 0 // 异常数 + ignoreCount := 0 + for _, mediaFilePath := range vars.GlobalMediaPathList { + fileMime := vars.GlobalFilePath2MimeInfoMap[mediaFilePath] + probe, err := util.ProbeMediaFile(util.FFprobeExePath(), mediaFilePath) + if err != nil { + if strings.Contains(fileMime, "video/") { + // mime是视频但ffprobe读不出来,视为损坏/异常视频 errorCount = errorCount + 1 - vars.VideoReadErrorPathList = append(vars.VideoReadErrorPathList, videoFilePath) - fmt.Printf("=== 异常视频: %s \n", videoFilePath) + vars.VideoReadErrorPathList = append(vars.VideoReadErrorPathList, mediaFilePath) + fmt.Printf("=== 异常视频: %s \n", mediaFilePath) + } else { + ignoreCount = ignoreCount + 1 + vars.VideoIgnorePathList = append(vars.VideoIgnorePathList, mediaFilePath) } continue } - // 其他的直接先忽略吧, 爱改改, 不改拉倒 + width, height := probe.VideoResolution() + if width > 0 && height > 0 { // 有视频流 + videoCount = videoCount + 1 + vars.VideoPath2WidthHeightMap[mediaFilePath] = fmt.Sprintf("%d-%d", width, height) + saveMediaRecord(mediaFilePath, "video", width, height, probe.DurationSeconds(), probe.VideoCodec(), probe.FormatName(), probe.RawJSON()) + fmt.Printf("=== 媒体总数: %d, 已处理: %d, 视频: %d, 音频: %d, 异常: %d, 忽略: %d \n", total, videoCount+audioCount+errorCount+ignoreCount, videoCount, audioCount, errorCount, ignoreCount) + continue + } + if probe.HasAudio() { // 仅有音频流 + audioCount = audioCount + 1 + vars.GlobalAudioPathList = append(vars.GlobalAudioPathList, mediaFilePath) + saveMediaRecord(mediaFilePath, "audio", 0, 0, probe.DurationSeconds(), probe.AudioCodec(), probe.FormatName(), probe.RawJSON()) + continue + } + // 既无视频也无音频(字幕流等) ignoreCount = ignoreCount + 1 - vars.VideoIgnorePathList = append(vars.VideoIgnorePathList, videoFilePath) + vars.VideoIgnorePathList = append(vars.VideoIgnorePathList, mediaFilePath) } if len(vars.VideoReadErrorPathList) > 0 { - readInfoErrorPath := pickerDir + string(os.PathSeparator) + "读取异常" + readInfoErrorPath := pickerDir + string(os.PathSeparator) + "视频_读取异常" if util.CreateDir(readInfoErrorPath) { util.DoMoveFileToDir(vars.VideoReadErrorPathList, readInfoErrorPath) } } + if len(vars.GlobalAudioPathList) > 0 { + audioPath := pickerDir + string(os.PathSeparator) + "音频" + if util.CreateDir(audioPath) { + util.DoMoveFileToDir(vars.GlobalAudioPathList, audioPath) + } + } if len(vars.VideoIgnorePathList) > 0 { ignorePath := pickerDir + string(os.PathSeparator) + "已忽略" if util.CreateDir(ignorePath) { @@ -64,21 +67,13 @@ func DoHandleVideo(pickerDir string) { } } doPickVideoFile(pickerDir, vars.VideoPath2WidthHeightMap) - // 删除ffprobe - if util.CheckFileIsExist(readerFileName) { - _ = os.Remove(readerFileName) - } - fmt.Printf("=== 视频处理完毕 \n\n") + fmt.Printf("=== 视频/音频处理完毕 \n\n") } // 条件视频并分组存放 func doPickVideoFile(pickerDir string, videoPath2WidthHeightMap map[string]string) { if len(videoPath2WidthHeightMap) == 0 { fmt.Printf("=== 当前目录下没有扫描到视频文件, %s \n", pickerDir) - readerFileName := "./ffprobe.exe" - if util.CheckFileIsExist(readerFileName) { - _ = os.Remove(readerFileName) - } return } for currentVideoPath, infoStr := range videoPath2WidthHeightMap { diff --git a/core/save_media.go b/core/save_media.go new file mode 100644 index 0000000..6cb8e5b --- /dev/null +++ b/core/save_media.go @@ -0,0 +1,38 @@ +package core + +import ( + "fmt" + "kenaito-media-picker/store" + "kenaito-media-picker/util" + "kenaito-media-picker/vars" + "os" +) + +// saveMediaRecord 计算MD5并将媒体信息写入数据库 +func saveMediaRecord(filePath string, mediaType string, width int, height int, duration float64, codec string, format string, rawInfo string) { + md5Str, err := util.CalcFileMd5(filePath) + if err != nil { + fmt.Printf("=== 计算MD5失败: %s, 错误: %v\n", filePath, err) + return + } + info := &store.MediaInfo{ + FileMd5: md5Str, + FilePath: filePath, + FileName: vars.GlobalFilePath2FileNameMap[filePath], + FileExt: vars.GlobalFilePath2FileExtMap[filePath], + MimeType: vars.GlobalFilePath2MimeInfoMap[filePath], + MediaType: mediaType, + Width: width, + Height: height, + Duration: duration, + Codec: codec, + Format: format, + RawInfo: rawInfo, + } + if st, err := os.Stat(filePath); err == nil { + info.FileSize = st.Size() + } + if err := store.SaveMedia(info); err != nil { + fmt.Printf("=== 保存媒体信息失败: %s, 错误: %v\n", filePath, err) + } +} diff --git a/go.mod b/go.mod index 40c8935..0584540 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,32 @@ module kenaito-media-picker -go 1.19 +go 1.25.0 require ( github.com/gabriel-vasile/mimetype v1.4.3 + github.com/glebarez/go-sqlite v1.20.3 github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d github.com/satori/go.uuid v1.2.0 golang.org/x/image v0.16.0 + xorm.io/xorm v1.3.4 ) require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/goccy/go-json v0.8.1 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect golang.org/x/net v0.17.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + golang.org/x/sys v0.13.0 // indirect + modernc.org/libc v1.22.2 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.20.4 // indirect + xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect ) diff --git a/go.sum b/go.sum index 8b4c09d..c07a5af 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,334 @@ +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +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/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= +github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= +github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.0/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +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/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d h1:h/hohIqMUCML2Rp9BXXAu0I3ZR68d7eqMHLPNq7N2tg= github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d/go.mod h1:LiYo3EFlYfk46Re4zgQysMo6yO3/kAXslB2fyMdl+uw= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M= +github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +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-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= +modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0= +modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA= +modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0= +modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= +modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= +modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= +modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/xorm v1.3.4 h1:vWFKzR3DhGUDl5b4srhUjhDwjxkZAc4C7BFszpu0swI= +xorm.io/xorm v1.3.4/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo= diff --git a/main.go b/main.go index d534fc7..1f71f5f 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( _ "image/jpeg" // 导入jpeg支持 _ "image/png" // 导入png支持 "kenaito-media-picker/core" + "kenaito-media-picker/store" + "kenaito-media-picker/util" "kenaito-media-picker/vars" "os" "path/filepath" @@ -27,10 +29,23 @@ func main() { scanner := core.FileScanner{} scanner.DoScan(rootDir) scanner.DoFilter() + // 初始化媒体信息数据库(失败不影响整理,仅无法记录) + if err := store.Init(filepath.Join(rootDir, store.DBFileName)); err != nil { + fmt.Println("=== 初始化数据库失败(继续整理, 不记录媒体信息):", err) + } + defer store.Close() + if len(vars.GlobalImagePathList) > 0 || len(vars.GlobalMediaPathList) > 0 { + if err := util.ReleaseFFprobe(); err != nil { + fmt.Println("=== 释放解码器失败, 5秒后将自动退出", err) + time.Sleep(time.Second * 5) + return + } + } if len(vars.GlobalImagePathList) > 0 { core.DoHandleImage(pickerDir) } - if len(vars.GlobalVideoPathList) > 0 { - core.DoHandleVideo(pickerDir) + if len(vars.GlobalMediaPathList) > 0 { + core.DoHandleMedia(pickerDir) } + util.RemoveFFprobe() } diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000..a20b6a3 --- /dev/null +++ b/store/store.go @@ -0,0 +1,80 @@ +package store + +import ( + "database/sql" + "time" + + sqlite "github.com/glebarez/go-sqlite" + "xorm.io/xorm" +) + +// DBFileName 数据库文件名(存放于被整理目录的根目录,扫描时跳过) +const DBFileName = "media-info.db" + +// init 将纯Go的sqlite驱动注册为 xorm 认识的 sqlite3 驱动名(避免CGO依赖) +func init() { + sql.Register("sqlite3", &sqlite.Driver{}) +} + +// MediaInfo 媒体文件信息记录,按字段拆分存放,便于查询 +type MediaInfo struct { + Id int64 `xorm:"pk autoincr 'id'"` + FileMd5 string `xorm:"char(32) index 'file_md5'"` // 文件MD5,唯一标识 + FilePath string `xorm:"text 'file_path'"` // 扫描时的源路径 + FileName string `xorm:"varchar(255) 'file_name'"` // 文件名(含扩展名) + FileExt string `xorm:"varchar(32) 'file_ext'"` // 扩展名 + FileSize int64 `xorm:"bigint 'file_size'"` // 文件大小(字节) + MimeType string `xorm:"varchar(128) 'mime_type'"` // MIME类型 + MediaType string `xorm:"varchar(16) index 'media_type'"` // image/video/audio + Width int `xorm:"int index 'width'"` // 宽(像素) + Height int `xorm:"int index 'height'"` // 高(像素) + Duration float64 `xorm:"real 'duration'"` // 时长(秒) + Codec string `xorm:"varchar(64) 'codec'"` // 视频编码,如h264/hevc + Format string `xorm:"varchar(64) 'format'"` // 容器格式,如mp4/mkv + RawInfo string `xorm:"text 'raw_info'"` // ffprobe原始JSON(完整信息兜底) + CreatedAt time.Time `xorm:"created 'created_at'"` // 首次入库时间 + UpdatedAt time.Time `xorm:"updated 'updated_at'"` // 最近更新时间 +} + +var engine *xorm.Engine + +// Init 初始化数据库引擎并同步表结构 +func Init(dbPath string) error { + e, err := xorm.NewEngine("sqlite3", dbPath) + if err != nil { + return err + } + e.ShowSQL(false) + e.SetMaxOpenConns(1) // sqlite同一时刻只允许一个写连接 + engine = e + return engine.Sync(new(MediaInfo)) +} + +// Close 关闭数据库连接 +func Close() error { + if engine != nil { + err := engine.Close() + engine = nil + return err + } + return nil +} + +// SaveMedia 按MD5去重保存/更新媒体信息 +func SaveMedia(info *MediaInfo) error { + if engine == nil { + return nil + } + existing := new(MediaInfo) + has, err := engine.Where("file_md5 = ?", info.FileMd5).Get(existing) + if err != nil { + return err + } + if has { + info.Id = existing.Id + _, err = engine.ID(existing.Id).Update(info) + return err + } + _, err = engine.Insert(info) + return err +} diff --git a/util/cale.go b/util/cale.go index 7105688..d20bd5d 100644 --- a/util/cale.go +++ b/util/cale.go @@ -1,9 +1,7 @@ package util import ( - "fmt" "strconv" - "time" ) // String2int 字符串转int @@ -15,17 +13,6 @@ func String2int(str string) int { return intValue } -// SecondsToHms 将秒数转换为小时、分钟、秒的格式 -func SecondsToHms(seconds int) string { - t := time.Duration(seconds) * time.Second - h := t / time.Hour - t -= h * time.Hour - m := t / time.Minute - t -= m * time.Minute - s := t / time.Second - return fmt.Sprintf("%02d-%02d-%02d", h, m, s) -} - // CalcPercentage 计算percentage相对于total的百分比 func CalcPercentage(percentage int, total int) int { return int(float64(percentage) / float64(total) * 100) diff --git a/util/file.go b/util/file.go index 8a7a259..969942e 100644 --- a/util/file.go +++ b/util/file.go @@ -1,8 +1,11 @@ package util import ( + "crypto/md5" + "encoding/hex" "fmt" "github.com/gabriel-vasile/mimetype" + "io" "io/ioutil" "os" "path/filepath" @@ -44,6 +47,20 @@ func WriteByteArraysToFile(content []byte, filename string) error { return ioutil.WriteFile(filename, content, 0777) } +// CalcFileMd5 计算文件MD5(分块读取,适合大文件) +func CalcFileMd5(filePath string) (string, error) { + file, err := os.Open(filePath) + if err != nil { + return "", err + } + defer file.Close() + h := md5.New() + if _, err := io.Copy(h, file); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} + //// 移动文件到根目录 //func renameFile(rootDir string, modelType string, videoList []string, pathSeparator string) { // total := len(videoList) diff --git a/util/video.go b/util/video.go index 5c03485..5d105e3 100644 --- a/util/video.go +++ b/util/video.go @@ -1,140 +1,165 @@ package util import ( - "bytes" + "encoding/json" "fmt" "io" - "math" "os" "os/exec" "path/filepath" "strconv" "strings" + "kenaito-media-picker/vars" + "github.com/redmask-hb/GoSimplePrint/goPrint" uuid "github.com/satori/go.uuid" ) -var supportVideoTypes = []string{ - ".ts", - ".flv", - ".rm", - ".avi", - ".mp4", - ".mov", - ".mpg", - ".mkv", - ".m4v", - ".rmvb", - ".3gp", - ".3g2", - ".webm", - ".wmv", +// FFprobeExeName 内嵌解码器文件名 +const FFprobeExeName = "ffprobe.exe" + +// FFprobeExePath 返回带当前目录前缀的可执行路径(Windows/Unix均可直接执行) +func FFprobeExePath() string { + return "." + string(os.PathSeparator) + FFprobeExeName } -func getVideoDuration(ffmpegExecPath string, videoPath string) (float64, error) { - // ffprobe命令,-v error 用于减少输出信息,-show_entries format=duration -of compact=p=0,nk=1 用于只输出时长 - cmd := exec.Command(ffmpegExecPath, "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoPath) - var stderr bytes.Buffer - cmd.Stderr = &stderr - - output, err := cmd.Output() - if err != nil { - return 0, fmt.Errorf("ffprobe failed with error: %v, stderr: %q", err, stderr.String()) - } - - // 解析输出的时长字符串为浮点数 - durationStr := strings.TrimSpace(string(output)) - duration, err := strconv.ParseFloat(durationStr, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse duration: %q, error: %v", durationStr, err) - } - - return duration, nil +// ReleaseFFprobe 释放内嵌的ffprobe到当前目录 +func ReleaseFFprobe() error { + return WriteByteArraysToFile(vars.VideoDecodeToolWin64, FFprobeExeName) } -func getVideoResolution(ffmpegExecPath string, filePath string) (width int, height int, err error) { - // 构建ffprobe命令 - cmd := exec.Command(ffmpegExecPath, "-v", "error", "-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", filePath) - // 执行命令并捕获输出 - output, err := cmd.Output() - if err != nil { - return 0, 0, fmt.Errorf("failed to run ffprobe: %w", err) +// RemoveFFprobe 删除当前目录下的ffprobe +func RemoveFFprobe() { + if CheckFileIsExist(FFprobeExeName) { + _ = os.Remove(FFprobeExeName) } - // 解析输出字符串,格式应为 "宽度,高度" - resolutionStr := strings.TrimSpace(string(output)) - parts := strings.Split(resolutionStr, ",") - if len(parts) == 2 { - width = String2int(parts[0]) - height = String2int(parts[1]) - return width, height, nil - } - parts = strings.Split(resolutionStr, "x") - if len(parts) == 2 { - width = String2int(parts[0]) - height = String2int(parts[1]) - return width, height, nil - } - parts = strings.Split(resolutionStr, "\r\n\r\n\r\n\r\n") - if len(parts) == 2 { - tempHw := parts[0] - parts = strings.Split(tempHw, "x") - if len(parts) == 2 { - width = String2int(parts[0]) - height = String2int(parts[1]) - return width, height, nil +} + +// StreamInfo ffprobe流信息 +type StreamInfo struct { + Index int `json:"index"` + CodecType string `json:"codec_type"` + CodecName string `json:"codec_name"` + Width int `json:"width"` + Height int `json:"height"` +} + +// FormatInfo ffprobe容器信息 +type FormatInfo struct { + Duration string `json:"duration"` + Format string `json:"format_name"` +} + +// ProbeResult ffprobe探测结果 +type ProbeResult struct { + Streams []StreamInfo `json:"streams"` + Format FormatInfo `json:"format"` + raw string +} + +// VideoResolution 返回第一个有效的视频分辨率 +func (p *ProbeResult) VideoResolution() (int, int) { + for _, s := range p.Streams { + if s.CodecType == "video" && s.Width > 0 && s.Height > 0 { + return s.Width, s.Height } } - parts = strings.Split(resolutionStr, "x") - if len(parts) == 3 { - width = String2int(parts[0]) - height = String2int(parts[1]) - return width, height, nil - } - resolutionStr = strings.ReplaceAll(resolutionStr, "\r", "") - resolutionStr = strings.ReplaceAll(resolutionStr, "\n", "") - resolutionStr = strings.ReplaceAll(resolutionStr, "N/AxN/A", "") - parts = strings.Split(resolutionStr, "x") - if len(parts) == 2 { - width = String2int(parts[0]) - height = String2int(parts[1]) - return width, height, nil - } - return 0, 0, fmt.Errorf("invalid resolution format: %s", resolutionStr) + return 0, 0 } -// ReadVideoDuration 获取视频的时长,单位秒 -func ReadVideoDuration(videoFilePath string) int { - duration, err := getVideoDuration("./ffprobe.exe", videoFilePath) - if err != nil { - fmt.Println("=== Error getting video duration:", err) - return 0 - } - //fmt.Printf("=== Video duration: %.2f seconds\n", duration) - return int(math.Floor(duration)) // 向下取整 -} - -// ReadVideoWidthHeight 获取视频的分辨率 -func ReadVideoWidthHeight(videoFilePath string) (int, int, error) { - width, height, err := getVideoResolution("./ffprobe.exe", videoFilePath) - if err != nil { - fmt.Printf("=== Error getting resolution: %v\n", err) - return 0, 0, err - } - //fmt.Printf("=== Video resolution: %dx%d\n", width, height) - return width, height, nil -} - -// IsSupportVideo 判断是否属于支持的视频 -func IsSupportVideo(videoType string) bool { - for _, supportVideoType := range supportVideoTypes { - if strings.EqualFold(videoType, supportVideoType) { +// HasAudio 是否包含音频流 +func (p *ProbeResult) HasAudio() bool { + for _, s := range p.Streams { + if s.CodecType == "audio" { return true } } return false } +// VideoCodec 第一个视频流的编码名称 +func (p *ProbeResult) VideoCodec() string { + for _, s := range p.Streams { + if s.CodecType == "video" { + return s.CodecName + } + } + return "" +} + +// AudioCodec 第一个音频流的编码名称 +func (p *ProbeResult) AudioCodec() string { + for _, s := range p.Streams { + if s.CodecType == "audio" { + return s.CodecName + } + } + return "" +} + +// FormatName 容器格式名称 +func (p *ProbeResult) FormatName() string { + return p.Format.Format +} + +// RawJSON 原始ffprobe JSON输出 +func (p *ProbeResult) RawJSON() string { + return p.raw +} + +// DurationSeconds 容器时长(秒),无法解析时返回0 +func (p *ProbeResult) DurationSeconds() float64 { + d, err := strconv.ParseFloat(p.Format.Duration, 64) + if err != nil { + return 0 + } + return d +} + +// ProbeMediaFile 用ffprobe探测媒体文件,一次调用同时获取视频/音频流与时长 +func ProbeMediaFile(ffprobeExecPath string, filePath string) (*ProbeResult, error) { + cmd := exec.Command(ffprobeExecPath, "-v", "error", + "-show_entries", "stream=index,codec_type,codec_name,width,height:format=duration,format_name", + "-of", "json", filePath) + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("ffprobe failed: %w", err) + } + var result ProbeResult + if err := json.Unmarshal(output, &result); err != nil { + return nil, fmt.Errorf("failed to parse ffprobe output: %w", err) + } + result.raw = string(output) + return &result, nil +} + +// IsPotentialMedia 判断是否可能是媒体文件(交由ffprobe进一步确认) +func IsPotentialMedia(fileMime string) bool { + if fileMime == "" || fileMime == "application/octet-stream" { + return true + } + if strings.HasPrefix(fileMime, "text/") { + return false + } + if strings.HasPrefix(fileMime, "video/") || strings.HasPrefix(fileMime, "audio/") || strings.HasPrefix(fileMime, "image/") { + return true + } + switch fileMime { + case "application/zip", "application/gzip", "application/x-gzip", "application/pdf", + "application/json", "application/xml", "application/x-httpd-php", "application/x-sh", + "application/javascript", "application/x-7z-compressed", "application/x-rar-compressed", + "application/x-tar", "application/x-bzip2", "application/x-archive", "application/vnd.sqlite3", + "application/vnd.microsoft.portable-executable", "application/x-dosexec", "application/x-executable", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint": + return false + } + return true +} + // DoMoveFileToDir 批量移动文件到目录 func DoMoveFileToDir(filePathList []string, videoDirPath string) { total := len(filePathList) diff --git a/vars/global.go b/vars/global.go index 7ee34eb..73bbd66 100644 --- a/vars/global.go +++ b/vars/global.go @@ -4,6 +4,7 @@ var GlobalFilePathList []string // 所有文件的路 var GlobalFilePath2MimeInfoMap = make(map[string]string) // 文件路径和mime信息的映射关系 var GlobalFilePath2FileNameMap = make(map[string]string) // 文件路径和文件名称的映射关系 var GlobalFilePath2FileExtMap = make(map[string]string) // 文件路径和文件扩展名的映射关系 -var GlobalVideoPathList []string // 所有视频文件的路径 var GlobalImagePathList []string // 所有图片文件的路径 +var GlobalMediaPathList []string // 待ffprobe确认的媒体候选文件 +var GlobalAudioPathList []string // 已确认的音频文件路径 var GlobalPickerDir string // 已整理文件的输出目录(白名单,扫描时跳过) diff --git a/vars/video.go b/vars/video.go index 8be6ceb..d287ab8 100644 --- a/vars/video.go +++ b/vars/video.go @@ -6,11 +6,9 @@ import ( //go:embed ffprobe.exe var VideoDecodeToolWin64 []byte -var VideoIgnorePathList []string // 忽略的文件路径 -var VideoReadErrorPathList []string // 读取信息异常的路径 -var VideoPath2WidthHeightMap = make(map[string]string) // 视频路径和宽高比 -var VideoPath2WidthHeightTagMap = make(map[string]string) // 视频路径和宽高比[640x480] -var VideoPath2DurationMap = make(map[string]string) // 视频路径和时长 +var VideoIgnorePathList []string // 忽略的文件路径 +var VideoReadErrorPathList []string // 读取信息异常的路径 +var VideoPath2WidthHeightMap = make(map[string]string) // 视频路径和宽高比 var VideoHorizontalNormalList []string var VideoHorizontalGifList []string var VideoHorizontal1KList []string