From eb662a8b66908eff65925806b56db267b7964800 Mon Sep 17 00:00:00 2001 From: yw1573 Date: Tue, 7 Apr 2026 15:27:55 +0800 Subject: [PATCH] feat: add FFmpeg status check and shutdown button - FFmpeg check no longer blocks server startup - Add /api/checkFFmpeg endpoint to query FFmpeg availability - Add /api/shutdown endpoint to gracefully stop the server - Add FFmpeg status indicator in web header - Add shutdown button in web interface - Move static files output to cmd/bilidown/static for embedding - Update .gitignore to exclude cmd/bilidown/static Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 ++- cmd/bilidown/main.go | 34 ++++++++++++++++++++++++---------- internal/util/db.go | 25 +++---------------------- web/src/header.ts | 31 +++++++++++++++++++++++++++++-- web/src/index.ts | 5 ++++- web/src/mixin.ts | 6 ++++++ web/vite.config.ts | 2 +- 7 files changed, 69 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index f6bacce..36cf0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ Thumbs.db backup/ *.db download/ -bin/ \ No newline at end of file +bin/ +cmd/bilidown/static diff --git a/cmd/bilidown/main.go b/cmd/bilidown/main.go index 1242101..665243c 100644 --- a/cmd/bilidown/main.go +++ b/cmd/bilidown/main.go @@ -15,7 +15,7 @@ import ( _ "modernc.org/sqlite" ) -//go:embed all:build/static +//go:embed all:static var staticFiles embed.FS const ( @@ -25,34 +25,48 @@ const ( ) var urlLocal = fmt.Sprintf("http://127.0.0.1:%d", HTTP_PORT) +var ffmpegAvailable bool func main() { - checkFFmpeg() + ffmpegAvailable = checkFFmpeg() mustInitTables() mustRunServer() fmt.Printf("Bilidown %s server running at %s\n", VERSION, urlLocal) + if !ffmpegAvailable { + fmt.Println("Warning: FFmpeg is not installed. Video download will not work.") + } select {} // 保持运行 } -// checkFFmpeg 检测 ffmpeg 的安装情况 -func checkFFmpeg() { - if _, err := util.GetFFmpegPath(); err != nil { - fmt.Println("FFmpeg is missing. Install it from https://www.ffmpeg.org/download.html, then restart.") - os.Exit(1) - } +// checkFFmpeg 检测 ffmpeg 的安装情况,返回是否可用 +func checkFFmpeg() bool { + _, err := util.GetFFmpegPath() + return err == nil } // mustRunServer 配置和启动 HTTP 服务器 func mustRunServer() { // 从嵌入的文件系统中获取静态文件 - staticFS, err := fs.Sub(staticFiles, "build/static") + staticFS, err := fs.Sub(staticFiles, "static") if err != nil { log.Fatal("Failed to load static files:", err) } // 前端静态文件 http.Handle("/", http.FileServer(http.FS(staticFS))) // 后端 API 接口 - http.Handle("/api/", http.StripPrefix("/api", router.API())) + apiRouter := router.API() + // 添加 FFmpeg 状态检查接口 + http.HandleFunc("/api/checkFFmpeg", func(w http.ResponseWriter, r *http.Request) { + util.Res{Success: true, Data: map[string]bool{"available": ffmpegAvailable}}.Write(w) + }) + // 添加关闭服务接口 + http.HandleFunc("/api/shutdown", func(w http.ResponseWriter, r *http.Request) { + util.Res{Success: true, Message: "Shutting down..."}.Write(w) + go func() { + os.Exit(0) + }() + }) + http.Handle("/api/", http.StripPrefix("/api", apiRouter)) // 启动 HTTP 服务器 go func() { err := http.ListenAndServe(fmt.Sprintf("%s:%d", HTTP_HOST, HTTP_PORT), nil) diff --git a/internal/util/db.go b/internal/util/db.go index 71a6516..b521f75 100644 --- a/internal/util/db.go +++ b/internal/util/db.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log" - "os" "strings" "sync" ) @@ -97,37 +96,19 @@ func GetCurrentFolder(db *sql.DB) (string, error) { if err != nil { return "", err } - err = os.MkdirAll(folder, os.ModePerm) - if err != nil { - return "", err - } err = SaveDownloadFolder(db, folder) if err != nil { return "", err } return folder, nil } - err = os.MkdirAll(folder, os.ModePerm) - if err != nil { - return "", err - } - return folder, nil + return folder, err } -// SaveDownloadFolder 保存下载路径,不存在则自动创建 +// SaveDownloadFolder 保存下载路径(不自动创建目录) func SaveDownloadFolder(db *sql.DB, downloadFolder string) error { - _, err := os.Stat(downloadFolder) - if err != nil { - if os.IsNotExist(err) { - err = os.MkdirAll(downloadFolder, os.ModePerm) - if err != nil { - return err - } - } - return err - } SqliteLock.Lock() - _, err = db.Exec(`INSERT OR REPLACE INTO "field" ("name", "value") VALUES ('download_folder', ?)`, downloadFolder) + _, err := db.Exec(`INSERT OR REPLACE INTO "field" ("name", "value") VALUES ('download_folder', ?)`, downloadFolder) SqliteLock.Unlock() return err } diff --git a/web/src/header.ts b/web/src/header.ts index c046715..ddc8ca6 100644 --- a/web/src/header.ts +++ b/web/src/header.ts @@ -1,12 +1,29 @@ import van from 'vanjs-core' import { now } from 'vanjs-router' -import { GLOBAL_HAS_LOGIN } from './mixin' +import { GLOBAL_HAS_LOGIN, getApi } from './mixin' -const { a, div } = van.tags +const { a, div, button, span } = van.tags + +export const FFmpegAvailable = van.state(null) + +export const checkFFmpeg = async () => { + try { + const res = await getApi('/checkFFmpeg') + FFmpegAvailable.val = res.data?.available ?? false + } catch { + FFmpegAvailable.val = false + } +} export default () => { const classStr = (name: string) => van.derive(() => `text-nowrap nav-link ${now.val.split('/')[0] == name ? 'active' : ''}`) + const shutdown = async () => { + if (confirm('确定要关闭服务吗?')) { + await getApi('/shutdown') + } + } + return div({ class: 'hstack gap-4' }, div({ class: 'fs-4 fw-bold text-nowrap' }, 'Bilidown'), div({ class: 'nav nav-underline flex-nowrap overflow-auto' }, @@ -22,6 +39,16 @@ export default () => { div({ class: 'nav-item', hidden: GLOBAL_HAS_LOGIN }, a({ class: classStr('login'), href: '#/login' }, '扫码登录') ), + ), + div({ class: 'ms-auto hstack gap-2' }, + // FFmpeg 状态指示器 + div({ class: 'hstack gap-1' }, + span({ class: () => `badge ${FFmpegAvailable.val === true ? 'bg-success' : FFmpegAvailable.val === false ? 'bg-danger' : 'bg-secondary'}` }, + () => FFmpegAvailable.val === true ? 'FFmpeg ✓' : FFmpegAvailable.val === false ? 'FFmpeg ✗' : '检查中...' + ) + ), + // 关闭按钮 + button({ class: 'btn btn-outline-danger btn-sm', onclick: shutdown }, '关闭服务') ) ) } \ No newline at end of file diff --git a/web/src/index.ts b/web/src/index.ts index ad7208b..5866bae 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -1,6 +1,6 @@ /// import van from 'vanjs-core' -import Header from './header' +import Header, { checkFFmpeg } from './header' import Work from './work' import Task from './task' import Login from './login' @@ -16,6 +16,9 @@ const { div } = van.tags redirect('home', 'work') +// 启动时检查 FFmpeg 状态 +checkFFmpeg() + van.add(document.body, div({ class: 'container py-4 vstack gap-4', hidden: GLOBAL_HIDE_PAGE }, Header(), diff --git a/web/src/mixin.ts b/web/src/mixin.ts index ab1b286..02e4f3e 100644 --- a/web/src/mixin.ts +++ b/web/src/mixin.ts @@ -66,4 +66,10 @@ export const formatSeconds = (seconds: number) => { if (secs > 0) str += `${secs}秒` return str +} + +/** API 请求封装 */ +export const getApi = async (path: string, options?: RequestInit): Promise> => { + const res = await fetch('/api' + path, options) + return res.json() as Promise> } \ No newline at end of file diff --git a/web/vite.config.ts b/web/vite.config.ts index 1d779b9..37d7df2 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ host: '0.0.0.0' }, build: { - outDir: '../build/static', + outDir: '../cmd/bilidown/static', emptyOutDir: true, }, css: {