feat(core): 添加PSD/PSB图片格式支持并重构媒体文件处理
- 新增ReadPsdImage函数支持PSD/PSB格式图片信息读取 - 修改文件扫描逻辑,将视频处理改为媒体候选处理,使用ffprobe统一判定 - 添加对PSB格式的支持,与PSD共享相同8BPS文件头处理 - 重构handle_image.go中的图片处理逻辑,优化错误处理和记录保存 - 更新文件扫描过滤规则,支持更多图片格式并跳过数据库文件 - 添加文件MD5计算功能用于文件唯一性标识 - 修改媒体处理流程,支持音频文件识别和分类处理 - 移除废弃的视频时长转换函数和相关依赖 - 更新依赖版本并添加SQLite支持用于媒体信息存储
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+132
-107
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user