Files
odboy 8979c744a8 refactor: 全面重构代码并加白话注释,移动全局并行,去掉去向/重复记录
- 重构后端与前端,全程大白话注释(不懂程序也能看懂)
- 移动阶段改为全局并行(MoveAll,工人数=CPU核心数)
- 去掉记录每个文件搬去哪了(删除去向回填,解决写库卡顿)
- 去掉重复文件标记与统计:重复判定改为 MD5+路径都相同才算,重复则原地忽略不移动;MD5 相同但路径不同仍照常搬
- 媒体库去掉归类目录列,看板去掉重复/归类分布统计
2025-05-11 21:05:35 +08:00

106 lines
3.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package probe 负责给媒体文件"体检":
// - 图片:读出它的宽高(尺寸),特殊格式(WebP/BMP/PSD)有专门的读法
// - 视频/音频:调 ffprobe 程序读出编码、时长、宽高等信息
// - 明暗分析:图片是偏暗还是偏亮(给图片分类用)
//
// 大白话理解:这一步负责搞清楚"这个文件到底是什么样的",好让后面决定把它归到哪。
// 体检结果统一放进 ProbeResult 这一个结构里,图片、视频、音频都往里填。
package probe
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strconv"
"time"
)
// ProbeResult 一份"体检报告",图片/视频/音频通用。
type ProbeResult struct {
HasVideo bool // 有没有视频流
HasAudio bool // 有没有音频流
Width int // 宽(像素)
Height int // 高(像素)
Duration float64 // 时长(秒)
VideoCodec string // 视频编码(如 h264)
AudioCodec string // 音频编码(如 aac)
Format string // 容器格式(如 mp4)
RawJSON string // ffprobe 读出的原始信息,存下来留底
}
// streamInfo 是 ffprobe 输出里"一条流"的 JSON 结构(视频流/音频流都会列出来)。
type streamInfo struct {
Index int `json:"index"`
CodecType string `json:"codec_type"` // video / audio
CodecName string `json:"codec_name"`
Width int `json:"width"`
Height int `json:"height"`
}
// formatInfo 是 ffprobe 输出里"容器格式"部分的 JSON 结构。
type formatInfo struct {
Duration string `json:"duration"`
Format string `json:"format_name"`
}
// probeOutput 是 ffprobe 完整输出的 JSON 结构。
type probeOutput struct {
Streams []streamInfo `json:"streams"`
Format formatInfo `json:"format"`
}
// ffprobeTimeout 调 ffprobe 的超时时间。超过就当探测失败,
// 免得遇到损坏文件时卡住整个程序。
const ffprobeTimeout = 30 * time.Second
// ProbeMediaFile 调 ffprobe 探测一个媒体文件,一次调用就把
// 视频流/音频流/时长/格式全部拿到。
func ProbeMediaFile(ffprobePath, filePath string) (*ProbeResult, error) {
// 带超时的执行环境:文件坏了也不至于永远卡住
ctx, cancel := context.WithTimeout(context.Background(), ffprobeTimeout)
defer cancel()
// 用 ffprobe 读信息,指定输出 JSON 格式
cmd := exec.CommandContext(ctx, ffprobePath, "-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)
}
// 把 JSON 输出翻译成结构体
var out probeOutput
if err := json.Unmarshal(output, &out); err != nil {
return nil, fmt.Errorf("parse ffprobe output: %w", err)
}
res := &ProbeResult{
Format: out.Format.Format,
RawJSON: string(output),
}
// 时长(秒)
if d, err := strconv.ParseFloat(out.Format.Duration, 64); err == nil {
res.Duration = d
}
// 把每条流的信息填进报告:
// 视频流 → 记下"有视频",补上编码和宽高;
// 音频流 → 记下"有音频",补上编码。
for _, s := range out.Streams {
switch s.CodecType {
case "video":
res.HasVideo = true
if res.VideoCodec == "" {
res.VideoCodec = s.CodecName
}
if res.Width == 0 && s.Width > 0 && s.Height > 0 {
res.Width, res.Height = s.Width, s.Height
}
case "audio":
res.HasAudio = true
if res.AudioCodec == "" {
res.AudioCodec = s.CodecName
}
}
}
return res, nil
}