fix(promotion-import): 扫描实例(一)(二)目录、多mov取最大文件

Made-with: Cursor
This commit is contained in:
whm
2026-03-21 21:58:57 +08:00
parent 6d049fe0e8
commit d04799db5f

View File

@@ -57,11 +57,12 @@ func mimeForExt(ext string) string {
} }
} }
// importRule按顺序尝试 SrcRels若均不存在且 FallbackScanDir 非空,则在该子目录下「仅一个 .mov/.jpg 时」自动选用(兼容实际文件夹命名与文件名不一致 // importRule按顺序尝试 SrcRels FallbackScanDir 下智能选文件EpisodeScan 在非空时扫描 视频发布 下含「实例+一/二」的子目录(兼容半角括号、文件夹名略有差异
type importRule struct { type importRule struct {
SrcRels []string SrcRels []string
Dst string Dst string
FallbackScanDir string // 相对 视频发布/,仅当目标为视频时用 .mov封面用 .jpg FallbackScanDir string
EpisodeScan string // "一" 或 "二"
} }
// 与 sync-video-assets-to-social.sh 对齐,并增加备选路径(线上常见「实例(一)」内不叫宣传片.mov 的情况) // 与 sync-video-assets-to-social.sh 对齐,并增加备选路径(线上常见「实例(一)」内不叫宣传片.mov 的情况)
@@ -69,25 +70,155 @@ var mappings = []importRule{
{[]string{ {[]string{
"宇恒一号操作计算软件实例(一)/宣传片-封面.jpg", "宇恒一号操作计算软件实例(一)/宣传片-封面.jpg",
"宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一)-封面.jpg", "宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一)-封面.jpg",
}, "video-calc-demo-1-cover.jpg", "宇恒一号操作计算软件实例(一)"}, "宇恒一号操作计算软件实例(一)/宣传片-封面.jpg",
"宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一)-封面.jpg",
}, "video-calc-demo-1-cover.jpg", "宇恒一号操作计算软件实例(一)", "一"},
{[]string{ {[]string{
"宇恒一号操作计算软件实例(一)/宣传片.mov", "宇恒一号操作计算软件实例(一)/宣传片.mov",
"宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一).mov", "宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一).mov",
}, "video-calc-demo-1.mov", "宇恒一号操作计算软件实例(一)"}, "宇恒一号操作计算软件实例(一)/宣传片.mov",
"宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一).mov",
}, "video-calc-demo-1.mov", "宇恒一号操作计算软件实例(一)", "一"},
{[]string{ {[]string{
"宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg", "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg",
"宇恒一号操作计算软件实例(二)/宣传片-封面.jpg", "宇恒一号操作计算软件实例(二)/宣传片-封面.jpg",
}, "video-calc-demo-2-cover.jpg", "宇恒一号操作计算软件实例(二)"}, "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg",
"宇恒一号操作计算软件实例(二)/宣传片-封面.jpg",
}, "video-calc-demo-2-cover.jpg", "宇恒一号操作计算软件实例(二)", "二"},
{[]string{ {[]string{
"宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov", "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov",
"宇恒一号操作计算软件实例(二)/宣传片.mov", "宇恒一号操作计算软件实例(二)/宣传片.mov",
}, "video-calc-demo-2.mov", "宇恒一号操作计算软件实例(二)"}, "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov",
{[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介-封面.jpg"}, "video-aiword-cover.jpg", "宇恒一号AIWord简介"}, "宇恒一号操作计算软件实例(二)/宣传片.mov",
{[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介.mov"}, "video-aiword.mov", "宇恒一号AIWord简介"}, }, "video-calc-demo-2.mov", "宇恒一号操作计算软件实例(二)", "二"},
{[]string{"宇恒一号语音办公实例/宇恒一号语音办公实例-封面.jpg"}, "video-voice-office-cover.jpg", "宇恒一号语音办公实例"}, {[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介-封面.jpg"}, "video-aiword-cover.jpg", "宇恒一号AIWord简介", ""},
{[]string{"宇恒一号语音办公实例/宇恒一号语音办公实例.mov"}, "video-voice-office.mov", "宇恒一号语音办公实例"}, {[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介.mov"}, "video-aiword.mov", "宇恒一号AIWord简介", ""},
{[]string{"宇恒一号AI 全自动办发票/宇恒一号AI 全自动办发票-封面.jpg"}, "video-invoice-ai-cover.jpg", "宇恒一号AI 全自动办发票"}, {[]string{"宇恒一号语音办公实例/宇恒一号语音办公实例-封面.jpg"}, "video-voice-office-cover.jpg", "宇恒一号语音办公实例", ""},
{[]string{"宇恒一号AI 全自动办发票/宇恒一号AI 全自动办发票.mov"}, "video-invoice-ai.mov", "宇恒一号AI 全自动办发票"}, {[]string{"宇恒一号语音办公实例/宇恒一号语音办公实例.mov"}, "video-voice-office.mov", "宇恒一号语音办公实例", ""},
{[]string{"宇恒一号AI 全自动办发票/宇恒一号AI 全自动办发票-封面.jpg"}, "video-invoice-ai-cover.jpg", "宇恒一号AI 全自动办发票", ""},
{[]string{"宇恒一号AI 全自动办发票/宇恒一号AI 全自动办发票.mov"}, "video-invoice-ai.mov", "宇恒一号AI 全自动办发票", ""},
}
// 在 视频发布 下找名称同时含「实例」与「(一)」或「(一)」等的子目录(排除另一集)
func discoverEpisodeDir(videoPublish, episode string) (dirName string, ok bool) {
full := "" + episode + ""
half := "(" + episode + ")"
entries, err := os.ReadDir(videoPublish)
if err != nil {
return "", false
}
var hits []string
for _, e := range entries {
if !e.IsDir() {
continue
}
n := e.Name()
if !strings.Contains(n, "实例") {
continue
}
marked := strings.Contains(n, full) || strings.Contains(n, half)
if !marked {
continue
}
if episode == "一" && (strings.Contains(n, "(二)") || strings.Contains(n, "(二)")) {
continue
}
if episode == "二" && (strings.Contains(n, "(一)") || strings.Contains(n, "(一)")) {
continue
}
hits = append(hits, n)
}
if len(hits) == 0 {
return "", false
}
if len(hits) == 1 {
return hits[0], true
}
for _, h := range hits {
if strings.Contains(h, "软件") {
return h, true
}
}
return hits[0], true
}
func pickMediaInDir(videoPublish, dirName string, dstFile string) (absPath, relChosen string, ok bool) {
ext := strings.ToLower(filepath.Ext(dstFile))
if ext != ".mov" && ext != ".jpg" && ext != ".jpeg" {
return "", "", false
}
dir := filepath.Join(videoPublish, filepath.FromSlash(dirName))
entries, err := os.ReadDir(dir)
if err != nil {
return "", "", false
}
type cand struct {
name string
size int64
}
var movs, imgs []cand
for _, e := range entries {
if e.IsDir() {
continue
}
ne := strings.ToLower(filepath.Ext(e.Name()))
p := filepath.Join(dir, e.Name())
st, err := os.Stat(p)
if err != nil {
continue
}
sz := st.Size()
if ne == ".mov" {
movs = append(movs, cand{e.Name(), sz})
}
if ne == ".jpg" || ne == ".jpeg" {
imgs = append(imgs, cand{e.Name(), sz})
}
}
pickMov := func() (string, bool) {
if len(movs) == 0 {
return "", false
}
best := movs[0]
for _, c := range movs[1:] {
if c.size > best.size {
best = c
}
}
return best.name, true
}
pickImg := func() (string, bool) {
if len(imgs) == 0 {
return "", false
}
for _, c := range imgs {
if strings.Contains(c.name, "封面") {
return c.name, true
}
}
best := imgs[0]
for _, c := range imgs[1:] {
if c.size > best.size {
best = c
}
}
return best.name, true
}
var name string
var found bool
switch ext {
case ".mov":
name, found = pickMov()
case ".jpg", ".jpeg":
name, found = pickImg()
default:
return "", "", false
}
if !found {
return "", "", false
}
rel := filepath.ToSlash(filepath.Join(dirName, name))
return filepath.Join(videoPublish, filepath.FromSlash(rel)), rel, true
} }
func resolveSourceFile(videoPublish string, rule importRule) (absPath, relChosen string, ok bool) { func resolveSourceFile(videoPublish string, rule importRule) (absPath, relChosen string, ok bool) {
@@ -97,37 +228,31 @@ func resolveSourceFile(videoPublish string, rule importRule) (absPath, relChosen
return p, rel, true return p, rel, true
} }
} }
if rule.FallbackScanDir == "" { tryDirs := []string{}
return "", "", false if rule.FallbackScanDir != "" {
tryDirs = append(tryDirs, rule.FallbackScanDir)
} }
ext := strings.ToLower(filepath.Ext(rule.Dst)) if rule.EpisodeScan != "" {
if ext != ".mov" && ext != ".jpg" && ext != ".jpeg" { if d, ok := discoverEpisodeDir(videoPublish, rule.EpisodeScan); ok {
return "", "", false // 避免与固定目录重复
} dup := false
dir := filepath.Join(videoPublish, filepath.FromSlash(rule.FallbackScanDir)) for _, x := range tryDirs {
entries, err := os.ReadDir(dir) if x == d {
if err != nil { dup = true
return "", "", false break
} }
var matches []string }
for _, e := range entries { if !dup {
if e.IsDir() { tryDirs = append(tryDirs, d)
continue }
}
ne := strings.ToLower(filepath.Ext(e.Name()))
if ext == ".mov" && ne == ".mov" {
matches = append(matches, e.Name())
}
if (ext == ".jpg" || ext == ".jpeg") && (ne == ".jpg" || ne == ".jpeg") {
matches = append(matches, e.Name())
} }
} }
if len(matches) != 1 { for _, dirName := range tryDirs {
return "", "", false if abs, rel, ok := pickMediaInDir(videoPublish, dirName, rule.Dst); ok {
return abs, rel, true
}
} }
rel := filepath.ToSlash(filepath.Join(rule.FallbackScanDir, matches[0])) return "", "", false
p := filepath.Join(videoPublish, filepath.FromSlash(rel))
return p, rel, true
} }
func main() { func main() {
@@ -202,7 +327,7 @@ func main() {
for _, m := range mappings { for _, m := range mappings {
from, srcRelUsed, found := resolveSourceFile(videoPublish, m) from, srcRelUsed, found := resolveSourceFile(videoPublish, m)
if !found { if !found {
log.Printf("SKIP 源文件不存在(已试备选路径): dst=%s dir=%s", m.Dst, m.FallbackScanDir) log.Printf("SKIP 源文件不存在(已试备选路径/扫描子目录: dst=%s episode=%s", m.Dst, m.EpisodeScan)
skip++ skip++
continue continue
} }