package main import ( "api/model" "embed" _ "embed" "encoding/json" "flag" "fmt" "html/template" "io" "log" "net/http" "os" "strconv" "time" "github.com/autobrr/go-qbittorrent" "github.com/jmoiron/sqlx" "github.com/joho/godotenv" "github.com/kylesanderson/go-jackett" _ "github.com/mattn/go-sqlite3" ) //go:embed web/dist/* var webOutput embed.FS var ( hostname = flag.String("H", "", "server http address") port = flag.Int("p", 5000, "server http port") ) func sendJSON(w http.ResponseWriter, data any, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(data); err != nil { http.Error(w, err.Error(), 500) } } type JackettTorrent struct { Seeders string Peers string } func checkForNewTorrents(jackettClient *jackett.Client, db *sqlx.DB, m *model.Model, item *model.Item) error { results, err := jackettClient.TVSearch(jackett.TVSearchOptions{ Query: item.Query, }) if err != nil { return fmt.Errorf("couldn't get to jackett api: %v\n", err) } for _, torrent := range results.Channel.Item { size := toIntOr(torrent.Size, 0) category := toIntOr(torrent.Category[0], 5000) pubDate, _ := time.Parse(time.RFC1123Z, torrent.PubDate) seeders := 0 peers := 0 for _, attr := range torrent.Attr { if attr.Name == "seeders" { seeders = toIntOr(attr.Value, 0) } if attr.Name == "peers" { peers = toIntOr(attr.Value, 0) } } guidTorrent, _ := m.GetTorrentByGuidAndItemId(torrent.Guid, item.ID) if guidTorrent != nil { // already have this exact one, for this item. ABORT! continue } // this shit will duplicate. idk if it's ok or not, but fuck it. we ball _, err = db.NamedExec("INSERT INTO torrents (title, guid, indexer, pubdate, size, download_url, seeders, peers, category, item_id) VALUES (:title, :guid, :indexer, :pubdate, :size, :download_url, :seeders, :peers, :category, :item_id)", map[string]any{ "title": torrent.Title, "guid": torrent.Guid, "indexer": torrent.Jackettindexer.ID, "pubdate": pubDate, "size": size, "download_url": torrent.Link, "seeders": seeders, "peers": peers, "category": category, "item_id": item.ID, }) if err != nil { log.Printf("couldn't add new torrent: %v\n", err) continue } } if err := m.UpdateRefreshedAt(item.ID, time.Now()); err != nil { return fmt.Errorf("couldn't update refreshed_at for %d: %v\n", item.ID, err) } return nil } func main() { flag.Parse() godotenv.Load() jackettHost := os.Getenv("JACKETT_HOST") jackettApiKey := os.Getenv("JACKETT_API_KEY") if jackettHost == "" || jackettApiKey == "" { log.Fatal("no JACKETT_HOST or JACKETT_API_KEY env found") } jackettClient := jackett.NewClient(jackett.Config{ Host: jackettHost, APIKey: jackettApiKey, }) qbittorrentHost := os.Getenv("QBITTORRENT_HOST") qbittorrentUsername := os.Getenv("QBITTORRENT_USERNAME") qbittorrentPassword := os.Getenv("QBITTORRENT_PASSWORD") if qbittorrentHost == "" || qbittorrentUsername == "" || qbittorrentPassword == "" { log.Fatal("no QBITTORRENT_HOST, QBITTORRENT_USERNAME or QBITTORRENT_PASSWORD env found") } torrentClient := qbittorrent.NewClient(qbittorrent.Config{ Host: qbittorrentHost, Username: qbittorrentUsername, Password: qbittorrentPassword, }) dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "./sqlite.db" } db, err := sqlx.Connect("sqlite3", dbPath) if err != nil { log.Fatalf("failed to connect to db: %v\n", err) } defer db.Close() m := model.New(db) m.Init() go func(m *model.Model) { for { items, _ := m.GetItems() for _, item := range items { if err := checkForNewTorrents(jackettClient, db, m, item); err != nil { log.Printf("failed to check for new torrents: %v\n", err) continue } } time.Sleep(1 * time.Hour) } }(m) mux := http.NewServeMux() mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if path == "/" { path = "/index.html" } filePath := "web/dist" + path _, err := webOutput.Open(filePath) if err != nil { filePath = "web/dist/index.html" } http.ServeFileFS(w, r, webOutput, filePath) fileName := r.URL.Path if fileName == "/" { fileName = "/web/index.html" } }) mux.HandleFunc("GET /glance", func(w http.ResponseWriter, r *http.Request) { type glanceTorrent struct { Title string } type glanceItem struct { Query string Link string Torrents []glanceTorrent } glanceItems := []glanceItem{} items, err := m.GetItems() if err != nil { http.Error(w, err.Error(), 500) return } baseUrl := r.URL.Query().Get("base_url") for _, item := range items { gItem := glanceItem{ Query: item.Query, Link: fmt.Sprintf("%s?item=%d", baseUrl, item.ID), Torrents: []glanceTorrent{}, } torrents, err := m.GetItemTorrents(item.ID) if err != nil { continue } for _, torrent := range torrents { gItem.Torrents = append(gItem.Torrents, glanceTorrent{ Title: torrent.Title, }) } glanceItems = append(glanceItems, gItem) } tmpl := `