Files
kenaito-media-picker/internal/scanner/scanner.go
T
odboy 39fe02b424 fix: 未知 MIME 文件不再被移入忽略目录
- scanner: 新增 IsExplicitMediaMime 判断明确媒体 MIME
- organize: 仅 video/audio MIME 文件移入异常/忽略目录
- octet-stream/空 MIME 且 ffprobe 无法确认的文件原地忽略,不移动
2025-04-10 21:05:14 +08:00

145 lines
4.3 KiB
Go
Raw 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 scanner 负责递归扫描文件、读取 MIME 并按类型过滤。
package scanner
import (
"fmt"
"os"
"path/filepath"
"strings"
"kenaito-media-picker/internal/probe"
"github.com/gabriel-vasile/mimetype"
"github.com/redmask-hb/GoSimplePrint/goPrint"
)
// File 扫描到的文件信息
type File struct {
Path string // 完整路径
Name string // 文件名(含扩展名)
Ext string // 扩展名(含点)
Mime string // MIME 类型
}
// Result 过滤结果
type Result struct {
Files []File // 全部文件
Images []File // 图片文件
Media []File // 待 ffprobe 确认的媒体候选
}
// Options 扫描选项
type Options struct {
RootDir string // 扫描根目录
SkipDir string // 白名单输出目录,整目录跳过
SkipNames []string // 按文件名跳过(数据库、ffprobe 等)
}
// Scan 递归扫描目录并读取每个文件的 MIME 信息
func Scan(opts Options) (*Result, error) {
res := &Result{}
err := filepath.Walk(opts.RootDir, func(currentPath string, info os.FileInfo, err error) error {
if err != nil {
// 单个路径访问失败(权限不足/损坏)不中止整体扫描,仅告警后跳过该子树
fmt.Printf("=== 扫描跳过(无法访问): %s, 错误: %v\n", currentPath, err)
return nil
}
if info.IsDir() {
if opts.SkipDir != "" && currentPath == opts.SkipDir {
return filepath.SkipDir
}
return nil
}
for _, n := range opts.SkipNames {
if info.Name() == n {
return nil
}
}
res.Files = append(res.Files, File{
Path: currentPath,
Name: info.Name(),
Ext: filepath.Ext(currentPath),
})
return nil
})
if err != nil {
return nil, err
}
total := len(res.Files)
fmt.Printf("=== 文件总数: %d \n", total)
bar := goPrint.NewBar(100)
bar.SetNotice("=== 扫描文件Mime:")
bar.SetGraph(">")
bar.SetNoticeColor(goPrint.FontColor.Red)
for i := range res.Files {
res.Files[i].Mime = detectMime(res.Files[i].Path)
bar.PrintBar(percent(i+1, total))
}
bar.PrintEnd("=== Finish")
return res, nil
}
// Filter 将文件分为图片与媒体候选
func (r *Result) Filter() {
for _, f := range r.Files {
if strings.Contains(f.Mime, "image/") || probe.IsSupportImage(f.Ext) || strings.EqualFold(f.Ext, ".psd") {
r.Images = append(r.Images, f)
continue
}
if isPotentialMedia(f.Mime) {
r.Media = append(r.Media, f)
continue
}
// 明显的非媒体文件(代码/文档/压缩包等)直接不处理
}
}
// detectMime 读取文件的 MIME 类型
func detectMime(filePath string) string {
mt, err := mimetype.DetectFile(filePath)
if err != nil {
return ""
}
return mt.String()
}
// IsExplicitMediaMime 判断 MIME 是否明确属于视频/音频(区别于 octet-stream/空 等无法确定的情况)
func IsExplicitMediaMime(fileMime string) bool {
return strings.HasPrefix(fileMime, "video/") || strings.HasPrefix(fileMime, "audio/")
}
// 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
}
// percent 计算百分比(0~100),total 为 0 时返回 100
func percent(part, total int) int {
if total <= 0 {
return 100
}
return int(float64(part) / float64(total) * 100)
}