init
This commit is contained in:
417
main.go
Normal file
417
main.go
Normal file
@@ -0,0 +1,417 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"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", "localhost", "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 Torrent struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Guid string `json:"guid" db:"guid"`
|
||||
Indexer string `json:"indexer" db:"indexer"`
|
||||
Pubdate time.Time `json:"pubdate" db:"pubdate"`
|
||||
Size int `json:"size" db:"size"`
|
||||
DownloadURL string `json:"downloadUrl" db:"download_url"`
|
||||
Seeders int `json:"seeders" db:"seeders"`
|
||||
Peers int `json:"peers" db:"peers"`
|
||||
Category int `json:"category" db:"category"`
|
||||
Hash *string `json:"hash" db:"hash"`
|
||||
Downloaded bool `json:"downloaded"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
ItemID int `json:"itemId" db:"item_id"`
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Query string `json:"query" db:"query"`
|
||||
Category int `json:"category" db:"category"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
Torrents []Torrent `json:"torrents,omitempty"`
|
||||
}
|
||||
|
||||
func getItems(db *sqlx.DB) ([]*Item, error) {
|
||||
rows, err := db.Queryx("SELECT * FROM items")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't query db: %v", err)
|
||||
}
|
||||
|
||||
items := []*Item{}
|
||||
for rows.Next() {
|
||||
item := &Item{}
|
||||
if err := rows.StructScan(&item); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func getItemTorrents(db *sqlx.DB, itemId int64) ([]*Torrent, error) {
|
||||
rows, err := db.Queryx("SELECT * FROM torrents WHERE item_id = ?", itemId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't query db: %v", err)
|
||||
}
|
||||
|
||||
torrents := []*Torrent{}
|
||||
for rows.Next() {
|
||||
torrent := &Torrent{}
|
||||
if err := rows.StructScan(&torrent); err != nil {
|
||||
continue
|
||||
}
|
||||
torrents = append(torrents, torrent)
|
||||
}
|
||||
|
||||
return torrents, nil
|
||||
}
|
||||
|
||||
func getTorrentById(db *sqlx.DB, torrentId int64) (Torrent, error) {
|
||||
var torrent Torrent
|
||||
|
||||
row := db.QueryRowx("SELECT * FROM torrents WHERE id = ?", torrentId)
|
||||
if err := row.StructScan(&torrent); err != nil {
|
||||
return torrent, fmt.Errorf("couldn't query torrent: %v", err)
|
||||
}
|
||||
|
||||
return torrent, nil
|
||||
}
|
||||
|
||||
type JackettTorrent struct {
|
||||
Seeders string
|
||||
Peers string
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
db.MustExec(`CREATE TABLE IF NOT EXISTS items (
|
||||
id integer primary key,
|
||||
query varchar not null,
|
||||
category integer not null,
|
||||
created_at datetime default CURRENT_TIMESTAMP
|
||||
)`)
|
||||
|
||||
db.MustExec(`CREATE TABLE IF NOT EXISTS torrents (
|
||||
id integer primary key,
|
||||
title varchar not null,
|
||||
guid varchar not null unique,
|
||||
indexer varchar not null,
|
||||
pubdate datetime not null,
|
||||
size integer not null,
|
||||
download_url varchar,
|
||||
seeders integer not null,
|
||||
peers integer not null,
|
||||
category integer not null,
|
||||
hash varchar,
|
||||
created_at datetime default CURRENT_TIMESTAMP,
|
||||
item_id integer not null,
|
||||
FOREIGN KEY (item_id) REFERENCES users(id)
|
||||
)`)
|
||||
|
||||
go func(db *sqlx.DB) {
|
||||
for {
|
||||
items, _ := getItems(db)
|
||||
for _, item := range items {
|
||||
results, err := jackettClient.TVSearch(jackett.TVSearchOptions{
|
||||
Query: item.Query,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, torrent := range results.Channel.Item {
|
||||
size, _ := strconv.Atoi(torrent.Size)
|
||||
category, _ := strconv.Atoi(torrent.Category[0])
|
||||
pubDate, _ := time.Parse(time.RFC1123Z, torrent.PubDate)
|
||||
|
||||
seeders := 0
|
||||
peers := 0
|
||||
for _, attr := range torrent.Attr {
|
||||
if attr.Name == "seeders" {
|
||||
seeders, _ = strconv.Atoi(attr.Value)
|
||||
}
|
||||
if attr.Name == "peers" {
|
||||
peers, _ = strconv.Atoi(attr.Value)
|
||||
}
|
||||
}
|
||||
|
||||
_, 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 {
|
||||
db.NamedExec("UPDATE torrents SET seeders = :seeders, peers = :peers WHERE id = :id", map[string]any{
|
||||
"seeders": seeders,
|
||||
"peers": peers,
|
||||
"id": item.ID,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}(db)
|
||||
|
||||
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("POST /api/items", func(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Query string
|
||||
Category int
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if body.Query == "" {
|
||||
http.Error(w, "invalid request, no query provided", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if body.Category == 0 {
|
||||
body.Category = 5000
|
||||
}
|
||||
|
||||
res, err := db.NamedExec("INSERT INTO items (query, category) VALUES (:query, :category)", map[string]any{
|
||||
"query": body.Query,
|
||||
"category": body.Category,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSON(w, struct {
|
||||
Id int64 `json:"id"`
|
||||
}{id}, 201)
|
||||
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /api/items", func(w http.ResponseWriter, r *http.Request) {
|
||||
items, err := getItems(db)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 404)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSON(w, items, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /api/items/{id}/torrents", func(w http.ResponseWriter, r *http.Request) {
|
||||
itemId, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
torrents, err := getItemTorrents(db, int64(itemId))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
for _, torrent := range torrents {
|
||||
if torrent.Hash != nil {
|
||||
properties, err := torrentClient.GetTorrentProperties(*torrent.Hash)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
torrent.Downloaded = properties.CompletionDate > -1
|
||||
}
|
||||
}
|
||||
|
||||
sendJSON(w, torrents, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("POST /api/torrents/{id}/download", func(w http.ResponseWriter, r *http.Request) {
|
||||
torrentId, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
torrent, err := getTorrentById(db, int64(torrentId))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 404)
|
||||
return
|
||||
}
|
||||
|
||||
downloadUrl := torrent.DownloadURL
|
||||
if torrent.Indexer == "limetorrents" {
|
||||
downloadUrl, err = getLimeTorrentsDownloadUrl(torrent.Guid)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.Get(downloadUrl)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
infoHash, err := getTorrentInfoHash(data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.NamedExec("UPDATE torrents SET hash = :hash WHERE id = :id", map[string]any{
|
||||
"hash": infoHash,
|
||||
"id": torrent.ID,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if err := torrentClient.AddTorrentFromMemory(data, map[string]string{}); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSON(w, struct {
|
||||
Ok bool `json:"ok"`
|
||||
}{true}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("DELETE /api/items/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
itemId, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.Exec("DELETE FROM items WHERE id = ?", itemId); err != nil {
|
||||
http.Error(w, err.Error(), 404)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.Exec("DELETE FROM torrents WHERE item_id = ?", itemId); err != nil {
|
||||
http.Error(w, err.Error(), 404)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSON(w, struct {
|
||||
Ok bool `json:"ok"`
|
||||
}{true}, 200)
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", *hostname, *port)
|
||||
fmt.Printf("starting http server on http://%s\n", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Fatalf("failed to start http server: %v\n", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user