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
|
item *indexer.SearchItemResult
|
||||||
rel *release.Release
|
rel *release.Release
|
||||||
torrentData []byte
|
torrentData []byte
|
||||||
|
realSeeders int
|
||||||
}
|
}
|
||||||
|
|
||||||
type MusicAgregatorService struct {
|
type MusicAgregatorService struct {
|
||||||
@@ -607,41 +608,61 @@ func (service *MusicAgregatorService) parseSearchResults(searchResult *indexer.S
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
r, torrentData := service.resolveRelease(parser, item, album)
|
out := service.resolveRelease(parser, item, album)
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("title", item.Title).
|
Str("title", item.Title).
|
||||||
Str("format", r.Format.String()).
|
Str("format", out.rel.Format.String()).
|
||||||
Int("tracks", r.TrackCount).
|
Int("tracks", out.rel.TrackCount).
|
||||||
Bool("lossless", r.Format.IsLossless()).
|
Bool("lossless", out.rel.Format.IsLossless()).
|
||||||
Int("seeders", item.Seeders).
|
Int("seeders", item.Seeders).
|
||||||
|
Int("real_seeders", out.realSeeders).
|
||||||
Str("tracker", item.Tracker).
|
Str("tracker", item.Tracker).
|
||||||
Msg("release parsed")
|
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")
|
log.Debug().Int("total", len(searchResult.Items)).Int("parsed", len(parsed)).Msg("parsing complete")
|
||||||
return parsed
|
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:") {
|
if strings.HasPrefix(item.DownloadLink, "magnet:") {
|
||||||
log.Trace().Str("title", item.Title).Int("reported_seeders", item.Seeders).Msg("resolving 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 {
|
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")
|
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)
|
torrentData, err := downloadTorrentData(item.DownloadLink)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Str("title", item.Title).Msg("failed to download torrent, falling back to title parse")
|
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 {
|
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 {
|
func selectBestRelease(items []parsedItem) parsedItem {
|
||||||
best := items[0]
|
best := items[0]
|
||||||
for _, p := range items[1:] {
|
for _, p := range items[1:] {
|
||||||
if p.item.Seeders > best.item.Seeders {
|
if betterRelease(p, best) {
|
||||||
best = p
|
best = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -672,6 +693,7 @@ func selectBestRelease(items []parsedItem) parsedItem {
|
|||||||
Str("title", best.item.Title).
|
Str("title", best.item.Title).
|
||||||
Str("format", best.rel.Format.String()).
|
Str("format", best.rel.Format.String()).
|
||||||
Int("seeders", best.item.Seeders).
|
Int("seeders", best.item.Seeders).
|
||||||
|
Int("real_seeders", best.realSeeders).
|
||||||
Str("tracker", best.item.Tracker).
|
Str("tracker", best.item.Tracker).
|
||||||
Str("hash", best.rel.InfoHash).
|
Str("hash", best.rel.InfoHash).
|
||||||
Msg("best release selected")
|
Msg("best release selected")
|
||||||
@@ -679,6 +701,22 @@ func selectBestRelease(items []parsedItem) parsedItem {
|
|||||||
return best
|
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 {
|
func (service *MusicAgregatorService) addToTorrentClient(best parsedItem) error {
|
||||||
if best.rel.InfoHash != "" {
|
if best.rel.InfoHash != "" {
|
||||||
existing, err := service.torrentClient.Find(torrent.FindOptions{Hash: best.rel.InfoHash})
|
existing, err := service.torrentClient.Find(torrent.FindOptions{Hash: best.rel.InfoHash})
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ResolveResult struct {
|
||||||
|
Data []byte
|
||||||
|
ConnectedSeeders int
|
||||||
|
}
|
||||||
|
|
||||||
type Resolver interface {
|
type Resolver interface {
|
||||||
Resolve(magnetURI string) ([]byte, error)
|
Resolve(magnetURI string) (*ResolveResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MagnetResolver struct {
|
type MagnetResolver struct {
|
||||||
@@ -41,7 +46,7 @@ func NewMagnetResolver(timeout time.Duration) (*MagnetResolver, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *MagnetResolver) Resolve(magnetURI string) ([]byte, error) {
|
func (r *MagnetResolver) Resolve(magnetURI string) (*ResolveResult, error) {
|
||||||
truncated := magnetURI
|
truncated := magnetURI
|
||||||
if len(truncated) > 80 {
|
if len(truncated) > 80 {
|
||||||
truncated = truncated[:80] + "..."
|
truncated = truncated[:80] + "..."
|
||||||
@@ -104,11 +109,14 @@ func (r *MagnetResolver) Resolve(magnetURI string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
resolved:
|
resolved:
|
||||||
|
|
||||||
|
stats := t.Stats()
|
||||||
info := t.Info()
|
info := t.Info()
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("name", info.Name).
|
Str("name", info.Name).
|
||||||
Int("files", len(info.Files)).
|
Int("files", len(info.Files)).
|
||||||
Int64("size", info.TotalLength()).
|
Int64("size", info.TotalLength()).
|
||||||
|
Int("connected_seeders", stats.ConnectedSeeders).
|
||||||
|
Int("active_peers", stats.ActivePeers).
|
||||||
Msg("magnet resolved")
|
Msg("magnet resolved")
|
||||||
|
|
||||||
mi := t.Metainfo()
|
mi := t.Metainfo()
|
||||||
@@ -121,7 +129,10 @@ resolved:
|
|||||||
return nil, fmt.Errorf("marshaling torrent data: %w", err)
|
return nil, fmt.Errorf("marshaling torrent data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return &ResolveResult{
|
||||||
|
Data: data,
|
||||||
|
ConnectedSeeders: stats.ConnectedSeeders,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *MagnetResolver) Close() {
|
func (r *MagnetResolver) Close() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||||
"homelab.lan/music-agregator/internal/indexer"
|
"homelab.lan/music-agregator/internal/indexer"
|
||||||
"homelab.lan/music-agregator/internal/torrent"
|
"homelab.lan/music-agregator/internal/torrent"
|
||||||
|
"homelab.lan/music-agregator/internal/tracker"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockMetadataClient struct {
|
type mockMetadataClient struct {
|
||||||
@@ -154,9 +155,13 @@ type mockResolver struct {
|
|||||||
ResolveFunc func(magnetURI string) ([]byte, error)
|
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 {
|
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")
|
return nil, fmt.Errorf("not mocked")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user