Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
165394e
fix(cross): pointers for rls
KyleSanderson Nov 22, 2025
71eb0c0
string interning
KyleSanderson Nov 22, 2025
1f3f941
vibe fixing
KyleSanderson Nov 22, 2025
3080ac6
remove this stupid fucking function
KyleSanderson Nov 22, 2025
3c6eab6
more dumb shit
KyleSanderson Nov 22, 2025
dbeef0d
fix reannounce
KyleSanderson Nov 22, 2025
6c9ee88
clam
KyleSanderson Nov 22, 2025
0390764
fix(search): restore torznab cache usage on search suggestions
s0up4200 Nov 22, 2025
b8f5ad2
perf(crossseed): optimize candidate lookups and file caching
s0up4200 Nov 22, 2025
41431b0
fix(jackett): honor torznab search timeouts
s0up4200 Nov 22, 2025
8a4798f
dont drop the soap
s0up4200 Nov 22, 2025
4bc8b51
feat(torznab): add priority search scheduler and interactive rate limits
s0up4200 Nov 22, 2025
fced4ce
feat(indexers): run bulk tests in parallel and update statuses live
s0up4200 Nov 22, 2025
5549440
feat(torznab): prioritize search queue for interactive, rss, completi…
s0up4200 Nov 22, 2025
2061809
feat(torznab): pipeline all searches through priority scheduler with …
s0up4200 Nov 23, 2025
4e79c39
refactor(torznab): improve search scheduling with worker tasks and pr…
s0up4200 Nov 23, 2025
6265c17
batch insert files
KyleSanderson Nov 23, 2025
84e9e0e
remove-files
KyleSanderson Nov 23, 2025
c40aed9
optimize filter calls
KyleSanderson Nov 23, 2025
52b4ef6
contentKeyMap
KyleSanderson Nov 23, 2025
06f0e05
pointers
KyleSanderson Nov 23, 2025
12769ec
remove the preallocation loop
KyleSanderson Nov 23, 2025
50cb91b
releasesMatch ptr
KyleSanderson Nov 23, 2025
6f05699
normalize
KyleSanderson Nov 23, 2025
56f195c
variant cache
KyleSanderson Nov 23, 2025
d24c79b
speculative insertion fix
KyleSanderson Nov 23, 2025
4d3aacc
bulk insert
KyleSanderson Nov 23, 2025
a2b3f66
remove manual builders
KyleSanderson Nov 23, 2025
227f00e
uplift go-qbittorrent
KyleSanderson Nov 23, 2025
5d5490d
abyss
KyleSanderson Nov 23, 2025
ea562bb
add go-jackett
KyleSanderson Nov 23, 2025
133b80f
hot-path
KyleSanderson Nov 23, 2025
3b04892
fix(cache): isolate torrent file slices and align interning
s0up4200 Nov 23, 2025
3d872c4
refactor(cache): comment out logging for cached torrent files
s0up4200 Nov 23, 2025
faaafb9
fix(crossseed): harden selectContentDetectionRelease against unrelate…
s0up4200 Nov 23, 2025
caf25f1
fix(torznab): rebase search cache TTL and prefer cached partials
s0up4200 Nov 23, 2025
d07602f
fix(torrents): reorder cross-seed menu actions
s0up4200 Nov 23, 2025
93d708e
please look it over
s0up4200 Nov 24, 2025
cc2e841
chongsheng
KyleSanderson Nov 24, 2025
dc7387a
ratelimiter
KyleSanderson Nov 24, 2025
9ff555b
streaming matching
KyleSanderson Nov 24, 2025
c0e3e62
cap params to stay under limit
KyleSanderson Nov 24, 2025
6b111eb
more caps
KyleSanderson Nov 24, 2025
49c49aa
handle cancelled a bit better
KyleSanderson Nov 24, 2025
6adfb6e
context cancelled
KyleSanderson Nov 24, 2025
f2c1204
submissions should continue
KyleSanderson Nov 24, 2025
c4b6e51
webhook needs cancellation
KyleSanderson Nov 24, 2025
80a4313
restore group matching
KyleSanderson Nov 24, 2025
0346583
clams
KyleSanderson Nov 24, 2025
6678550
fix(jackett): avoid scheduler stalls on rate-limited indexers
s0up4200 Nov 24, 2025
ee1c902
fix(jackett): skip rate-limited indexers and cap waits to keep schedu…
s0up4200 Nov 24, 2025
98bfa3e
fix(web,search): hide recent suggestions after submitting a query
s0up4200 Nov 24, 2025
372f7d9
fix(jackett):keep scheduler priority ordering and skip stalled indexers
s0up4200 Nov 24, 2025
3b8cd5c
fix(jackett): stabilize scheduler dispatch and guard search cache config
s0up4200 Nov 24, 2025
3cad92a
update go.mod
s0up4200 Nov 24, 2025
46074d0
feat(jackett, prowlarr): add User-Agent and Version fields to client …
s0up4200 Nov 24, 2025
043f5db
refactor(jackett): centralize per-indexer search execution
s0up4200 Nov 24, 2025
fefdf34
fix test
s0up4200 Nov 24, 2025
2f6561c
fix(crossseed): use fresh torrent files for alignment renames
s0up4200 Nov 24, 2025
ea22571
fix(qbittorrent): avoid overlapping debounced sync timers
s0up4200 Nov 24, 2025
a9ff749
fix(crossseed): rename episode files directly instead of via root folder
s0up4200 Nov 24, 2025
d2b6d27
test(crossseed): cover multi-extension sidecar rename matching
s0up4200 Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 326 additions & 55 deletions internal/qbittorrent/sync_manager.go

