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 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -19,4 +19,5 @@ Thumbs.db
|
|||||||
backup/
|
backup/
|
||||||
*.db
|
*.db
|
||||||
download/
|
download/
|
||||||
bin/
|
bin/
|
||||||
|
cmd/bilidown/static
|
||||||
|
|||||||
+24
-10
@@ -15,7 +15,7 @@ import (
|
|||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed all:build/static
|
//go:embed all:static
|
||||||
var staticFiles embed.FS
|
var staticFiles embed.FS
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,34 +25,48 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var urlLocal = fmt.Sprintf("http://127.0.0.1:%d", HTTP_PORT)
|
var urlLocal = fmt.Sprintf("http://127.0.0.1:%d", HTTP_PORT)
|
||||||
|
var ffmpegAvailable bool
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
checkFFmpeg()
|
ffmpegAvailable = checkFFmpeg()
|
||||||
mustInitTables()
|
mustInitTables()
|
||||||
mustRunServer()
|
mustRunServer()
|
||||||
fmt.Printf("Bilidown %s server running at %s\n", VERSION, urlLocal)
|
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 {} // 保持运行
|
select {} // 保持运行
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkFFmpeg 检测 ffmpeg 的安装情况
|
// checkFFmpeg 检测 ffmpeg 的安装情况,返回是否可用
|
||||||
func checkFFmpeg() {
|
func checkFFmpeg() bool {
|
||||||
if _, err := util.GetFFmpegPath(); err != nil {
|
_, err := util.GetFFmpegPath()
|
||||||
fmt.Println("FFmpeg is missing. Install it from https://www.ffmpeg.org/download.html, then restart.")
|
return err == nil
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustRunServer 配置和启动 HTTP 服务器
|
// mustRunServer 配置和启动 HTTP 服务器
|
||||||
func mustRunServer() {
|
func mustRunServer() {
|
||||||
// 从嵌入的文件系统中获取静态文件
|
// 从嵌入的文件系统中获取静态文件
|
||||||
staticFS, err := fs.Sub(staticFiles, "build/static")
|
staticFS, err := fs.Sub(staticFiles, "static")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to load static files:", err)
|
log.Fatal("Failed to load static files:", err)
|
||||||
}
|
}
|
||||||
// 前端静态文件
|
// 前端静态文件
|
||||||
http.Handle("/", http.FileServer(http.FS(staticFS)))
|
http.Handle("/", http.FileServer(http.FS(staticFS)))
|
||||||
// 后端 API 接口
|
// 后端 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 服务器
|
// 启动 HTTP 服务器
|
||||||
go func() {
|
go func() {
|
||||||
err := http.ListenAndServe(fmt.Sprintf("%s:%d", HTTP_HOST, HTTP_PORT), nil)
|
err := http.ListenAndServe(fmt.Sprintf("%s:%d", HTTP_HOST, HTTP_PORT), nil)
|
||||||
|
|||||||
+3
-22
@@ -5,7 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -97,37 +96,19 @@ func GetCurrentFolder(db *sql.DB) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
err = os.MkdirAll(folder, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
err = SaveDownloadFolder(db, folder)
|
err = SaveDownloadFolder(db, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return folder, nil
|
return folder, nil
|
||||||
}
|
}
|
||||||
err = os.MkdirAll(folder, os.ModePerm)
|
return folder, err
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return folder, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveDownloadFolder 保存下载路径,不存在则自动创建
|
// SaveDownloadFolder 保存下载路径(不自动创建目录)
|
||||||
func SaveDownloadFolder(db *sql.DB, downloadFolder string) error {
|
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()
|
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()
|
SqliteLock.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-2
@@ -1,12 +1,29 @@
|
|||||||
import van from 'vanjs-core'
|
import van from 'vanjs-core'
|
||||||
import { now } from 'vanjs-router'
|
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<boolean | null>(null)
|
||||||
|
|
||||||
|
export const checkFFmpeg = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getApi('/checkFFmpeg')
|
||||||
|
FFmpegAvailable.val = res.data?.available ?? false
|
||||||
|
} catch {
|
||||||
|
FFmpegAvailable.val = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const classStr = (name: string) => van.derive(() => `text-nowrap nav-link ${now.val.split('/')[0] == name ? 'active' : ''}`)
|
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' },
|
return div({ class: 'hstack gap-4' },
|
||||||
div({ class: 'fs-4 fw-bold text-nowrap' }, 'Bilidown'),
|
div({ class: 'fs-4 fw-bold text-nowrap' }, 'Bilidown'),
|
||||||
div({ class: 'nav nav-underline flex-nowrap overflow-auto' },
|
div({ class: 'nav nav-underline flex-nowrap overflow-auto' },
|
||||||
@@ -22,6 +39,16 @@ export default () => {
|
|||||||
div({ class: 'nav-item', hidden: GLOBAL_HAS_LOGIN },
|
div({ class: 'nav-item', hidden: GLOBAL_HAS_LOGIN },
|
||||||
a({ class: classStr('login'), href: '#/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 }, '关闭服务')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
import van from 'vanjs-core'
|
import van from 'vanjs-core'
|
||||||
import Header from './header'
|
import Header, { checkFFmpeg } from './header'
|
||||||
import Work from './work'
|
import Work from './work'
|
||||||
import Task from './task'
|
import Task from './task'
|
||||||
import Login from './login'
|
import Login from './login'
|
||||||
@@ -16,6 +16,9 @@ const { div } = van.tags
|
|||||||
|
|
||||||
redirect('home', 'work')
|
redirect('home', 'work')
|
||||||
|
|
||||||
|
// 启动时检查 FFmpeg 状态
|
||||||
|
checkFFmpeg()
|
||||||
|
|
||||||
van.add(document.body,
|
van.add(document.body,
|
||||||
div({ class: 'container py-4 vstack gap-4', hidden: GLOBAL_HIDE_PAGE },
|
div({ class: 'container py-4 vstack gap-4', hidden: GLOBAL_HIDE_PAGE },
|
||||||
Header(),
|
Header(),
|
||||||
|
|||||||
@@ -66,4 +66,10 @@ export const formatSeconds = (seconds: number) => {
|
|||||||
if (secs > 0) str += `${secs}秒`
|
if (secs > 0) str += `${secs}秒`
|
||||||
|
|
||||||
return str
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/** API 请求封装 */
|
||||||
|
export const getApi = async <T = unknown>(path: string, options?: RequestInit): Promise<ResJSON<T>> => {
|
||||||
|
const res = await fetch('/api' + path, options)
|
||||||
|
return res.json() as Promise<ResJSON<T>>
|
||||||
}
|
}
|
||||||
+1
-1
@@ -9,7 +9,7 @@ export default defineConfig({
|
|||||||
host: '0.0.0.0'
|
host: '0.0.0.0'
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: '../build/static',
|
outDir: '../cmd/bilidown/static',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
Reference in New Issue
Block a user