mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
215 lines
5.5 KiB
Go
215 lines
5.5 KiB
Go
// Package service — 跨站聚合搜索服务.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/model"
|
|
"github.com/ShukeBta/MediaStationGo/internal/repository"
|
|
)
|
|
|
|
// SiteSearchService 跨站聚合搜索服务.
|
|
type SiteSearchService struct {
|
|
log *zap.Logger
|
|
repo *repository.Container
|
|
site *SiteService
|
|
}
|
|
|
|
// NewSiteSearchService 创建跨站搜索服务.
|
|
func NewSiteSearchService(log *zap.Logger, repo *repository.Container, siteSvc *SiteService) *SiteSearchService {
|
|
return &SiteSearchService{log: log, repo: repo, site: siteSvc}
|
|
}
|
|
|
|
// SearchAll 在所有启用的站点中搜索关键字.
|
|
func (s *SiteSearchService) SearchAll(ctx context.Context, keyword string, page, pageSize int) (*AggregatedResult, error) {
|
|
sites, err := s.repo.Site.ListEnabled(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list enabled sites: %w", err)
|
|
}
|
|
|
|
if len(sites) == 0 {
|
|
return &AggregatedResult{
|
|
Keyword: keyword,
|
|
Items: []TorrentItem{},
|
|
Total: 0,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}, nil
|
|
}
|
|
|
|
return s.SearchSites(ctx, keyword, sites, page, pageSize)
|
|
}
|
|
|
|
// SearchSites 在指定站点中搜索关键字.
|
|
func (s *SiteSearchService) SearchSites(ctx context.Context, keyword string, sites []model.Site, page, pageSize int) (*AggregatedResult, error) {
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
var allItems []TorrentItem
|
|
|
|
for _, site := range sites {
|
|
wg.Add(1)
|
|
go func(siteModel model.Site) {
|
|
defer wg.Done()
|
|
|
|
cfg, err := s.site.GetSiteConfig(ctx, siteModel.ID)
|
|
if err != nil {
|
|
s.log.Warn("get site config failed", zap.String("site_id", siteModel.ID), zap.Error(err))
|
|
return
|
|
}
|
|
|
|
adapter := GetAdapterForType(siteModel.Type)
|
|
result, err := adapter.Search(ctx, *cfg, keyword, page)
|
|
if err != nil {
|
|
s.log.Warn("site search failed",
|
|
zap.String("site_id", siteModel.ID),
|
|
zap.String("site_name", siteModel.Name),
|
|
zap.Error(err),
|
|
)
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
allItems = append(allItems, result.Items...)
|
|
mu.Unlock()
|
|
}(site)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
sort.Slice(allItems, func(i, j int) bool {
|
|
if allItems[i].Seeders != allItems[j].Seeders {
|
|
return allItems[i].Seeders > allItems[j].Seeders
|
|
}
|
|
return allItems[i].UploadTime.After(allItems[j].UploadTime)
|
|
})
|
|
|
|
allItems = deduplicateItems(allItems)
|
|
|
|
total := len(allItems)
|
|
start := (page - 1) * pageSize
|
|
end := start + pageSize
|
|
if start > total {
|
|
start = total
|
|
}
|
|
if end > total {
|
|
end = total
|
|
}
|
|
|
|
return &AggregatedResult{
|
|
Keyword: keyword,
|
|
Items: allItems[start:end],
|
|
Total: total,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}, nil
|
|
}
|
|
|
|
// SearchSite 在单个站点中搜索.
|
|
func (s *SiteSearchService) SearchSite(ctx context.Context, siteID, keyword string, page int) (*SearchResult, error) {
|
|
cfg, err := s.site.GetSiteConfig(ctx, siteID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get site config: %w", err)
|
|
}
|
|
|
|
siteModel, err := s.repo.Site.FindByID(ctx, siteID)
|
|
if err != nil || siteModel == nil {
|
|
return nil, fmt.Errorf("find site: %w", err)
|
|
}
|
|
|
|
adapter := GetAdapterForType(siteModel.Type)
|
|
result, err := adapter.Search(ctx, *cfg, keyword, page)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("search site %s: %w", siteModel.Name, err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// BrowseSite 浏览站点资源.
|
|
func (s *SiteSearchService) BrowseSite(ctx context.Context, siteID, category string, page int) (*SearchResult, error) {
|
|
cfg, err := s.site.GetSiteConfig(ctx, siteID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get site config: %w", err)
|
|
}
|
|
|
|
siteModel, err := s.repo.Site.FindByID(ctx, siteID)
|
|
if err != nil || siteModel == nil {
|
|
return nil, fmt.Errorf("find site: %w", err)
|
|
}
|
|
|
|
adapter := GetAdapterForType(siteModel.Type)
|
|
result, err := adapter.Browse(ctx, *cfg, category, page)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("browse site %s: %w", siteModel.Name, err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetTorrentDetail 获取种子详情.
|
|
func (s *SiteSearchService) GetTorrentDetail(ctx context.Context, siteID, torrentID string) (*TorrentDetail, error) {
|
|
cfg, err := s.site.GetSiteConfig(ctx, siteID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get site config: %w", err)
|
|
}
|
|
|
|
siteModel, err := s.repo.Site.FindByID(ctx, siteID)
|
|
if err != nil || siteModel == nil {
|
|
return nil, fmt.Errorf("find site: %w", err)
|
|
}
|
|
|
|
adapter := GetAdapterForType(siteModel.Type)
|
|
detail, err := adapter.GetDetail(ctx, *cfg, torrentID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get detail from %s: %w", siteModel.Name, err)
|
|
}
|
|
|
|
return detail, nil
|
|
}
|
|
|
|
// AggregatedResult 聚合搜索结果.
|
|
type AggregatedResult struct {
|
|
Keyword string `json:"keyword"`
|
|
Items []TorrentItem `json:"items"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
}
|
|
|
|
// deduplicateItems 通过标题相似性去重.
|
|
func deduplicateItems(items []TorrentItem) []TorrentItem {
|
|
seen := make(map[string]bool)
|
|
result := make([]TorrentItem, 0, len(items))
|
|
|
|
for _, item := range items {
|
|
key := normalizeTitle(item.Title)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if !seen[key] {
|
|
seen[key] = true
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// normalizeTitle 标题标准化.
|
|
func normalizeTitle(title string) string {
|
|
title = strings.ToLower(strings.TrimSpace(title))
|
|
title = strings.ReplaceAll(title, ".", " ")
|
|
title = strings.ReplaceAll(title, "_", " ")
|
|
title = strings.ReplaceAll(title, "-", " ")
|
|
for strings.Contains(title, " ") {
|
|
title = strings.ReplaceAll(title, " ", " ")
|
|
}
|
|
return strings.TrimSpace(title)
|
|
}
|