refactor: restructure project to standard Go layout
- Reorganize code into cmd/bilidown and internal/ packages - Rename client/ to web/ for frontend source - Remove systray dependency for headless web service - Embed static files into binary using go:embed - Update import paths to use internal/ prefix - Update .gitignore with common patterns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func CreateLog(db *sql.DB, content string) error {
|
||||
SqliteLock.Lock()
|
||||
_, err := db.Exec(`INSERT INTO "log" ("content") VALUES (?)`, content)
|
||||
SqliteLock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func GetFields(db *sql.DB, names ...string) (map[string]string, error) {
|
||||
|
||||
if len(names) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
placeholders := make([]string, len(names))
|
||||
for i := 0; i < len(names); i++ {
|
||||
placeholders[i] = "?"
|
||||
}
|
||||
query := fmt.Sprintf(`SELECT "name", "value" FROM "field" WHERE "name" IN (%s)`, strings.Join(placeholders, ","))
|
||||
|
||||
values := make([]interface{}, len(names))
|
||||
for i := 0; i < len(names); i++ {
|
||||
values[i] = names[i]
|
||||
}
|
||||
SqliteLock.Lock()
|
||||
row, err := db.Query(query, values...)
|
||||
SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer row.Close()
|
||||
var name, value string
|
||||
fields := make(map[string]string)
|
||||
for row.Next() {
|
||||
if err := row.Scan(&name, &value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields[name] = value
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func SaveFields(db *sql.DB, data [][2]string) error {
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
|
||||
stmt, err := tx.Prepare(`INSERT OR REPLACE INTO "field" ("name", "value") VALUES (?, ?)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, d := range data {
|
||||
SqliteLock.Lock()
|
||||
_, err = stmt.Exec(d[0], d[1])
|
||||
SqliteLock.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentFolder 获取数据库中的下载保存路径,如果不存在则将默认路径保存到数据库
|
||||
func GetCurrentFolder(db *sql.DB) (string, error) {
|
||||
var folder string
|
||||
SqliteLock.Lock()
|
||||
err := db.QueryRow(`SELECT "value" FROM "field" WHERE "name" = 'download_folder'`).Scan(&folder)
|
||||
SqliteLock.Unlock()
|
||||
if err != nil && err == sql.ErrNoRows {
|
||||
folder, err = GetDefaultDownloadFolder()
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
SqliteLock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
var SqliteLock sync.Mutex
|
||||
|
||||
func MustGetDB(path ...string) *sql.DB {
|
||||
pathStr := ""
|
||||
if len(path) == 0 {
|
||||
pathStr = "./data.db"
|
||||
} else if len(path) > 1 {
|
||||
log.Fatalln(errors.New("len(path) <= 1"))
|
||||
} else {
|
||||
pathStr = path[0]
|
||||
}
|
||||
db, err := sql.Open("sqlite", pathStr)
|
||||
if err != nil {
|
||||
log.Fatalln("sql.Open:", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package util
|
||||
|
||||
type FieldUtil struct{}
|
||||
|
||||
func (f FieldUtil) AllowSelect() []string {
|
||||
return []string{
|
||||
"download_folder",
|
||||
}
|
||||
}
|
||||
|
||||
func (f FieldUtil) AllowUpdate() []string {
|
||||
return []string{
|
||||
"download_folder",
|
||||
}
|
||||
}
|
||||
|
||||
func (f FieldUtil) IsAllow(allFields []string, names ...string) bool {
|
||||
allowedFields := make(map[string]struct{})
|
||||
for _, field := range allFields {
|
||||
allowedFields[field] = struct{}{}
|
||||
}
|
||||
for _, name := range names {
|
||||
if _, exists := allowedFields[name]; !exists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f FieldUtil) IsAllowSelect(names ...string) bool {
|
||||
return f.IsAllow(f.AllowSelect(), names...)
|
||||
}
|
||||
|
||||
func (f FieldUtil) IsAllowUpdate(names ...string) bool {
|
||||
return f.IsAllow(f.AllowUpdate(), names...)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package res_error
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bilidown/internal/util"
|
||||
)
|
||||
|
||||
// Send 发送异常响应
|
||||
func Send(w http.ResponseWriter, message string) {
|
||||
util.Res{Message: message, Success: false}.Write(w)
|
||||
}
|
||||
|
||||
const (
|
||||
BvidFormatError = "错误的 Bvid 格式"
|
||||
URLFormatError = "错误的 URL 格式"
|
||||
MidFormatError = "错误的 Mid 格式"
|
||||
SeasonIdFormatError = "错误的 SeasonId 格式"
|
||||
ParamError = "参数错误"
|
||||
MethodNotAllowError = "不允许的请求方式"
|
||||
NoLocationError = "无重定向目标地址"
|
||||
FileNotFountError = "文件不存在"
|
||||
FileTypeNotAllowError = "不允许的文件类型"
|
||||
SystemError = "系统错误"
|
||||
NotLogin = "未登录"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 统一的 JSON 响应结构
|
||||
type Res struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
func (r Res) Write(w http.ResponseWriter) {
|
||||
bs, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
w.Write([]byte(`{"success":false,"message":"系统错误","data":null}`))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(bs)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package util
|
||||
|
||||
import "sync"
|
||||
|
||||
type Semaphore struct {
|
||||
ch chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewSemaphore(concurrency int) *Semaphore {
|
||||
return &Semaphore{
|
||||
ch: make(chan struct{}, concurrency),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Semaphore) Acquire() {
|
||||
s.ch <- struct{}{}
|
||||
s.wg.Add(1)
|
||||
}
|
||||
|
||||
func (s *Semaphore) Release() {
|
||||
<-s.ch
|
||||
s.wg.Done()
|
||||
}
|
||||
|
||||
func (s *Semaphore) Wait() {
|
||||
s.wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"bilidown/internal/common"
|
||||
)
|
||||
|
||||
func CheckBvidFormat(bvid string) bool {
|
||||
return regexp.MustCompile("^BV1[a-zA-Z0-9]+").MatchString(bvid)
|
||||
}
|
||||
|
||||
// GetDefaultDownloadFolder 获取默认下载路径
|
||||
func GetDefaultDownloadFolder() (string, error) {
|
||||
return filepath.Abs("./download")
|
||||
}
|
||||
|
||||
func IsNumber(str string) bool {
|
||||
_, err := strconv.Atoi(str)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsValidURL 判断字符串是否为合法的URL
|
||||
func IsValidURL(u string) bool {
|
||||
_, err := url.ParseRequestURI(u)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsValidFormatCode 判断格式码是否合法
|
||||
func IsValidFormatCode(format common.MediaFormat) bool {
|
||||
allowed := []common.MediaFormat{6, 16, 32, 64, 74, 80, 112, 116, 120, 125, 126, 127}
|
||||
for _, v := range allowed {
|
||||
if v == format {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FilterFileName 过滤字符串中的特殊字符,使其允许作为文件名。
|
||||
func FilterFileName(fileName string) string {
|
||||
return regexp.MustCompile(`[\\/:*?"<>|\n]`).ReplaceAllString(fileName, "")
|
||||
}
|
||||
|
||||
// GetFFmpegPath 获取可用的 FFmpeg 执行路径。
|
||||
func GetFFmpegPath() (string, error) {
|
||||
if err := exec.Command("ffmpeg", "-version").Run(); err == nil {
|
||||
return "ffmpeg", nil
|
||||
}
|
||||
if err := exec.Command("bin/ffmpeg", "-version").Run(); err == nil {
|
||||
return "bin/ffmpeg", nil
|
||||
}
|
||||
return "", errors.New("ffmpeg not found")
|
||||
}
|
||||
|
||||
// GetRedirectedLocation 获取响应头中的 Location,不会自动跟随重定向。
|
||||
func GetRedirectedLocation(url string) (string, error) {
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
request, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if locationURL, err := response.Location(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return locationURL.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func MD5Hash(str string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(str))
|
||||
hash := hasher.Sum(nil)
|
||||
hashString := hex.EncodeToString(hash)
|
||||
return hashString
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"bilidown/common"
|
||||
"bilidown/util"
|
||||
)
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
for i := 4; i < 10; i++ {
|
||||
for j := 0; j < 3; j++ {
|
||||
str := common.RandomString(i)
|
||||
t.Log(str)
|
||||
}
|
||||
t.Log("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRedirectedLocation(t *testing.T) {
|
||||
os.Setenv("https_proxy", "http://192.168.1.5:9000")
|
||||
if location, err := util.GetRedirectedLocation("https://b23.tv/Ga6sbzT"); err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
fmt.Println(location)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user