diff --git a/internal/service/subscription.go b/internal/service/subscription.go index 9acc2ed..5209559 100644 --- a/internal/service/subscription.go +++ b/internal/service/subscription.go @@ -315,6 +315,7 @@ func (s *SubscriptionService) enqueueRSSSubscriptionCandidate(ctx context.Contex AllowExistingLibrary: sub.WashEnabled, }); err != nil { if IsDownloadDedupError(err) { + state.markCandidateAvailable(candidate) state.markSeen(candidate.GUID) return false } @@ -358,6 +359,12 @@ func (state *rssSubscriptionRunState) markTitleAvailable(title string) { } } +func (state *rssSubscriptionRunState) markCandidateAvailable(candidate siteSearchCandidate) { + if state.washOff { + addSiteSearchCandidateAvailability(candidate, &state.availability) + } +} + func (state *rssSubscriptionRunState) markSeen(guid string) { state.seen = append(state.seen, guid) if state.seenSet != nil { diff --git a/internal/service/subscription_availability_test.go b/internal/service/subscription_availability_test.go index 5a46173..70d495c 100644 --- a/internal/service/subscription_availability_test.go +++ b/internal/service/subscription_availability_test.go @@ -226,7 +226,7 @@ func TestSubscriptionPendingDownloadAvailabilityIncludesLiveQBTorrents(t *testin } } -func TestSiteSearchDownloadDedupDoesNotMarkCandidateAvailable(t *testing.T) { +func TestSiteSearchDownloadDedupMarksCandidateAvailable(t *testing.T) { db := newServiceTestDB(t, &model.DownloadTask{}, &model.Setting{}) repos := repository.New(db) sub := &model.Subscription{ @@ -281,10 +281,12 @@ func TestSiteSearchDownloadDedupDoesNotMarkCandidateAvailable(t *testing.T) { if title != "" { t.Fatalf("queued title = %q, want empty on dedup", title) } - if _, ok := state.Availability.ExistingEpisodeKeys[episodeKey(1, 7)]; ok { - t.Fatalf("deduped candidate should not mark E07 available: %#v", state.Availability.ExistingEpisodeKeys) + for _, episode := range []int{7, 8} { + if _, ok := state.Availability.ExistingEpisodeKeys[episodeKey(1, episode)]; !ok { + t.Fatalf("deduped candidate should mark E%d available: %#v", episode, state.Availability.ExistingEpisodeKeys) + } } - if len(state.Seen) != 0 { - t.Fatalf("deduped candidate should not be marked seen: %#v", state.Seen) + if len(state.Seen) != 1 || state.Seen[0] != "site|mteam|nanyang-7-8" { + t.Fatalf("deduped candidate should be marked seen: %#v", state.Seen) } } diff --git a/internal/service/subscription_site_search_enqueue.go b/internal/service/subscription_site_search_enqueue.go index 6b5579c..63519dc 100644 --- a/internal/service/subscription_site_search_enqueue.go +++ b/internal/service/subscription_site_search_enqueue.go @@ -68,6 +68,7 @@ func (s *SubscriptionService) enqueueSiteSearchCandidate(ctx context.Context, su AllowExistingLibrary: sub.WashEnabled, }); err != nil { if IsDownloadDedupError(err) { + state.markCandidateAvailable(candidate) s.logSiteSearchCandidateSkipped(sub, state, candidate, "download_dedup", mediaType, mediaCategory, savePath) return "", nil } diff --git a/internal/service/subscription_site_search_enqueue_test.go b/internal/service/subscription_site_search_enqueue_test.go new file mode 100644 index 0000000..1a789c5 --- /dev/null +++ b/internal/service/subscription_site_search_enqueue_test.go @@ -0,0 +1,77 @@ +package service + +import ( + "testing" + + "go.uber.org/zap" + + "github.com/ShukeBta/MediaStationGo/internal/model" + "github.com/ShukeBta/MediaStationGo/internal/repository" +) + +func TestEnqueueSiteSearchDedupMarksEnglishRangeAvailableForChineseSubscription(t *testing.T) { + db := newServiceTestDB(t, &model.DownloadTask{}, &model.Setting{}) + repos := repository.New(db) + if err := repos.Download.Create(t.Context(), &model.DownloadTask{ + UserID: "u1", + Source: "qbittorrent", + URL: "https://pt.example/download?id=old", + Title: "Archives The Nanyang Mystery 2026 S01E07-S01E08 2160p WEB-DL", + SavePath: "/downloads/国产剧", + Status: "queued", + Progress: 0.1, + }); err != nil { + t.Fatal(err) + } + + site := NewSiteService(zap.NewNop(), repos, "") + downloads := NewDownloadService(zap.NewNop(), repos, NewHub(zap.NewNop()), nil) + svc := NewSubscriptionService(nil, zap.NewNop(), repos, downloads, site, NewHub(zap.NewNop())) + sub := &model.Subscription{ + Base: model.Base{ID: "sub-nanyang"}, + UserID: "u1", + Name: "南部档案 自动订阅", + Filter: "南部档案 2026", + MediaType: "tv", + MediaCategory: "国产剧", + SavePath: "/downloads", + TotalEpisodes: 33, + } + state := &siteSearchRunState{ + Keyword: "南部档案 2026", + SeenSet: map[string]struct{}{}, + Availability: LocalAvailability{ + TotalEpisodes: 33, + ExistingEpisodeKeys: map[string]struct{}{}, + MissingEpisodeKeys: map[string]struct{}{}, + }, + } + candidate := siteSearchCandidate{ + Item: SearchResult{ + Title: "Archives The Nanyang Mystery 2026 S01E07-S01E08 2160p WEB-DL", + DownloadURL: "https://pt.example/download?id=new", + }, + Download: "https://pt.example/download?id=new", + GUID: "site|m-team|nanyang-7-8", + Season: 1, + Episode: 7, + Episodes: []int{7, 8}, + Pack: true, + } + + title, err := svc.enqueueSiteSearchCandidate(t.Context(), sub, candidate, state) + if err != nil { + t.Fatalf("enqueueSiteSearchCandidate returned %v, want dedup skip without error", err) + } + if title != "" { + t.Fatalf("title = %q, want empty because candidate was deduped", title) + } + for _, episode := range []int{7, 8} { + if _, ok := state.Availability.ExistingEpisodeKeys[episodeKey(1, episode)]; !ok { + t.Fatalf("availability missing E%d after dedup range: %#v", episode, state.Availability.ExistingEpisodeKeys) + } + } + if _, ok := state.SeenSet[candidate.GUID]; !ok { + t.Fatalf("seen set missing candidate guid after dedup") + } +}