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 <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Alexander
2026-05-11 20:02:02 +02:00
parent 3e8b8153b6
commit ff6bc444d0
3 changed files with 71 additions and 17 deletions
+50 -12
View File
@@ -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})
+14 -3
View File
@@ -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() {
+7 -2
View File
@@ -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")
}