mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 11:36:36 +08:00
155 lines
4.0 KiB
Go
155 lines
4.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
)
|
|
|
|
// 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"`
|
|
Subtitle string `json:"subtitle,omitempty"`
|
|
TorrentURL string `json:"torrent_url"`
|
|
DownloadURL string `json:"download_url"`
|
|
Category string `json:"category,omitempty"`
|
|
SearchKeyword string `json:"search_keyword,omitempty"`
|
|
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
|
|
enabledCount int
|
|
failedCount int
|
|
failureErrs []error
|
|
failures []string
|
|
results []SearchResult
|
|
)
|
|
|
|
for i := range sites {
|
|
if !sites[i].Enabled {
|
|
continue
|
|
}
|
|
enabledCount++
|
|
wg.Add(1)
|
|
go func(site model.Site) {
|
|
defer wg.Done()
|
|
|
|
adapter := NewSiteAdapter(&site)
|
|
if adapter == nil {
|
|
mu.Lock()
|
|
failedCount++
|
|
err := fmt.Errorf("%s: unsupported site type %s", site.Name, site.Type)
|
|
failureErrs = append(failureErrs, err)
|
|
failures = append(failures, err.Error())
|
|
mu.Unlock()
|
|
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 {
|
|
mu.Lock()
|
|
failedCount++
|
|
failureErr := fmt.Errorf("%s: %w", site.Name, err)
|
|
failureErrs = append(failureErrs, failureErr)
|
|
failures = append(failures, failureErr.Error())
|
|
mu.Unlock()
|
|
s.log.Warn("site search failed",
|
|
zap.String("site", site.Name),
|
|
zap.String("type", site.Type),
|
|
zap.String("url", site.URL),
|
|
zap.String("keyword", keyword),
|
|
zap.Duration("timeout", timeout),
|
|
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,
|
|
Subtitle: item.Subtitle,
|
|
TorrentURL: item.DetailURL,
|
|
DownloadURL: item.DownloadURL,
|
|
Category: item.Category,
|
|
SearchKeyword: keyword,
|
|
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
|
|
})
|
|
if s.log != nil {
|
|
s.log.Info("site search completed",
|
|
zap.String("keyword", keyword),
|
|
zap.Int("enabled_sites", enabledCount),
|
|
zap.Int("failed_sites", failedCount),
|
|
zap.Int("results_count", len(results)))
|
|
}
|
|
if enabledCount > 0 && failedCount >= enabledCount && len(results) == 0 {
|
|
if len(failureErrs) > 0 {
|
|
return results, fmt.Errorf("all enabled sites failed while searching %q: %w", keyword, errors.Join(failureErrs...))
|
|
}
|
|
return results, fmt.Errorf("all enabled sites failed while searching %q: %s", keyword, strings.Join(failures, "; "))
|
|
}
|
|
return results, nil
|
|
}
|