Large diffs are not rendered by default.

195 changes: 195 additions & 0 deletions internal/qbittorrent/sync_manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package qbittorrent

import (
"context"
"fmt"
"testing"

qbt "github.com/autobrr/go-qbittorrent"
"github.com/stretchr/testify/require"
)

func TestNormalizeHashes(t *testing.T) {
t.Parallel()

normalized := normalizeHashes([]string{" ABC123 ", "abc123", "Def456", "def456", ""})

require.Equal(t, []string{"abc123", "def456"}, normalized.canonical)
require.Equal(t, map[string]struct{}{
"abc123": {},
"def456": {},
}, normalized.canonicalSet)
require.Equal(t, "ABC123", normalized.canonicalToPreferred["abc123"])
require.Equal(t, []string{"ABC123", "abc123", "Def456", "def456", "DEF456"}, normalized.lookup)
}

func TestGetTorrentFilesBatch_NormalizesAndCaches(t *testing.T) {
t.Parallel()

ctx := context.Background()

client := &stubTorrentFilesClient{
torrents: []qbt.Torrent{
{Hash: "ABC123", Progress: 1.0},
{Hash: "def456", Progress: 0.5},
},
filesByHash: map[string]qbt.TorrentFiles{
"ABC123": {
{
Name: "cached-a.mkv",
Size: 1,
},
},
"Def456": {
{
Name: "def-file.mkv",
Size: 2,
},
},
},
}

fm := &stubFilesManager{
cached: map[string]qbt.TorrentFiles{
"abc123": {
{
Name: "cached-a.mkv",
Size: 1,
},
},
},
}

sm := &SyncManager{
torrentFilesClientProvider: func(context.Context, int) (torrentFilesClient, error) {
return client, nil
},
}
sm.SetFilesManager(fm)

filesByHash, err := sm.GetTorrentFilesBatch(ctx, 1, []string{" ABC123 ", "abc123", "Def456"})
require.NoError(t, err)

require.Len(t, filesByHash, 2)
require.Contains(t, filesByHash, "abc123")
require.Contains(t, filesByHash, "def456")
require.Equal(t, "cached-a.mkv", filesByHash["abc123"][0].Name)
require.Equal(t, "def-file.mkv", filesByHash["def456"][0].Name)

require.ElementsMatch(t, []string{"abc123", "def456"}, fm.lastHashes)
require.Len(t, fm.cacheCalls, 1)
require.Equal(t, cacheCall{hash: "def456", progress: 0.5}, fm.cacheCalls[0])

require.Len(t, client.requestedHashes, 1)
require.Equal(t, []string{"ABC123", "abc123", "Def456", "def456", "DEF456"}, client.requestedHashes[0])
require.Equal(t, []string{"Def456"}, client.fileRequests)
}

