From ff6bc444d080f91822f112ec560effe669211e4f Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 11 May 2026 20:02:02 +0200 Subject: [PATCH] Prefer torrents with real seeders: resolve connected seeder count from swarm during magnet resolution Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus --- internal/service.go | 62 +++++++++++++++++++++++++++++------- internal/tracker/magnet.go | 17 ++++++++-- test/component/mocks_test.go | 9 ++++-- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/internal/service.go b/internal/service.go index 2d23d4f..4f186ae 100644 --- a/internal/service.go +++ b/internal/service.go @@ -32,6 +32,7 @@ type parsedItem struct { item *indexer.SearchItemResult rel *release.Release torrentData []byte + realSeeders int } type MusicAgregatorService struct { @@ -607,41 +608,61 @@ func (service *MusicAgregatorService) parseSearchResults(searchResult *indexer.S continue } - r, torrentData := service.resolveRelease(parser, item, album) + out := service.resolveRelease(parser, item, album) log.Debug(). Str("title", item.Title). - Str("format", r.Format.String()). - Int("tracks", r.TrackCount). - Bool("lossless", r.Format.IsLossless()). + Str("format", out.rel.Format.String()). + Int("tracks", out.rel.TrackCount). + Bool("lossless", out.rel.Format.IsLossless()). Int("seeders", item.Seeders). + Int("real_seeders", out.realSeeders). Str("tracker", item.Tracker). Msg("release parsed") - parsed = append(parsed, parsedItem{item: item, rel: r, torrentData: torrentData}) + parsed = append(parsed, parsedItem{item: item, rel: out.rel, torrentData: out.torrentData, realSeeders: out.realSeeders}) } log.Debug().Int("total", len(searchResult.Items)).Int("parsed", len(parsed)).Msg("parsing complete") return parsed } -func (service *MusicAgregatorService) resolveRelease(parser *torrentParser.GenericParser, item *indexer.SearchItemResult, album *metadataPb.Album) (*release.Release, []byte) { +type resolveOutput struct { + rel *release.Release + torrentData []byte + realSeeders int +} + +func (service *MusicAgregatorService) resolveRelease(parser *torrentParser.GenericParser, item *indexer.SearchItemResult, album *metadataPb.Album) resolveOutput { if strings.HasPrefix(item.DownloadLink, "magnet:") { log.Trace().Str("title", item.Title).Int("reported_seeders", item.Seeders).Msg("resolving magnet") - torrentData, err := service.magnetResolver.Resolve(item.DownloadLink) + result, err := service.magnetResolver.Resolve(item.DownloadLink) if err != nil { log.Warn().Err(err).Str("title", item.Title).Int("reported_seeders", item.Seeders).Msg("magnet resolve failed, falling back to title parse") - return parser.Parse(item.Title), nil + return resolveOutput{rel: parser.Parse(item.Title)} + } + log.Debug(). + Str("title", item.Title). + Int("reported_seeders", item.Seeders). + Int("real_seeders", result.ConnectedSeeders). + Msg("magnet resolved with seeder info") + return resolveOutput{ + rel: parser.ParseTorrent(result.Data, album), + torrentData: result.Data, + realSeeders: result.ConnectedSeeders, } - return parser.ParseTorrent(torrentData, album), torrentData } torrentData, err := downloadTorrentData(item.DownloadLink) if err != nil { log.Warn().Err(err).Str("title", item.Title).Msg("failed to download torrent, falling back to title parse") - return parser.Parse(item.Title), nil + return resolveOutput{rel: parser.Parse(item.Title)} + } + return resolveOutput{ + rel: parser.ParseTorrent(torrentData, album), + torrentData: torrentData, + realSeeders: -1, } - return parser.ParseTorrent(torrentData, album), torrentData } func filterByQuality(items []parsedItem, quality pb.QualityType) []parsedItem { @@ -663,7 +684,7 @@ func filterByQuality(items []parsedItem, quality pb.QualityType) []parsedItem { func selectBestRelease(items []parsedItem) parsedItem { best := items[0] for _, p := range items[1:] { - if p.item.Seeders > best.item.Seeders { + if betterRelease(p, best) { best = p } } @@ -672,6 +693,7 @@ func selectBestRelease(items []parsedItem) parsedItem { Str("title", best.item.Title). Str("format", best.rel.Format.String()). Int("seeders", best.item.Seeders). + Int("real_seeders", best.realSeeders). Str("tracker", best.item.Tracker). Str("hash", best.rel.InfoHash). Msg("best release selected") @@ -679,6 +701,22 @@ func selectBestRelease(items []parsedItem) parsedItem { return best } +func betterRelease(candidate, current parsedItem) bool { + cHasReal := candidate.realSeeders > 0 + bHasReal := current.realSeeders > 0 + + if cHasReal && !bHasReal { + return true + } + if !cHasReal && bHasReal { + return false + } + if cHasReal && bHasReal { + return candidate.realSeeders > current.realSeeders + } + return candidate.item.Seeders > current.item.Seeders +} + func (service *MusicAgregatorService) addToTorrentClient(best parsedItem) error { if best.rel.InfoHash != "" { existing, err := service.torrentClient.Find(torrent.FindOptions{Hash: best.rel.InfoHash}) diff --git a/internal/tracker/magnet.go b/internal/tracker/magnet.go index fe7a3d5..1302476 100644 --- a/internal/tracker/magnet.go +++ b/internal/tracker/magnet.go @@ -11,8 +11,13 @@ import ( "github.com/rs/zerolog/log" ) +type ResolveResult struct { + Data []byte + ConnectedSeeders int +} + type Resolver interface { - Resolve(magnetURI string) ([]byte, error) + Resolve(magnetURI string) (*ResolveResult, error) } type MagnetResolver struct { @@ -41,7 +46,7 @@ func NewMagnetResolver(timeout time.Duration) (*MagnetResolver, error) { }, nil } -func (r *MagnetResolver) Resolve(magnetURI string) ([]byte, error) { +func (r *MagnetResolver) Resolve(magnetURI string) (*ResolveResult, error) { truncated := magnetURI if len(truncated) > 80 { truncated = truncated[:80] + "..." @@ -104,11 +109,14 @@ func (r *MagnetResolver) Resolve(magnetURI string) ([]byte, error) { } resolved: + stats := t.Stats() info := t.Info() log.Debug(). Str("name", info.Name). Int("files", len(info.Files)). Int64("size", info.TotalLength()). + Int("connected_seeders", stats.ConnectedSeeders). + Int("active_peers", stats.ActivePeers). Msg("magnet resolved") mi := t.Metainfo() @@ -121,7 +129,10 @@ resolved: return nil, fmt.Errorf("marshaling torrent data: %w", err) } - return data, nil + return &ResolveResult{ + Data: data, + ConnectedSeeders: stats.ConnectedSeeders, + }, nil } func (r *MagnetResolver) Close() { diff --git a/test/component/mocks_test.go b/test/component/mocks_test.go index a0e6d8a..c8250de 100644 --- a/test/component/mocks_test.go +++ b/test/component/mocks_test.go @@ -11,6 +11,7 @@ import ( metadataPb "homelab.lan/music-agregator/gen/metadata/v1" "homelab.lan/music-agregator/internal/indexer" "homelab.lan/music-agregator/internal/torrent" + "homelab.lan/music-agregator/internal/tracker" ) type mockMetadataClient struct { @@ -154,9 +155,13 @@ type mockResolver struct { ResolveFunc func(magnetURI string) ([]byte, error) } -func (m *mockResolver) Resolve(magnetURI string) ([]byte, error) { +func (m *mockResolver) Resolve(magnetURI string) (*tracker.ResolveResult, error) { if m.ResolveFunc != nil { - return m.ResolveFunc(magnetURI) + data, err := m.ResolveFunc(magnetURI) + if err != nil { + return nil, err + } + return &tracker.ResolveResult{Data: data, ConnectedSeeders: 5}, nil } return nil, fmt.Errorf("not mocked") }