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:
+50
-12
@@ -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})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user