mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
5d1ae91419
- site_adapter.go: SiteConfig 扩展 UserAgent/Timeout/Extra/FlareSolverrURL 字段 - site_adapter.go: doRequest() GET 请求支持 FlareSolverr 代理绕过 Cloudflare/WAF - site_adapter.go: buildRequest() 使用 cfg.UserAgent(覆盖默认 UA) - site_adapter.go: MTeam api_key 认证改为 Authorization: Bearer 格式 - site_adapter.go: 修复 2 个 go vet 冗余 StatusFound 警告 - site.go: Search() 重构为 sync.WaitGroup 并发执行,提升多站搜索性能 - site.go: siteModelToConfig() 改为 SiteService 方法,按 BrowserEmulation 填充 FlareSolverrURL - site.go: Search() 空关键词返回 [] 而非 error,nil slice 防护 - 新增 system_handler.go: 系统工具探测接口 - 新增 ffmpeg_auto_install.go: FFmpeg 自动安装服务 - 新增 scripts/install-ffmpeg.ps1: Windows FFmpeg 安装脚本
254 lines
7.0 KiB
Go
254 lines
7.0 KiB
Go
// Package service — site management (PT/BT tracker CRUD + connection test).
|
|
//
|
|
// SiteService owns the lifecycle of Site rows and exposes a cross-site
|
|
// search dispatcher that fans out a keyword query to every enabled site's
|
|
// adapter, collects results and returns them merged + sorted.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/helper"
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
// SiteService manages PT/BT site configurations.
|
|
type SiteService struct {
|
|
log *zap.Logger
|
|
repo *repository.Container
|
|
flareSolverrURL string
|
|
}
|
|
|
|
// NewSiteService is the constructor.
|
|
func NewSiteService(log *zap.Logger, repo *repository.Container, flareSolverrURL string) *SiteService {
|
|
return &SiteService{log: log, repo: repo, flareSolverrURL: flareSolverrURL}
|
|
}
|
|
|
|
// Create persists a new site.
|
|
func (s *SiteService) Create(ctx context.Context, site *model.Site) error {
|
|
if strings.TrimSpace(site.Name) == "" || strings.TrimSpace(site.URL) == "" {
|
|
return errors.New("name and url required")
|
|
}
|
|
site.URL = strings.TrimRight(site.URL, "/")
|
|
if site.Type == "" {
|
|
site.Type = "nexusphp"
|
|
}
|
|
if site.AuthType == "" {
|
|
site.AuthType = "cookie"
|
|
}
|
|
return s.repo.DB.WithContext(ctx).Create(site).Error
|
|
}
|
|
|
|
// List returns every site ordered by created_at.
|
|
func (s *SiteService) List(ctx context.Context) ([]model.Site, error) {
|
|
var sites []model.Site
|
|
err := s.repo.DB.WithContext(ctx).Order("created_at asc").Find(&sites).Error
|
|
if sites == nil {
|
|
sites = []model.Site{}
|
|
}
|
|
return sites, err
|
|
}
|
|
|
|
// FindByID returns a single site or nil.
|
|
func (s *SiteService) FindByID(ctx context.Context, id string) (*model.Site, error) {
|
|
var site model.Site
|
|
err := s.repo.DB.WithContext(ctx).Where("id = ?", id).First(&site).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return &site, err
|
|
}
|
|
|
|
// Update applies a partial patch to an existing site.
|
|
func (s *SiteService) Update(ctx context.Context, id string, updates map[string]any) error {
|
|
return s.repo.DB.WithContext(ctx).Model(&model.Site{}).Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
// Delete removes a site.
|
|
func (s *SiteService) Delete(ctx context.Context, id string) error {
|
|
return s.repo.DB.WithContext(ctx).Where("id = ?", id).Delete(&model.Site{}).Error
|
|
}
|
|
|
|
// TestConnection tries to reach the site's base URL with the configured
|
|
// credentials and reports success/failure.
|
|
// Now uses helper.TestSiteConnectivity with browser-like headers
|
|
// and optional FlareSolverr support.
|
|
func (s *SiteService) TestConnection(ctx context.Context, id string) (bool, string, error) {
|
|
site, err := s.FindByID(ctx, id)
|
|
if err != nil || site == nil {
|
|
return false, "site not found", err
|
|
}
|
|
|
|
// Get timeout from site config (default 15 seconds)
|
|
timeout := site.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 15
|
|
}
|
|
|
|
// Use configured FlareSolverr URL (may be empty if not configured)
|
|
flareSolverrURL := s.flareSolverrURL
|
|
|
|
ok, msg, err := helper.TestSiteConnectivity(site, flareSolverrURL, timeout, s.log)
|
|
if err != nil {
|
|
now := time.Now()
|
|
_ = s.repo.DB.WithContext(ctx).Model(&model.Site{}).Where("id = ?", id).
|
|
Updates(map[string]any{"last_error": err.Error(), "last_check_at": &now}).Error
|
|
return false, err.Error(), nil
|
|
}
|
|
|
|
loginStatus := "ok"
|
|
if !ok {
|
|
loginStatus = "fail"
|
|
}
|
|
now := time.Now()
|
|
_ = s.repo.DB.WithContext(ctx).Model(&model.Site{}).Where("id = ?", id).
|
|
Updates(map[string]any{"last_error": loginStatus, "last_check_at": &now}).Error
|
|
return ok, msg, nil
|
|
}
|
|
|
|
// SearchResult is one torrent returned by a site adapter search.
|
|
type SearchResult struct {
|
|
SiteName string `json:"site_name"`
|
|
SiteID string `json:"site_id"`
|
|
Title string `json:"title"`
|
|
TorrentURL string `json:"torrent_url"`
|
|
DownloadURL string `json:"download_url"`
|
|
Size int64 `json:"size"`
|
|
Seeders int `json:"seeders"`
|
|
Leechers int `json:"leechers"`
|
|
Free bool `json:"free"`
|
|
}
|
|
|
|
// Search fans out a keyword query to every enabled site and returns
|
|
// merged results sorted by seeders descending.
|
|
// Uses concurrent search with sync.WaitGroup for performance.
|
|
func (s *SiteService) Search(ctx context.Context, keyword string) ([]SearchResult, error) {
|
|
if strings.TrimSpace(keyword) == "" {
|
|
return []SearchResult{}, nil
|
|
}
|
|
sites, err := s.List(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
wg sync.WaitGroup
|
|
results []SearchResult
|
|
)
|
|
|
|
for i := range sites {
|
|
if !sites[i].Enabled {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
go func(site model.Site) {
|
|
defer wg.Done()
|
|
|
|
adapter := NewSiteAdapter(&site)
|
|
if adapter == nil {
|
|
return
|
|
}
|
|
|
|
cfg := s.siteModelToConfig(&site)
|
|
|
|
// Use site timeout or default 30s
|
|
timeout := time.Duration(site.Timeout) * time.Second
|
|
if timeout <= 0 {
|
|
timeout = 30 * time.Second
|
|
}
|
|
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
result, err := adapter.Search(ctxWithTimeout, cfg, keyword, 1)
|
|
if err != nil {
|
|
s.log.Debug("site search failed",
|
|
zap.String("site", site.Name), zap.Error(err))
|
|
return
|
|
}
|
|
if result == nil {
|
|
return
|
|
}
|
|
items := result.Items
|
|
if items == nil {
|
|
items = []TorrentItem{}
|
|
}
|
|
for _, item := range items {
|
|
mu.Lock()
|
|
results = append(results, SearchResult{
|
|
SiteName: site.Name,
|
|
SiteID: site.ID,
|
|
Title: item.Title,
|
|
TorrentURL: item.DetailURL,
|
|
DownloadURL: item.DownloadURL,
|
|
Size: item.Size,
|
|
Seeders: item.Seeders,
|
|
Leechers: item.Leechers,
|
|
Free: item.Free,
|
|
})
|
|
mu.Unlock()
|
|
}
|
|
}(sites[i])
|
|
}
|
|
wg.Wait()
|
|
|
|
// Ensure results is never nil (return [] instead of null in JSON)
|
|
if results == nil {
|
|
results = []SearchResult{}
|
|
}
|
|
|
|
// Sort by seeders desc.
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return results[i].Seeders > results[j].Seeders
|
|
})
|
|
return results, nil
|
|
}
|
|
|
|
// siteModelToConfig 将 model.Site 转换为适配器使用的 SiteConfig。
|
|
// 当全局 FlareSolverr 已启用且此站点开启了 BrowserEmulation 时,填充 FlareSolverrURL。
|
|
func (svc *SiteService) siteModelToConfig(s *model.Site) SiteConfig {
|
|
timeout := time.Duration(s.Timeout) * time.Second
|
|
if timeout <= 0 {
|
|
timeout = 15 * time.Second
|
|
}
|
|
userAgent := s.UserAgent
|
|
if userAgent == "" {
|
|
userAgent = model.DefaultUserAgent
|
|
}
|
|
var extra map[string]string
|
|
if s.Extra != "" {
|
|
_ = json.Unmarshal([]byte(s.Extra), &extra)
|
|
}
|
|
|
|
// Per-site FlareSolverr opt-in: only when global FlareSolverr is enabled
|
|
// AND this site has BrowserEmulation turned on.
|
|
flareSolverrURL := ""
|
|
if svc.flareSolverrURL != "" && s.BrowserEmulation {
|
|
flareSolverrURL = svc.flareSolverrURL
|
|
}
|
|
|
|
return SiteConfig{
|
|
Name: s.Name,
|
|
Type: s.Type,
|
|
URL: s.URL,
|
|
AuthType: s.AuthType,
|
|
Cookie: s.Cookie,
|
|
APIKey: s.APIKey,
|
|
AuthHeader: s.AuthHeader,
|
|
UserAgent: userAgent,
|
|
Timeout: timeout,
|
|
Extra: extra,
|
|
FlareSolverrURL: flareSolverrURL,
|
|
}
|
|
}
|