func TestHasTorrentByAnyHash(t *testing.T) {
t.Parallel()

ctx := context.Background()

lookup := &stubTorrentLookup{
torrents: map[string]qbt.Torrent{
"ABC123": {Hash: "ABC123", Name: "first"},
"DEF456": {Hash: "zzz", InfohashV2: "def456", Name: "second"},
},
}

sm := &SyncManager{
torrentLookupProvider: func(context.Context, int) (torrentLookup, error) {
return lookup, nil
},
}

torrent, found, err := sm.HasTorrentByAnyHash(ctx, 1, []string{" abc123 "})
require.NoError(t, err)
require.True(t, found)
require.NotNil(t, torrent)
require.Equal(t, "ABC123", torrent.Hash)

torrent, found, err = sm.HasTorrentByAnyHash(ctx, 1, []string{"def456"})
require.NoError(t, err)
require.True(t, found)
require.NotNil(t, torrent)
require.Equal(t, "zzz", torrent.Hash)
require.Equal(t, "second", torrent.Name)
}

type stubTorrentFilesClient struct {
torrents []qbt.Torrent
filesByHash map[string]qbt.TorrentFiles
requestedHashes [][]string
fileRequests []string
}

func (c *stubTorrentFilesClient) getTorrentsByHashes(hashes []string) []qbt.Torrent {
copied := append([]string(nil), hashes...)
c.requestedHashes = append(c.requestedHashes, copied)
return c.torrents
}

func (c *stubTorrentFilesClient) GetFilesInformationCtx(ctx context.Context, hash string) (*qbt.TorrentFiles, error) {
c.fileRequests = append(c.fileRequests, hash)
files, ok := c.filesByHash[hash]
if !ok {
return nil, fmt.Errorf("no files for hash %s", hash)
}
copied := make(qbt.TorrentFiles, len(files))
copy(copied, files)
return &copied, nil
}

type cacheCall struct {
hash string
progress float64
}

type stubFilesManager struct {
cached map[string]qbt.TorrentFiles
lastHashes []string
cacheCalls []cacheCall
}

func (fm *stubFilesManager) GetCachedFiles(context.Context, int, string, float64) (qbt.TorrentFiles, error) {
return nil, nil
}

func (fm *stubFilesManager) GetCachedFilesBatch(_ context.Context, _ int, hashes []string, _ map[string]float64) (map[string]qbt.TorrentFiles, []string, error) {
fm.lastHashes = append([]string(nil), hashes...)

cached := make(map[string]qbt.TorrentFiles, len(hashes))
missing := make([]string, 0, len(hashes))

for _, hash := range hashes {
if files, ok := fm.cached[hash]; ok {
copied := make(qbt.TorrentFiles, len(files))
copy(copied, files)
cached[hash] = copied
} else {
missing = append(missing, hash)
}
}

return cached, missing, nil
}

func (fm *stubFilesManager) CacheFiles(_ context.Context, _ int, hash string, torrentProgress float64, files qbt.TorrentFiles) error {
fm.cacheCalls = append(fm.cacheCalls, cacheCall{hash: hash, progress: torrentProgress})
fm.cached[hash] = files
return nil
}

func (*stubFilesManager) InvalidateCache(context.Context, int, string) error {
return nil
}

type stubTorrentLookup struct {
torrents map[string]qbt.Torrent
}

func (s *stubTorrentLookup) GetTorrent(hash string) (qbt.Torrent, bool) {
torrent, ok := s.torrents[hash]
return torrent, ok
}
15 changes: 10 additions & 5 deletions internal/services/crossseed/align.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func (s *Service) alignCrossSeedContentPaths(
return
}

canonicalHash := normalizeHash(torrentHash)

