Files
kenaito-media-picker/main.go
T
odboy e053b378fb feat: 增加 Web 管理界面,扫描整理改为页面点击触发
- main.go 改为 Web 服务入口(Gin + 内嵌前端 dist + 自动打开浏览器)
- internal/service: 新增扫描整理后台任务,支持状态机与实时日志捕获
- internal/api: 新增统一响应、媒体列表/统计、扫描开始/状态接口
- internal/store: 追加 ListMedia/CountMedia/Stats 查询,MediaInfo 补 JSON tag
- internal/config: application.properties 配置加载(端口/数据库/自动开浏览器)
- frontend: Vue2 + ElementUI + Vite 工程,含扫描整理/统计看板/媒体库三页
- build.sh 整合前端构建,输出单一 kenaito-media-picker.exe
2025-04-21 21:08:02 +08:00

118 lines
3.1 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.
// 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)
}
}