Files
music-agregator/internal/tracker/magnet.go
T

130 lines
3.1 KiB
Go

package tracker
import (
"context"
"fmt"
"time"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/bencode"
"github.com/anacrolix/torrent/metainfo"
"github.com/rs/zerolog/log"
)
type Resolver interface {
Resolve(magnetURI string) ([]byte, error)
}
type MagnetResolver struct {
client *torrent.Client
timeout time.Duration
}
func NewMagnetResolver(timeout time.Duration) (*MagnetResolver, error) {
cfg := torrent.NewDefaultClientConfig()
cfg.DataDir = ""
cfg.NoDHT = false
cfg.NoUpload = true
cfg.Seed = false
cfg.ListenPort = 0
client, err := torrent.NewClient(cfg)
if err != nil {
return nil, fmt.Errorf("creating torrent client: %w", err)
}
log.Info().Dur("timeout", timeout).Msg("magnet resolver initialized")
return &MagnetResolver{
client: client,
timeout: timeout,
}, nil
}
func (r *MagnetResolver) Resolve(magnetURI string) ([]byte, error) {
truncated := magnetURI
if len(truncated) > 80 {
truncated = truncated[:80] + "..."
}
log.Trace().Str("magnet", truncated).Msg("resolving magnet")
t, err := r.client.AddMagnet(magnetURI)
if err != nil {
return nil, fmt.Errorf("adding magnet: %w", err)
}
defer t.Drop()
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
defer cancel()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
noActiveSince := time.Now()
for {
select {
case <-t.GotInfo():
ticker.Stop()
goto resolved
case <-ctx.Done():
stats := t.Stats()
log.Warn().
Str("hash", t.InfoHash().HexString()).
Int("total_peers", stats.TotalPeers).
Int("active_peers", stats.ActivePeers).
Int("pending_peers", stats.PendingPeers).
Int("half_open_peers", stats.HalfOpenPeers).
Int("connected_seeders", stats.ConnectedSeeders).
Msg("magnet resolve timed out")
return nil, fmt.Errorf("timeout resolving magnet after %s: peers=%d active=%d seeders=%d",
r.timeout, stats.TotalPeers, stats.ActivePeers, stats.ConnectedSeeders)
case <-ticker.C:
stats := t.Stats()
log.Trace().
Str("hash", t.InfoHash().HexString()).
Int("total_peers", stats.TotalPeers).
Int("active_peers", stats.ActivePeers).
Int("connected_seeders", stats.ConnectedSeeders).
Msg("magnet resolve waiting")
if stats.ActivePeers > 0 {
noActiveSince = time.Now()
}
if stats.TotalPeers > 0 && time.Since(noActiveSince) > 15*time.Second {
log.Warn().
Str("hash", t.InfoHash().HexString()).
Int("total_peers", stats.TotalPeers).
Int("active_peers", stats.ActivePeers).
Msg("magnet has peers but none active for 15s, giving up early")
return nil, fmt.Errorf("no active peers after 15s: total=%d active=%d", stats.TotalPeers, stats.ActivePeers)
}
}
}
resolved:
info := t.Info()
log.Debug().
Str("name", info.Name).
Int("files", len(info.Files)).
Int64("size", info.TotalLength()).
Msg("magnet resolved")
mi := t.Metainfo()
data, err := bencode.Marshal(metainfo.MetaInfo{
InfoBytes: mi.InfoBytes,
Announce: mi.Announce,
AnnounceList: mi.AnnounceList,
})
if err != nil {
return nil, fmt.Errorf("marshaling torrent data: %w", err)
}
return data, nil
}
func (r *MagnetResolver) Close() {
r.client.Close()
}