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 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() }