trimmedSourceName := strings.TrimSpace(sourceTorrentName)
trimmedMatchedName := strings.TrimSpace(matchedTorrent.Name)
if shouldRenameTorrentDisplay(sourceRelease, matchedRelease) && trimmedMatchedName != "" && trimmedSourceName != trimmedMatchedName {
Expand Down Expand Up @@ -76,8 +78,11 @@ func (s *Service) alignCrossSeedContentPaths(
}

sourceFiles := expectedSourceFiles
if currentFilesPtr, err := s.syncManager.GetTorrentFiles(ctx, instanceID, torrentHash); err == nil && currentFilesPtr != nil && len(*currentFilesPtr) > 0 {
sourceFiles = *currentFilesPtr
filesMap, err := s.syncManager.GetTorrentFilesBatch(ctx, instanceID, []string{torrentHash})
if err == nil {
if currentFiles, ok := filesMap[canonicalHash]; ok && len(currentFiles) > 0 {
sourceFiles = currentFiles
}
}

sourceRoot := detectCommonRoot(sourceFiles)
Expand Down Expand Up @@ -181,7 +186,7 @@ func (s *Service) waitForTorrentAvailability(ctx context.Context, instanceID int
}
}

torrents, err := s.syncManager.GetAllTorrents(ctx, instanceID)
torrents, err := s.syncManager.GetTorrents(ctx, instanceID, qbt.TorrentFilterOptions{Filter: qbt.TorrentFilterAll})
if err == nil {
for _, t := range torrents {
if t.Hash == hash || t.InfohashV1 == hash || t.InfohashV2 == hash {
Expand Down Expand Up @@ -383,7 +388,7 @@ func adjustPathForRootRename(path, oldRoot, newRoot string) string {
return path
}

func shouldRenameTorrentDisplay(newRelease, matchedRelease rls.Release) bool {
func shouldRenameTorrentDisplay(newRelease, matchedRelease *rls.Release) bool {
// Keep episode torrents named after the episode even when pointing at season pack files
if newRelease.Series > 0 && newRelease.Episode > 0 &&
matchedRelease.Series > 0 && matchedRelease.Episode == 0 {
Expand All @@ -392,7 +397,7 @@ func shouldRenameTorrentDisplay(newRelease, matchedRelease rls.Release) bool {
return true
}

func shouldAlignFilesWithCandidate(newRelease, matchedRelease rls.Release) bool {
func shouldAlignFilesWithCandidate(newRelease, matchedRelease *rls.Release) bool {
if newRelease.Series > 0 && newRelease.Episode > 0 &&
matchedRelease.Series > 0 && matchedRelease.Episode == 0 {
return false
Expand Down
16 changes: 8 additions & 8 deletions internal/services/crossseed/align_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,10 @@ func TestShouldRenameTorrentDisplay(t *testing.T) {
seasonPack := rls.Release{Series: 1, Episode: 0}
otherPack := rls.Release{Series: 2, Episode: 0}

require.False(t, shouldRenameTorrentDisplay(episode, seasonPack))
require.True(t, shouldRenameTorrentDisplay(seasonPack, episode))
require.True(t, shouldRenameTorrentDisplay(seasonPack, otherPack))
require.False(t, shouldRenameTorrentDisplay(episode, otherPack))
require.False(t, shouldRenameTorrentDisplay(&episode, &seasonPack))
require.True(t, shouldRenameTorrentDisplay(&seasonPack, &episode))
require.True(t, shouldRenameTorrentDisplay(&seasonPack, &otherPack))
require.False(t, shouldRenameTorrentDisplay(&episode, &otherPack))
}

func TestShouldAlignFilesWithCandidate(t *testing.T) {
Expand All @@ -152,8 +152,8 @@ func TestShouldAlignFilesWithCandidate(t *testing.T) {
seasonPack := rls.Release{Series: 1, Episode: 0}
otherEpisode := rls.Release{Series: 1, Episode: 3}

require.False(t, shouldAlignFilesWithCandidate(episode, seasonPack))
require.True(t, shouldAlignFilesWithCandidate(seasonPack, episode))
require.True(t, shouldAlignFilesWithCandidate(seasonPack, seasonPack))
require.True(t, shouldAlignFilesWithCandidate(episode, otherEpisode))
require.False(t, shouldAlignFilesWithCandidate(&episode, &seasonPack))
require.True(t, shouldAlignFilesWithCandidate(&seasonPack, &episode))
require.True(t, shouldAlignFilesWithCandidate(&seasonPack, &seasonPack))
require.True(t, shouldAlignFilesWithCandidate(&episode, &otherEpisode))
}
Loading
Loading