mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-10 23:36:37 +08:00
refactor: split modules and harden scraping workflows
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/ShukeBta/MediaStationGo/internal/model"
|
||||
)
|
||||
|
||||
func (s *SubscriptionService) runSiteSearch(ctx context.Context, sub *model.Subscription) (int, error) {
|
||||
if s.site == nil {
|
||||
if s.log != nil {
|
||||
s.log.Warn("site-search subscription service unavailable", subscriptionSiteSearchLogFields(sub, "")...)
|
||||
}
|
||||
return 0, errors.New("site search service unavailable")
|
||||
}
|
||||
keywords := siteSearchKeywords(sub)
|
||||
keyword := ""
|
||||
if len(keywords) > 0 {
|
||||
keyword = keywords[0]
|
||||
}
|
||||
if keyword == "" {
|
||||
if s.log != nil {
|
||||
s.log.Warn("site-search subscription keyword missing", subscriptionSiteSearchLogFields(sub, "")...)
|
||||
}
|
||||
return 0, errors.New("site-search subscription keyword required")
|
||||
}
|
||||
if s.log != nil {
|
||||
s.log.Info("site-search subscription run started", subscriptionSiteSearchLogFields(sub, keyword)...)
|
||||
}
|
||||
|
||||
var (
|
||||
results []SearchResult
|
||||
lastSearchErr error
|
||||
searchErrors int
|
||||
)
|
||||
for _, searchKeyword := range keywords {
|
||||
found, err := s.site.Search(ctx, searchKeyword)
|
||||
if err != nil {
|
||||
lastSearchErr = err
|
||||
searchErrors++
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, searchKeyword)
|
||||
fields = append(fields, zap.Error(err))
|
||||
s.log.Warn("site-search subscription search failed", fields...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
results = append(results, found...)
|
||||
}
|
||||
results = dedupeSiteSearchResults(results)
|
||||
if len(results) == 0 && lastSearchErr != nil && searchErrors == len(keywords) {
|
||||
return 0, lastSearchErr
|
||||
}
|
||||
if len(results) == 0 {
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, keyword)
|
||||
fields = append(fields, zap.Int("results_count", 0))
|
||||
s.log.Info("site-search subscription no results", fields...)
|
||||
}
|
||||
now := time.Now()
|
||||
_ = s.repo.DB.Model(sub).Updates(map[string]any{"last_run_at": &now}).Error
|
||||
return 0, nil
|
||||
}
|
||||
s.updateSubscriptionTotalEpisodes(ctx, sub, s.resolveSubscriptionTotalEpisodes(ctx, sub, inferSearchTotalEpisodes(results, sub)))
|
||||
|
||||
guidKey := fmt.Sprintf("subscription.%s.seen", sub.ID)
|
||||
seenRaw, _ := s.repo.Setting.Get(ctx, guidKey)
|
||||
seen := splitNonEmpty(seenRaw)
|
||||
seenSet := make(map[string]struct{}, len(seen))
|
||||
for _, g := range seen {
|
||||
seenSet[g] = struct{}{}
|
||||
}
|
||||
|
||||
availability := mergeLocalAvailability(
|
||||
SubscriptionLocalAvailability(ctx, s.repo, sub),
|
||||
s.pendingDownloadAvailability(ctx, sub),
|
||||
)
|
||||
candidates, selectionStats := selectSiteSearchCandidatesWithStats(results, sub, seenSet, availability)
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, keyword)
|
||||
fields = appendSiteSearchSelectionLogFields(fields, selectionStats)
|
||||
fields = appendAvailabilityLogFields(fields, availability)
|
||||
s.log.Info("site-search subscription selection summary", fields...)
|
||||
}
|
||||
var lastEnqueueErr error
|
||||
queued := 0
|
||||
var resources []string
|
||||
for _, candidate := range candidates {
|
||||
item := candidate.Item
|
||||
matchText := subscriptionSearchResultText(item)
|
||||
mediaType, mediaCategory := s.classifySubscriptionItem(ctx, sub, matchText, item.Category)
|
||||
if s.shouldSkipExistingTorrent(ctx, mediaType, candidate) {
|
||||
addSiteSearchCandidateAvailability(candidate, &availability)
|
||||
seen = append(seen, candidate.GUID)
|
||||
seenSet[candidate.GUID] = struct{}{}
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, keyword)
|
||||
fields = append(fields,
|
||||
zap.String("reason", "existing_torrent"),
|
||||
zap.String("title", item.Title),
|
||||
zap.String("subtitle", item.Subtitle),
|
||||
zap.String("site", firstNonEmpty(item.SiteName, item.SiteID)),
|
||||
zap.String("site_category", item.Category),
|
||||
zap.Int("season", candidate.Season),
|
||||
zap.Int("episode", candidate.Episode),
|
||||
zap.Bool("pack", candidate.Pack),
|
||||
zap.String("media_type", mediaType),
|
||||
)
|
||||
s.log.Info("site-search subscription candidate skipped", fields...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
realURL := s.site.ResolveDownloadURL(ctx, candidate.Download)
|
||||
savePath := s.resolveSubscriptionSavePath(ctx, sub, mediaType, mediaCategory)
|
||||
if s.downloadPathHasCandidate(ctx, sub, matchText, savePath) {
|
||||
addSiteSearchCandidateAvailability(candidate, &availability)
|
||||
seen = append(seen, candidate.GUID)
|
||||
seenSet[candidate.GUID] = struct{}{}
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, keyword)
|
||||
fields = append(fields,
|
||||
zap.String("reason", "download_path_has_candidate"),
|
||||
zap.String("title", item.Title),
|
||||
zap.String("subtitle", item.Subtitle),
|
||||
zap.String("site", firstNonEmpty(item.SiteName, item.SiteID)),
|
||||
zap.String("site_category", item.Category),
|
||||
zap.Int("season", candidate.Season),
|
||||
zap.Int("episode", candidate.Episode),
|
||||
zap.Bool("pack", candidate.Pack),
|
||||
zap.String("media_type", mediaType),
|
||||
zap.String("media_category", mediaCategory),
|
||||
zap.String("save_path", savePath),
|
||||
)
|
||||
s.log.Info("site-search subscription candidate skipped", fields...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := s.downloads.AddDownloadWithMeta(ctx, sub.UserID, realURL, savePath, DownloadTaskMeta{
|
||||
SubscriptionID: sub.ID,
|
||||
Title: firstNonEmpty(item.Title, sub.Name),
|
||||
PosterURL: sub.PosterURL,
|
||||
BackdropURL: sub.BackdropURL,
|
||||
Overview: sub.Overview,
|
||||
MediaType: mediaType,
|
||||
MediaCategory: mediaCategory,
|
||||
SourceCategory: item.Category,
|
||||
AllowExistingLibrary: sub.WashEnabled,
|
||||
}); err != nil {
|
||||
if IsDownloadDedupError(err) {
|
||||
addSiteSearchCandidateAvailability(candidate, &availability)
|
||||
seen = append(seen, candidate.GUID)
|
||||
seenSet[candidate.GUID] = struct{}{}
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, keyword)
|
||||
fields = append(fields,
|
||||
zap.String("reason", "download_dedup"),
|
||||
zap.String("title", item.Title),
|
||||
zap.String("subtitle", item.Subtitle),
|
||||
zap.String("site", firstNonEmpty(item.SiteName, item.SiteID)),
|
||||
zap.String("site_category", item.Category),
|
||||
zap.Int("season", candidate.Season),
|
||||
zap.Int("episode", candidate.Episode),
|
||||
zap.Bool("pack", candidate.Pack),
|
||||
zap.String("media_type", mediaType),
|
||||
zap.String("media_category", mediaCategory),
|
||||
zap.String("save_path", savePath),
|
||||
)
|
||||
s.log.Info("site-search subscription candidate skipped", fields...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
lastEnqueueErr = err
|
||||
s.log.Warn("site-search subscription enqueue failed",
|
||||
zap.String("subscription_id", sub.ID),
|
||||
zap.String("subscription", sub.Name),
|
||||
zap.String("keyword", keyword),
|
||||
zap.String("title", item.Title),
|
||||
zap.String("subtitle", item.Subtitle),
|
||||
zap.String("site", firstNonEmpty(item.SiteName, item.SiteID)),
|
||||
zap.String("site_category", item.Category),
|
||||
zap.String("media_type", mediaType),
|
||||
zap.String("media_category", mediaCategory),
|
||||
zap.String("save_path", savePath),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
queued++
|
||||
addSiteSearchCandidateAvailability(candidate, &availability)
|
||||
resources = append(resources, item.Title)
|
||||
seen = append(seen, candidate.GUID)
|
||||
seenSet[candidate.GUID] = struct{}{}
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, keyword)
|
||||
fields = append(fields,
|
||||
zap.String("title", item.Title),
|
||||
zap.String("subtitle", item.Subtitle),
|
||||
zap.String("site", firstNonEmpty(item.SiteName, item.SiteID)),
|
||||
zap.String("site_category", item.Category),
|
||||
zap.Int("season", candidate.Season),
|
||||
zap.Int("episode", candidate.Episode),
|
||||
zap.Bool("pack", candidate.Pack),
|
||||
zap.Int("score", candidate.Score),
|
||||
zap.String("media_type", mediaType),
|
||||
zap.String("media_category", mediaCategory),
|
||||
zap.String("save_path", savePath),
|
||||
)
|
||||
s.log.Info("site-search subscription candidate queued", fields...)
|
||||
}
|
||||
}
|
||||
availability = s.finalizePendingAvailability(sub, availability)
|
||||
if len(seen) > 200 {
|
||||
seen = seen[len(seen)-200:]
|
||||
}
|
||||
_ = s.repo.Setting.Set(ctx, guidKey, strings.Join(seen, "\n"))
|
||||
now := time.Now()
|
||||
_ = s.repo.DB.Model(sub).Updates(map[string]any{"last_run_at": &now}).Error
|
||||
_ = s.archiveCompletedSubscription(ctx, sub, availability)
|
||||
if queued > 0 {
|
||||
s.hub.Publish("subscription", map[string]any{
|
||||
"id": sub.ID,
|
||||
"name": sub.Name,
|
||||
"queued": queued,
|
||||
"keyword": keyword,
|
||||
"resources": resources,
|
||||
})
|
||||
s.notifySubscriptionHit(sub, queued, resources)
|
||||
return queued, nil
|
||||
}
|
||||
if lastEnqueueErr != nil {
|
||||
return 0, fmt.Errorf("找到 PT 资源但加入下载器失败: %w", lastEnqueueErr)
|
||||
}
|
||||
if s.log != nil {
|
||||
fields := subscriptionSiteSearchLogFields(sub, keyword)
|
||||
fields = appendSiteSearchSelectionLogFields(fields, selectionStats)
|
||||
fields = appendAvailabilityLogFields(fields, availability)
|
||||
fields = append(fields, zap.Int("queued", queued))
|
||||
s.log.Info("site-search subscription no candidate queued", fields...)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
Reference in New Issue
Block a user