diff --git a/server/cmd/promotion-import/README.md b/server/cmd/promotion-import/README.md index 00ac8d2..591298f 100644 --- a/server/cmd/promotion-import/README.md +++ b/server/cmd/promotion-import/README.md @@ -2,7 +2,7 @@ 将 `web/promotion/视频发布/` 下映射表中的文件复制到 **`{upload}/sites/{site_id}/promotion/social/`**,并在 **`site_assets`** 集合插入记录(与后台「保留原文件名」上传到 `promotion/social` 一致)。 -对「操作与计算(一)(二)」等条目会**按顺序尝试多个源文件名**;若仍找不到且子目录内**恰好只有一个** `.mov` 或 `.jpg`,会自动选用(解决「有些视频有、有些 404」多为源文件名与映射不一致)。 +对「操作与计算(一)(二)」会尝试多组路径名、半角括号、子目录内**最大** `.mov`;若仍无法按「一/二」识别文件夹,会在 `视频发布` 下找出**恰好两个**含「实例」的兄弟目录(排除 AIWord/语音/发票),排序后**第一个 → demo-1、第二个 → demo-2**(文件夹名不含「一」也能配对)。 ## 参数 diff --git a/server/cmd/promotion-import/main.go b/server/cmd/promotion-import/main.go index a9768dc..ea3e5e5 100644 --- a/server/cmd/promotion-import/main.go +++ b/server/cmd/promotion-import/main.go @@ -17,6 +17,7 @@ import ( "log" "os" "path/filepath" + "sort" "strings" "time" @@ -142,6 +143,107 @@ func discoverEpisodeDir(videoPublish, episode string) (dirName string, ok bool) return hits[0], true } +func dirHasMov(dir string) bool { + entries, err := os.ReadDir(dir) + if err != nil { + return false + } + for _, e := range entries { + if e.IsDir() { + continue + } + if strings.EqualFold(filepath.Ext(e.Name()), ".mov") { + return true + } + } + return false +} + +func dirHasJpeg(dir string) bool { + entries, err := os.ReadDir(dir) + if err != nil { + return false + } + for _, e := range entries { + if e.IsDir() { + continue + } + ext := strings.ToLower(filepath.Ext(e.Name())) + if ext == ".jpg" || ext == ".jpeg" { + return true + } + } + return false +} + +// 列出「操作与计算」类子目录:含「实例」、内有 .mov 或 .jpg(避免仅封面无 mov 的(一)被漏掉),排除 AIWord/语音/发票等 +func listCalcInstanceDirsForPairing(videoPublish string) []string { + entries, err := os.ReadDir(videoPublish) + if err != nil { + return nil + } + skipSubstr := []string{"AIWord", "语音", "发票", "全自动"} + var out []string +outer: + for _, e := range entries { + if !e.IsDir() { + continue + } + n := e.Name() + if !strings.Contains(n, "实例") { + continue + } + for _, s := range skipSubstr { + if strings.Contains(n, s) { + continue outer + } + } + sub := filepath.Join(videoPublish, n) + if !dirHasMov(sub) && !dirHasJpeg(sub) { + continue + } + out = append(out, n) + } + return out +} + +// 将目录排序为 demo-1 在前、demo-2 在后(优先认全角/半角「一」「二」标记) +func orderCalcDirsForDemo12(dirs []string) []string { + if len(dirs) <= 1 { + return dirs + } + type scored struct { + name string + prio int + } + var xs []scored + for _, d := range dirs { + p := 100 + switch { + case strings.Contains(d, "(一)") || strings.Contains(d, "(一)"): + p = 1 + case strings.Contains(d, "(二)") || strings.Contains(d, "(二)"): + p = 2 + case strings.Contains(d, "一") && !strings.Contains(d, "二"): + p = 5 + case strings.Contains(d, "二"): + p = 6 + } + xs = append(xs, scored{d, p}) + } + sort.Slice(xs, func(i, j int) bool { + if xs[i].prio != xs[j].prio { + return xs[i].prio < xs[j].prio + } + return xs[i].name < xs[j].name + }) + out := make([]string, len(xs)) + for i, x := range xs { + out[i] = x.name + } + return out +} + 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" { @@ -221,7 +323,7 @@ func pickMediaInDir(videoPublish, dirName string, dstFile string) (absPath, relC 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, calcPair []string) (absPath, relChosen string, ok bool) { for _, rel := range rule.SrcRels { p := filepath.Join(videoPublish, filepath.FromSlash(rel)) if st, err := os.Stat(p); err == nil && !st.IsDir() { @@ -252,6 +354,22 @@ func resolveSourceFile(videoPublish string, rule importRule) (absPath, relChosen return abs, rel, true } } + // 恰好两个「实例」类目录且无法按名称命中时:排序后第 1 个 -> demo-1,第 2 个 -> demo-2 + if rule.EpisodeScan == "一" && len(calcPair) >= 2 { + if abs, rel, ok := pickMediaInDir(videoPublish, calcPair[0], rule.Dst); ok { + return abs, rel, true + } + } + if rule.EpisodeScan == "一" && len(calcPair) == 1 { + if abs, rel, ok := pickMediaInDir(videoPublish, calcPair[0], rule.Dst); ok { + return abs, rel, true + } + } + if rule.EpisodeScan == "二" && len(calcPair) >= 2 { + if abs, rel, ok := pickMediaInDir(videoPublish, calcPair[1], rule.Dst); ok { + return abs, rel, true + } + } return "", "", false } @@ -281,6 +399,11 @@ func main() { } videoPublish = filepath.Clean(videoPublish) + calcPair := orderCalcDirsForDemo12(listCalcInstanceDirsForPairing(videoPublish)) + if *dryRun && len(calcPair) > 0 { + log.Printf("[dry-run] 操作与计算类目录配对顺序(1<-[0], 2<-[1]): %v", calcPair) + } + uploadDir := *uploadRoot if uploadDir == "" { uploadDir = os.Getenv("UPLOAD_DIR") @@ -325,7 +448,7 @@ func main() { ok, skip, fail := 0, 0, 0 for _, m := range mappings { - from, srcRelUsed, found := resolveSourceFile(videoPublish, m) + from, srcRelUsed, found := resolveSourceFile(videoPublish, m, calcPair) if !found { log.Printf("SKIP 源文件不存在(已试备选路径/扫描子目录): dst=%s episode=%s", m.Dst, m.EpisodeScan) skip++