package main import ( "encoding/xml" "fmt" "io" "log" "net/http" "os" "path" "strings" "time" "github.com/anacrolix/torrent" ) // TorznabResponse represents the XML response structure type TorznabResponse struct { XMLName xml.Name `xml:"rss"` Channel Channel `xml:"channel"` } type Channel struct { Title string `xml:"title"` Description string `xml:"description"` Items []Item `xml:"item"` } type Item struct { Title string `xml:"title"` Link string `xml:"link"` Size int64 `xml:"size"` PubDate time.Time `xml:"pubDate"` InfoHash string `xml:"infohash"` MagnetURI string `xml:"magnetURI"` } // Indexer handles the book searching and downloading type Indexer struct { downloadDir string client *torrent.Client } // NewIndexer creates a new indexer instance func NewIndexer(downloadDir string) (*Indexer, error) { cfg := torrent.NewDefaultClientConfig() cfg.DataDir = downloadDir client, err := torrent.NewClient(cfg) if err != nil { return nil, fmt.Errorf("failed to create torrent client: %v", err) } return &Indexer{ downloadDir: downloadDir, client: client, }, nil } // Search performs a search on oceanpdf for ebooks func (i *Indexer) Search(query string) (*TorznabResponse, error) { // Replace with actual oceanpdf search endpoint searchURL := fmt.Sprintf("https://oceanofpdf.com/?q=%s", query) resp, err := http.Get(searchURL) if err != nil { return nil, fmt.Errorf("search failed: %v", err) } defer resp.Body.Close() var result TorznabResponse if err := xml.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed to decode response: %v", err) } return &result, nil } // Download starts downloading a book and creates a torrent func (i *Indexer) Download(item Item) error { // Download the book resp, err := http.Get(item.Link) if err != nil { return fmt.Errorf("download failed: %v", err) } defer resp.Body.Close() // Create file fileName := path.Join(i.downloadDir, sanitizeFilename(item.Title)+".pdf") out, err := os.Create(fileName) if err != nil { return fmt.Errorf("failed to create file: %v", err) } defer out.Close() // Copy data if _, err := io.Copy(out, resp.Body); err != nil { return fmt.Errorf("failed to write file: %v", err) } // Create torrent t, err := i.client.AddMagnet(item.MagnetURI) if err != nil { return fmt.Errorf("failed to add magnet: %v", err) } <-t.GotInfo() t.DownloadAll() return nil } // StartServer starts the Torznab API server func (i *Indexer) StartServer(port int) error { http.HandleFunc("/api", i.handleAPI) return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) } func (i *Indexer) handleAPI(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") result, err := i.Search(query) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/xml") xml.NewEncoder(w).Encode(result) } func sanitizeFilename(filename string) string { // Remove invalid characters return strings.Map(func(r rune) rune { if strings.ContainsRune(`<>:"/\|?*`, r) { return '-' } return r }, filename) } func main() { indexer, err := NewIndexer("./downloads") if err != nil { log.Fatal(err) } log.Printf("Starting Torznab server on port 9117...") if err := indexer.StartServer(9117); err != nil { log.Fatal(err) } }