package main import ( "encoding/json" "fmt" "html/template" "log" "net/http" "net/url" "regexp" "sort" "strconv" "strings" "time" ) type Settings struct { UxLang string Safe string } type TorrentSite interface { Name() string Search(query string, category string) ([]TorrentResult, error) } var ( torrentGalaxy TorrentSite nyaa TorrentSite thePirateBay TorrentSite rutor TorrentSite ) var fileResultsChan = make(chan []TorrentResult) func initializeTorrentSites() { torrentGalaxy = NewTorrentGalaxy() // nyaa = NewNyaa() thePirateBay = NewThePirateBay() // rutor = NewRutor() } func handleFileSearch(w http.ResponseWriter, query, safe, lang string, page int) { startTime := time.Now() cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "true", Lang: lang, Type: "file"} combinedResults := getFileResultsFromCacheOrFetch(cacheKey, query, safe, lang, page) sort.Slice(combinedResults, func(i, j int) bool { return combinedResults[i].Seeders > combinedResults[j].Seeders }) elapsedTime := time.Since(startTime) funcMap := template.FuncMap{ "sub": subtract, "add": add, } tmpl, err := template.New("files.html").Funcs(funcMap).ParseFiles("templates/files.html") if err != nil { log.Printf("Failed to load template: %v", err) http.Error(w, "Failed to load template", http.StatusInternalServerError) return } data := struct { Results []TorrentResult Query string Fetched string Category string Sort string HasPrevPage bool HasNextPage bool Page int Settings Settings }{ Results: combinedResults, Query: query, Fetched: fmt.Sprintf("%.2f", elapsedTime.Seconds()), Category: "all", Sort: "seed", HasPrevPage: page > 1, HasNextPage: len(combinedResults) > 0, Page: page, Settings: Settings{UxLang: lang, Safe: safe}, } // Debugging: Print results before rendering template for _, result := range combinedResults { fmt.Printf("Title: %s, Magnet: %s\n", result.Title, result.Magnet) } if err := tmpl.Execute(w, data); err != nil { log.Printf("Failed to render template: %v", err) http.Error(w, "Failed to render template", http.StatusInternalServerError) } } func getFileResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []TorrentResult { cacheChan := make(chan []SearchResult) var combinedResults []TorrentResult go func() { results, exists := resultsCache.Get(cacheKey) if exists { log.Println("Cache hit") cacheChan <- results } else { log.Println("Cache miss") cacheChan <- nil } }() select { case results := <-cacheChan: if results == nil { combinedResults = fetchFileResults(query, safe, lang, page) if len(combinedResults) > 0 { resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) } } else { _, torrentResults, _ := convertToSpecificResults(results) combinedResults = torrentResults } case <-time.After(2 * time.Second): log.Println("Cache check timeout") combinedResults = fetchFileResults(query, safe, lang, page) if len(combinedResults) > 0 { resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) } } return combinedResults } func fetchFileResults(query, safe, lang string, page int) []TorrentResult { sites := []TorrentSite{torrentGalaxy, nyaa, thePirateBay, rutor} results := []TorrentResult{} for _, site := range sites { if site == nil { continue } res, err := site.Search(query, "all") if err != nil { continue } for _, r := range res { r.Magnet = removeMagnetLink(r.Magnet) // Remove "magnet:", prehaps usless now? results = append(results, r) } } if len(results) == 0 { log.Printf("No file results found for query: %s, trying other nodes", query) results = tryOtherNodesForFileSearch(query, safe, lang, page) } return results } func tryOtherNodesForFileSearch(query, safe, lang string, page int) []TorrentResult { for _, nodeAddr := range peers { results, err := sendFileSearchRequestToNode(nodeAddr, query, safe, lang, page) if err != nil { log.Printf("Error contacting node %s: %v", nodeAddr, err) continue } if len(results) > 0 { return results } } return nil } func sendFileSearchRequestToNode(nodeAddr, query, safe, lang string, page int) ([]TorrentResult, error) { searchParams := struct { Query string `json:"query"` Safe string `json:"safe"` Lang string `json:"lang"` Page int `json:"page"` ResponseAddr string `json:"responseAddr"` }{ Query: query, Safe: safe, Lang: lang, Page: page, ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), } msgBytes, err := json.Marshal(searchParams) if err != nil { return nil, fmt.Errorf("failed to marshal search parameters: %v", err) } msg := Message{ ID: hostID, Type: "search-file", Content: string(msgBytes), } err = sendMessage(nodeAddr, msg) if err != nil { return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) } // Wait for results select { case res := <-fileResultsChan: return res, nil case <-time.After(20 * time.Second): return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) } } func handleFileResultsMessage(msg Message) { var results []TorrentResult err := json.Unmarshal([]byte(msg.Content), &results) if err != nil { log.Printf("Error unmarshalling file results: %v", err) return } log.Printf("Received file results: %+v", results) // Send results to fileResultsChan go func() { fileResultsChan <- results }() } func removeMagnetLink(magnet string) string { // Remove the magnet: prefix unconditionally return strings.TrimPrefix(magnet, "magnet:") } func subtract(a, b int) int { return a - b } func add(a, b int) int { return a + b } func parseInt(s string) int { i, err := strconv.Atoi(s) if err != nil { return 0 } return i } func parseSize(sizeStr string) int64 { sizeStr = strings.TrimSpace(sizeStr) if sizeStr == "" { return 0 } // Use regex to extract numeric value and unit separately re := regexp.MustCompile(`(?i)([\d.]+)\s*([KMGT]?B)`) matches := re.FindStringSubmatch(sizeStr) if len(matches) < 3 { log.Printf("Error parsing size: invalid format %s", sizeStr) return 0 } sizeStr = matches[1] unit := strings.ToUpper(matches[2]) var multiplier int64 = 1 switch unit { case "KB": multiplier = 1024 case "MB": multiplier = 1024 * 1024 case "GB": multiplier = 1024 * 1024 * 1024 case "TB": multiplier = 1024 * 1024 * 1024 * 1024 default: log.Printf("Unknown unit: %s", unit) return 0 } size, err := strconv.ParseFloat(sizeStr, 64) if err != nil { log.Printf("Error parsing size: %v", err) return 0 } return int64(size * float64(multiplier)) } // apparently this is needed so it can announce that magnet link is being used and people start seeding it, but I dont like the fact that I add trackers purposefully func applyTrackers(magnetLink string) string { if magnetLink == "" { return "" } trackers := []string{ "udp://tracker.openbittorrent.com:80/announce", "udp://tracker.opentrackr.org:1337/announce", "udp://tracker.coppersurfer.tk:6969/announce", "udp://tracker.leechers-paradise.org:6969/announce", } for _, tracker := range trackers { magnetLink += "&tr=" + url.QueryEscape(tracker) } return magnetLink } func formatSize(size int64) string { if size >= 1024*1024*1024*1024 { return fmt.Sprintf("%.2f TB", float64(size)/(1024*1024*1024*1024)) } else if size >= 1024*1024*1024 { return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024)) } else if size >= 1024*1024 { return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024)) } else if size >= 1024 { return fmt.Sprintf("%.2f KB", float64(size)/1024) } return fmt.Sprintf("%d B", size) } func sanitizeFileName(name string) string { // Replace spaces with dashes sanitized := regexp.MustCompile(`\s+`).ReplaceAllString(name, "-") // Remove any characters that are not alphanumeric, dashes, or parentheses sanitized = regexp.MustCompile(`[^a-zA-Z0-9\-\(\)]`).ReplaceAllString(sanitized, "") return sanitized }