// kenaito-media-picker 主入口:启动 Web 服务(内嵌前端),媒体库扫描整理由页面点击触发。 package main import ( "fmt" "io" "io/fs" "mime" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "kenaito-media-picker/frontend" "kenaito-media-picker/internal/api" "kenaito-media-picker/internal/config" "kenaito-media-picker/internal/store" "github.com/gin-gonic/gin" ) func main() { config.LoadAppConfig() cfg := config.GetAppConfig() // 打开数据库;失败时服务照常启动,仅媒体库查询不可用、扫描不记录信息 st, err := store.Open(cfg.Dsn) if err != nil { fmt.Printf("=== 打开数据库失败(媒体库查询不可用): %v\n", err) st = nil } else { defer st.Close() } r := api.NewRouter(st) // 请求日志输出到 stderr,避免占用被扫描流程捕获的 stdout r.Use(gin.LoggerWithWriter(os.Stderr)) r.NoRoute(staticHandler()) addr := fmt.Sprintf(":%d", cfg.Port) url := "http://localhost" + addr fmt.Printf("=== Web 服务已启动: %s\n", url) fmt.Printf("=== 数据库: %s\n", cfg.Dsn) fmt.Printf("=== 在页面上点击【开始扫描】执行媒体整理(会移动文件,注意备份)\n") if cfg.AutoOpen { go func() { time.Sleep(600 * time.Millisecond) // 等服务端口就绪再打开 openBrowser(url) }() } if err := r.Run(addr); err != nil { fmt.Printf("=== 服务启动失败: %v\n", err) } } // openBrowser 调用系统默认浏览器打开指定 URL(Windows/macOS/Linux) func openBrowser(url string) { var cmd *exec.Cmd switch runtime.GOOS { case "windows": cmd = exec.Command("cmd", "/c", "start", "", url) case "darwin": cmd = exec.Command("open", url) default: cmd = exec.Command("xdg-open", url) } if err := cmd.Start(); err != nil { fmt.Printf("=== 自动打开浏览器失败: %v\n", err) } } // staticHandler 服务内嵌前端产物:命中静态资源则返回,否则回退 index.html(hash 路由)。 func staticHandler() gin.HandlerFunc { distFS, err := fs.Sub(frontend.Dist, "dist") if err != nil { return func(c *gin.Context) { c.String(http.StatusNotFound, "前端产物缺失,请先 npm run build 后重新编译") } } return func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api") { c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "接口不存在", "data": nil}) return } path := strings.TrimPrefix(c.Request.URL.Path, "/") if path == "" { path = "index.html" } file, err := distFS.Open(path) if err != nil { file, err = distFS.Open("index.html") } if err != nil { c.String(http.StatusNotFound, "页面不存在") return } defer file.Close() info, statErr := file.Stat() if statErr != nil { c.String(http.StatusInternalServerError, "读取前端资源失败") return } if rs, ok := file.(io.ReadSeeker); ok { http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), rs) return } body, err := io.ReadAll(file) if err != nil { c.String(http.StatusInternalServerError, "读取前端资源失败") return } c.Data(http.StatusOK, mime.TypeByExtension(filepath.Ext(path)), body) } }