diff --git a/server/cmd/promotion-import/main.go b/server/cmd/promotion-import/main.go index 7c2b348..a9768dc 100644 --- a/server/cmd/promotion-import/main.go +++ b/server/cmd/promotion-import/main.go @@ -57,11 +57,12 @@ func mimeForExt(ext string) string { } } -// importRule:按顺序尝试 SrcRels;若均不存在且 FallbackScanDir 非空,则在该子目录下「仅一个 .mov/.jpg 时」自动选用(兼容实际文件夹命名与文件名不一致) +// importRule:按顺序尝试 SrcRels;再 FallbackScanDir 下智能选文件;EpisodeScan 在非空时扫描 视频发布 下含「实例+一/二」的子目录(兼容半角括号、文件夹名略有差异) type importRule struct { SrcRels []string Dst string - FallbackScanDir string // 相对 视频发布/,仅当目标为视频时用 .mov;封面用 .jpg + FallbackScanDir string + EpisodeScan string // "一" 或 "二" } // 与 sync-video-assets-to-social.sh 对齐,并增加备选路径(线上常见「实例(一)」内不叫宣传片.mov 的情况) @@ -69,25 +70,155 @@ var mappings = []importRule{ {[]string{ "宇恒一号操作计算软件实例(一)/宣传片-封面.jpg", "宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一)-封面.jpg", - }, "video-calc-demo-1-cover.jpg", "宇恒一号操作计算软件实例(一)"}, + "宇恒一号操作计算软件实例(一)/宣传片-封面.jpg", + "宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一)-封面.jpg", + }, "video-calc-demo-1-cover.jpg", "宇恒一号操作计算软件实例(一)", "一"}, {[]string{ "宇恒一号操作计算软件实例(一)/宣传片.mov", "宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一).mov", - }, "video-calc-demo-1.mov", "宇恒一号操作计算软件实例(一)"}, + "宇恒一号操作计算软件实例(一)/宣传片.mov", + "宇恒一号操作计算软件实例(一)/宇恒一号操作计算软件实例(一).mov", + }, "video-calc-demo-1.mov", "宇恒一号操作计算软件实例(一)", "一"}, {[]string{ "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg", "宇恒一号操作计算软件实例(二)/宣传片-封面.jpg", - }, "video-calc-demo-2-cover.jpg", "宇恒一号操作计算软件实例(二)"}, + "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二)-封面.jpg", + "宇恒一号操作计算软件实例(二)/宣传片-封面.jpg", + }, "video-calc-demo-2-cover.jpg", "宇恒一号操作计算软件实例(二)", "二"}, {[]string{ "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov", "宇恒一号操作计算软件实例(二)/宣传片.mov", - }, "video-calc-demo-2.mov", "宇恒一号操作计算软件实例(二)"}, - {[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介-封面.jpg"}, "video-aiword-cover.jpg", "宇恒一号AIWord简介"}, - {[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介.mov"}, "video-aiword.mov", "宇恒一号AIWord简介"}, - {[]string{"宇恒一号语音办公实例/宇恒一号语音办公实例-封面.jpg"}, "video-voice-office-cover.jpg", "宇恒一号语音办公实例"}, - {[]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 全自动办发票"}, + "宇恒一号操作计算软件实例(二)/宇恒一号操作计算软件实例(二).mov", + "宇恒一号操作计算软件实例(二)/宣传片.mov", + }, "video-calc-demo-2.mov", "宇恒一号操作计算软件实例(二)", "二"}, + {[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介-封面.jpg"}, "video-aiword-cover.jpg", "宇恒一号AIWord简介", ""}, + {[]string{"宇恒一号AIWord简介/宇恒一号AIWord简介.mov"}, "video-aiword.mov", "宇恒一号AIWord简介", ""}, + {[]string{"宇恒一号语音办公实例/宇恒一号语音办公实例-封面.jpg"}, "video-voice-office-cover.jpg", "宇恒一号语音办公实例", ""}, + {[]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) { @@ -97,37 +228,31 @@ func resolveSourceFile(videoPublish string, rule importRule) (absPath, relChosen return p, rel, true } } - if rule.FallbackScanDir == "" { - return "", "", false + tryDirs := []string{} + if rule.FallbackScanDir != "" { + tryDirs = append(tryDirs, rule.FallbackScanDir) } - ext := strings.ToLower(filepath.Ext(rule.Dst)) - if ext != ".mov" && ext != ".jpg" && ext != ".jpeg" { - return "", "", false - } - dir := filepath.Join(videoPublish, filepath.FromSlash(rule.FallbackScanDir)) - entries, err := os.ReadDir(dir) - if err != nil { - return "", "", false - } - var matches []string - for _, e := range entries { - if e.IsDir() { - 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 rule.EpisodeScan != "" { + if d, ok := discoverEpisodeDir(videoPublish, rule.EpisodeScan); ok { + // 避免与固定目录重复 + dup := false + for _, x := range tryDirs { + if x == d { + dup = true + break + } + } + if !dup { + tryDirs = append(tryDirs, d) + } } } - if len(matches) != 1 { - return "", "", false + for _, dirName := range tryDirs { + if abs, rel, ok := pickMediaInDir(videoPublish, dirName, rule.Dst); ok { + return abs, rel, true + } } - rel := filepath.ToSlash(filepath.Join(rule.FallbackScanDir, matches[0])) - p := filepath.Join(videoPublish, filepath.FromSlash(rel)) - return p, rel, true + return "", "", false } func main() { @@ -202,7 +327,7 @@ func main() { for _, m := range mappings { from, srcRelUsed, found := resolveSourceFile(videoPublish, m) 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++ continue }