package magnet import ( "encoding/base32" "encoding/hex" "errors" "fmt" "net/url" "strings" ) type MagnetLink struct { InfoHash [20]byte Name string Trackers []string } func Parse(uri string) (*MagnetLink, error) { uri = strings.TrimSpace(uri) if !strings.HasPrefix(uri, "magnet:") { return nil, errors.New("invalid magnet link prefix") } parts := strings.SplitN(uri, "?", 2) if len(parts) < 2 { return nil, errors.New("magnet link missing query parameters") } q, err := url.ParseQuery(parts[1]) if err != nil { return nil, fmt.Errorf("failed to parse magnet link query: %w", err) } xts := q["xt"] if len(xts) == 0 { return nil, errors.New("magnet link missing exact topic (xt)") } var infoHash [20]byte foundHash := false for _, xt := range xts { if strings.HasPrefix(xt, "urn:btih:") { hashStr := strings.TrimPrefix(xt, "urn:btih:") if len(hashStr) == 40 { hashBytes, err := hex.DecodeString(hashStr) if err == nil { copy(infoHash[:], hashBytes) foundHash = true break } } else if len(hashStr) == 32 { // Some magnet links have base32 encoded infohash (Base32 without padding is common, or with padding) // Try standard encoding first, maybe upper/lower case. hashStrUpper := strings.ToUpper(hashStr) hashBytes, err := base32.StdEncoding.DecodeString(hashStrUpper) if err != nil { hashBytes, err = base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(hashStrUpper) } if err == nil { copy(infoHash[:], hashBytes) foundHash = true break } } } } if !foundHash { return nil, errors.New("magnet link missing valid BitTorrent info hash (urn:btih:)") } ml := &MagnetLink{ InfoHash: infoHash, Trackers: q["tr"], } dns := q["dn"] if len(dns) > 0 { ml.Name = dns[0] } return ml, nil }