mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 16:26:36 +08:00
feat: merge conflict resolution, site management, UI fixes
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user