|
|
package main |
|
|
|
|
|
import ( |
|
|
"bytes" |
|
|
"context" |
|
|
"encoding/json" |
|
|
"errors" |
|
|
"fmt" |
|
|
"io" |
|
|
"log" |
|
|
"math/rand" |
|
|
"net" |
|
|
"net/http" |
|
|
"os" |
|
|
"reflect" |
|
|
"runtime" |
|
|
"strconv" |
|
|
"strings" |
|
|
"sync" |
|
|
"time" |
|
|
|
|
|
"net/url" |
|
|
"path/filepath" |
|
|
|
|
|
"github.com/anacrolix/torrent" |
|
|
"github.com/anacrolix/torrent/metainfo" |
|
|
"github.com/anacrolix/torrent/storage" |
|
|
"golang.org/x/net/proxy" |
|
|
) |
|
|
|
|
|
var ( |
|
|
currentSettings Settings |
|
|
settingsMutex sync.RWMutex |
|
|
) |
|
|
|
|
|
type TorrentSession struct { |
|
|
Client *torrent.Client |
|
|
Torrent *torrent.Torrent |
|
|
Port int |
|
|
LastUsed time.Time |
|
|
} |
|
|
|
|
|
type Settings struct { |
|
|
EnableProxy bool `json:"enableProxy"` |
|
|
ProxyURL string `json:"proxyUrl"` |
|
|
EnableProwlarr bool `json:"enableProwlarr"` |
|
|
ProwlarrHost string `json:"prowlarrHost"` |
|
|
ProwlarrApiKey string `json:"prowlarrApiKey"` |
|
|
EnableJackett bool `json:"enableJackett"` |
|
|
JackettHost string `json:"jackettHost"` |
|
|
JackettApiKey string `json:"jackettApiKey"` |
|
|
} |
|
|
|
|
|
type ProxySettings struct { |
|
|
EnableProxy bool `json:"enableProxy"` |
|
|
ProxyURL string `json:"proxyUrl"` |
|
|
} |
|
|
|
|
|
type ProwlarrSettings struct { |
|
|
EnableProwlarr bool `json:"enableProwlarr"` |
|
|
ProwlarrHost string `json:"prowlarrHost"` |
|
|
ProwlarrApiKey string `json:"prowlarrApiKey"` |
|
|
} |
|
|
|
|
|
type JackettSettings struct { |
|
|
EnableJackett bool `json:"enableJackett"` |
|
|
JackettHost string `json:"jackettHost"` |
|
|
JackettApiKey string `json:"jackettApiKey"` |
|
|
} |
|
|
|
|
|
var ( |
|
|
sessions sync.Map |
|
|
usedPorts sync.Map |
|
|
portMutex sync.Mutex |
|
|
) |
|
|
|
|
|
|
|
|
func formatSize(sizeInBytes float64) string { |
|
|
if sizeInBytes < 1024 { |
|
|
return fmt.Sprintf("%.0f B", sizeInBytes) |
|
|
} |
|
|
|
|
|
sizeInKB := sizeInBytes / 1024 |
|
|
if sizeInKB < 1024 { |
|
|
return fmt.Sprintf("%.2f KB", sizeInKB) |
|
|
} |
|
|
|
|
|
sizeInMB := sizeInKB / 1024 |
|
|
if sizeInMB < 1024 { |
|
|
return fmt.Sprintf("%.2f MB", sizeInMB) |
|
|
} |
|
|
|
|
|
sizeInGB := sizeInMB / 1024 |
|
|
return fmt.Sprintf("%.2f GB", sizeInGB) |
|
|
} |
|
|
|
|
|
var ( |
|
|
proxyTransport = &http.Transport{ |
|
|
|
|
|
TLSHandshakeTimeout: 10 * time.Second, |
|
|
ResponseHeaderTimeout: 20 * time.Second, |
|
|
ExpectContinueTimeout: 1 * time.Second, |
|
|
IdleConnTimeout: 30 * time.Second, |
|
|
MaxIdleConnsPerHost: 10, |
|
|
} |
|
|
proxyClient = &http.Client{ |
|
|
Transport: proxyTransport, |
|
|
Timeout: 30 * time.Second, |
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error { |
|
|
if len(via) >= 10 { |
|
|
return errors.New("too many redirects") |
|
|
} |
|
|
for k, vv := range via[0].Header { |
|
|
if _, ok := req.Header[k]; !ok { |
|
|
req.Header[k] = vv |
|
|
} |
|
|
} |
|
|
return nil |
|
|
}, |
|
|
} |
|
|
) |
|
|
|
|
|
func createSelectiveProxyClient() *http.Client { |
|
|
settingsMutex.RLock() |
|
|
defer settingsMutex.RUnlock() |
|
|
|
|
|
if !currentSettings.EnableProxy { |
|
|
return &http.Client{Timeout: 30 * time.Second} |
|
|
} |
|
|
|
|
|
dialer, _ := createProxyDialer(currentSettings.ProxyURL) |
|
|
proxyTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { |
|
|
return dialer.Dial(network, addr) |
|
|
} |
|
|
|
|
|
proxyTransport.CloseIdleConnections() |
|
|
|
|
|
return proxyClient |
|
|
} |
|
|
|
|
|
|
|
|
func createProxyDialer(proxyURL string) (proxy.Dialer, error) { |
|
|
proxyURLParsed, err := url.Parse(proxyURL) |
|
|
if err != nil { |
|
|
return nil, fmt.Errorf("failed to parse proxy URL: %v", err) |
|
|
} |
|
|
|
|
|
|
|
|
auth := &proxy.Auth{} |
|
|
if proxyURLParsed.User != nil { |
|
|
auth.User = proxyURLParsed.User.Username() |
|
|
if password, ok := proxyURLParsed.User.Password(); ok { |
|
|
auth.Password = password |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return proxy.SOCKS5("tcp", proxyURLParsed.Host, auth, proxy.Direct) |
|
|
} |
|
|
|
|
|
|
|
|
func getAvailablePort() int { |
|
|
portMutex.Lock() |
|
|
defer portMutex.Unlock() |
|
|
|
|
|
|
|
|
for i := 0; i < 50; i++ { |
|
|
|
|
|
port := 10000 + rand.Intn(50000) |
|
|
|
|
|
|
|
|
if _, exists := usedPorts.Load(port); !exists { |
|
|
|
|
|
usedPorts.Store(port, true) |
|
|
return port |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return 60000 + rand.Intn(5000) |
|
|
} |
|
|
|
|
|
|
|
|
func releasePort(port int) { |
|
|
portMutex.Lock() |
|
|
defer portMutex.Unlock() |
|
|
usedPorts.Delete(port) |
|
|
} |
|
|
|
|
|
|
|
|
func initTorrentWithProxy() (*torrent.Client, int, error) { |
|
|
settingsMutex.RLock() |
|
|
enableProxy := currentSettings.EnableProxy |
|
|
proxyURL := currentSettings.ProxyURL |
|
|
settingsMutex.RUnlock() |
|
|
|
|
|
config := torrent.NewDefaultClientConfig() |
|
|
config.DefaultStorage = storage.NewFile("./torrent-data") |
|
|
port := getAvailablePort() |
|
|
config.ListenPort = port |
|
|
|
|
|
if enableProxy { |
|
|
log.Println("Creating torrent client with proxy...") |
|
|
os.Setenv("ALL_PROXY", proxyURL) |
|
|
os.Setenv("SOCKS_PROXY", proxyURL) |
|
|
os.Setenv("HTTP_PROXY", proxyURL) |
|
|
os.Setenv("HTTPS_PROXY", proxyURL) |
|
|
|
|
|
proxyDialer, err := createProxyDialer(proxyURL) |
|
|
if err != nil { |
|
|
releasePort(port) |
|
|
return nil, port, fmt.Errorf("could not create proxy dialer: %v", err) |
|
|
} |
|
|
|
|
|
config.HTTPProxy = func(*http.Request) (*url.URL, error) { |
|
|
return url.Parse(proxyURL) |
|
|
} |
|
|
|
|
|
client, err := torrent.NewClient(config) |
|
|
if err != nil { |
|
|
releasePort(port) |
|
|
return nil, port, err |
|
|
} |
|
|
|
|
|
setValue(client, "dialerNetwork", func(ctx context.Context, network, addr string) (net.Conn, error) { |
|
|
return proxyDialer.Dial(network, addr) |
|
|
}) |
|
|
|
|
|
return client, port, nil |
|
|
} |
|
|
|
|
|
log.Println("Creating torrent client without proxy...") |
|
|
os.Unsetenv("ALL_PROXY") |
|
|
os.Unsetenv("SOCKS_PROXY") |
|
|
os.Unsetenv("HTTP_PROXY") |
|
|
os.Unsetenv("HTTPS_PROXY") |
|
|
|
|
|
client, err := torrent.NewClient(config) |
|
|
if err != nil { |
|
|
releasePort(port) |
|
|
return nil, port, err |
|
|
} |
|
|
return client, port, nil |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func setValue(obj interface{}, fieldName string, value interface{}) { |
|
|
|
|
|
defer func() { |
|
|
if r := recover(); r != nil { |
|
|
log.Printf("Warning: Could not set %s field: %v", fieldName, r) |
|
|
} |
|
|
}() |
|
|
|
|
|
reflectValue := reflect.ValueOf(obj).Elem() |
|
|
field := reflectValue.FieldByName(fieldName) |
|
|
|
|
|
if field.IsValid() && field.CanSet() { |
|
|
field.Set(reflect.ValueOf(value)) |
|
|
log.Printf("Successfully set %s to use proxy", fieldName) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func init() { |
|
|
|
|
|
|
|
|
if _, err := os.Stat("config/settings.json"); os.IsNotExist(err) { |
|
|
log.Println("settings.json not found, creating default settings") |
|
|
defaultSettings := Settings{ |
|
|
EnableProxy: false, |
|
|
ProxyURL: "", |
|
|
EnableProwlarr: false, |
|
|
ProwlarrHost: "", |
|
|
ProwlarrApiKey: "", |
|
|
EnableJackett: false, |
|
|
JackettHost: "", |
|
|
JackettApiKey: "", |
|
|
} |
|
|
|
|
|
if err := os.MkdirAll("config", 0755); err != nil { |
|
|
log.Fatalf("Failed to create config directory: %v", err) |
|
|
} |
|
|
settingsFile, err := os.Create("config/settings.json") |
|
|
if err != nil { |
|
|
log.Fatalf("Failed to create settings.json: %v", err) |
|
|
} |
|
|
defer settingsFile.Close() |
|
|
encoder := json.NewEncoder(settingsFile) |
|
|
encoder.SetIndent("", " ") |
|
|
if err := encoder.Encode(defaultSettings); err != nil { |
|
|
log.Fatalf("Failed to encode default settings: %v", err) |
|
|
} |
|
|
log.Println("Default settings created in settings.json") |
|
|
} |
|
|
|
|
|
|
|
|
settingsFile, err := os.Open("config/settings.json") |
|
|
if err != nil { |
|
|
log.Fatalf("Failed to open settings.json: %v", err) |
|
|
} |
|
|
defer settingsFile.Close() |
|
|
|
|
|
var s Settings |
|
|
if err := json.NewDecoder(settingsFile).Decode(&s); err != nil { |
|
|
log.Fatalf("Failed to decode settings.json: %v", err) |
|
|
} |
|
|
|
|
|
settingsMutex.Lock() |
|
|
currentSettings = s |
|
|
settingsMutex.Unlock() |
|
|
} |
|
|
|
|
|
func main() { |
|
|
|
|
|
rand.Seed(time.Now().UnixNano()) |
|
|
|
|
|
|
|
|
setGlobalProxy() |
|
|
|
|
|
|
|
|
http.HandleFunc("/api/v1/torrent/add", addTorrentHandler) |
|
|
http.HandleFunc("/api/v1/torrent/", torrentHandler) |
|
|
http.HandleFunc("/api/v1/settings", func(w http.ResponseWriter, r *http.Request) { |
|
|
if r.Method == http.MethodGet { |
|
|
settingsMutex.RLock() |
|
|
defer settingsMutex.RUnlock() |
|
|
respondWithJSON(w, http.StatusOK, currentSettings) |
|
|
} else { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
} |
|
|
}) |
|
|
http.HandleFunc("/api/v1/settings/proxy", saveProxySettingsHandler) |
|
|
http.HandleFunc("/api/v1/settings/prowlarr", saveProwlarrSettingsHandler) |
|
|
http.HandleFunc("/api/v1/settings/jackett", saveJackettSettingsHandler) |
|
|
http.HandleFunc("/api/v1/prowlarr/search", searchFromProwlarr) |
|
|
http.HandleFunc("/api/v1/jackett/search", searchFromJackett) |
|
|
http.HandleFunc("/api/v1/prowlarr/test", testProwlarrConnection) |
|
|
http.HandleFunc("/api/v1/jackett/test", testJackettConnection) |
|
|
http.HandleFunc("/api/v1/proxy/test", testProxyConnection) |
|
|
http.HandleFunc("/api/v1/torrent/convert", convertTorrentToMagnetHandler) |
|
|
|
|
|
|
|
|
http.Handle("/", http.FileServer(http.Dir("./client"))) |
|
|
http.HandleFunc("/client/", func(w http.ResponseWriter, r *http.Request) { |
|
|
http.StripPrefix("/client/", http.FileServer(http.Dir("./client"))).ServeHTTP(w, r) |
|
|
}) |
|
|
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { |
|
|
http.ServeFile(w, r, "./client/favicon.ico") |
|
|
}) |
|
|
|
|
|
go cleanupSessions() |
|
|
|
|
|
port := 3347 |
|
|
|
|
|
addr := fmt.Sprintf(":%d", port) |
|
|
log.Printf("Attempting to start server on %s", addr) |
|
|
|
|
|
|
|
|
serverStarted := make(chan bool, 1) |
|
|
|
|
|
|
|
|
server := &http.Server{ |
|
|
Addr: addr, |
|
|
Handler: nil, |
|
|
} |
|
|
|
|
|
|
|
|
go func() { |
|
|
err := server.ListenAndServe() |
|
|
if err != nil && err != http.ErrServerClosed { |
|
|
log.Printf("Server failed on %s: %v", addr, err) |
|
|
serverStarted <- false |
|
|
} |
|
|
}() |
|
|
|
|
|
|
|
|
select { |
|
|
case success := <-serverStarted: |
|
|
if !success { |
|
|
log.Printf("Server failed to start on %s", addr) |
|
|
return |
|
|
} |
|
|
case <-time.After(1 * time.Second): |
|
|
|
|
|
log.Printf("🚀 Server successfully started on %s", addr) |
|
|
|
|
|
|
|
|
fmt.Printf("\n------------------------------------------------\n") |
|
|
fmt.Printf("✅ Server started! Open in your browser:\n") |
|
|
fmt.Printf(" http://localhost:%d\n", port) |
|
|
fmt.Printf("------------------------------------------------\n\n") |
|
|
|
|
|
|
|
|
select {} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func setGlobalProxy() { |
|
|
settingsMutex.RLock() |
|
|
enableProxy := currentSettings.EnableProxy |
|
|
proxyURL := currentSettings.ProxyURL |
|
|
settingsMutex.RUnlock() |
|
|
|
|
|
if !enableProxy { |
|
|
log.Println("Proxy is disabled, not setting global HTTP proxy.") |
|
|
return |
|
|
} |
|
|
|
|
|
proxyDialer, err := createProxyDialer(proxyURL) |
|
|
if err != nil { |
|
|
log.Printf("Warning: Could not create proxy dialer: %v", err) |
|
|
return |
|
|
} |
|
|
|
|
|
httpTransport, ok := http.DefaultTransport.(*http.Transport) |
|
|
if ok { |
|
|
httpTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { |
|
|
return proxyDialer.Dial(network, addr) |
|
|
} |
|
|
log.Printf("Successfully configured SOCKS5 proxy for all HTTP traffic: %s", proxyURL) |
|
|
} else { |
|
|
log.Println("⚠️ Warning: Could not override HTTP transport") |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func addTorrentHandler(w http.ResponseWriter, r *http.Request) { |
|
|
var request struct{ Magnet string } |
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request"}) |
|
|
return |
|
|
} |
|
|
|
|
|
magnet := request.Magnet |
|
|
if magnet == "" { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "No magnet link provided"}) |
|
|
} |
|
|
|
|
|
|
|
|
if strings.HasPrefix(request.Magnet, "http") { |
|
|
|
|
|
httpClient := createSelectiveProxyClient() |
|
|
|
|
|
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { |
|
|
return http.ErrUseLastResponse |
|
|
} |
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", request.Magnet, nil) |
|
|
if err != nil { |
|
|
log.Printf("Error creating request: %v", err) |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{ |
|
|
"error": "Invalid URL: " + err.Error(), |
|
|
}) |
|
|
return |
|
|
} |
|
|
|
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") |
|
|
|
|
|
|
|
|
log.Printf("Following Prowlarr URL: %s", request.Magnet) |
|
|
resp, err := httpClient.Do(req) |
|
|
if err != nil { |
|
|
log.Printf("Error following URL: %v", err) |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{ |
|
|
"error": "Failed to download: " + err.Error(), |
|
|
}) |
|
|
return |
|
|
} |
|
|
defer resp.Body.Close() |
|
|
|
|
|
log.Printf("Got response: %d %s", resp.StatusCode, resp.Status) |
|
|
|
|
|
|
|
|
if resp.StatusCode >= 300 && resp.StatusCode < 400 { |
|
|
location := resp.Header.Get("Location") |
|
|
log.Printf("Found redirect to: %s", location) |
|
|
|
|
|
if strings.HasPrefix(location, "magnet:") { |
|
|
log.Printf("Found magnet redirect: %s", location) |
|
|
magnet = location |
|
|
} else { |
|
|
log.Printf("Non-magnet redirect: %s", location) |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{ |
|
|
"error": "URL redirects to non-magnet content", |
|
|
}) |
|
|
return |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if magnet == "" || !strings.HasPrefix(magnet, "magnet:") { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid magnet link"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
client, port, err := initTorrentWithProxy() |
|
|
if err != nil { |
|
|
log.Printf("Client creation error: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, |
|
|
map[string]string{"error": "Failed to create client with proxy"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
defer func() { |
|
|
if client != nil { |
|
|
releasePort(port) |
|
|
client.Close() |
|
|
} |
|
|
}() |
|
|
|
|
|
t, err := client.AddMagnet(magnet) |
|
|
if err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid magnet url"}) |
|
|
return |
|
|
} |
|
|
log.Printf("Torrent added: %s", t.InfoHash().HexString()) |
|
|
|
|
|
select { |
|
|
case <-t.GotInfo(): |
|
|
log.Printf("Successfully got torrent info for %s", t.InfoHash().HexString()) |
|
|
case <-time.After(3 * time.Minute): |
|
|
respondWithJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "Timeout getting info - proxy might be blocking BitTorrent traffic"}) |
|
|
} |
|
|
|
|
|
sessionID := t.InfoHash().HexString() |
|
|
log.Printf("Creating new session with ID: %s", sessionID) |
|
|
sessions.Store(sessionID, &TorrentSession{ |
|
|
Client: client, |
|
|
Torrent: t, |
|
|
Port: port, |
|
|
LastUsed: time.Now(), |
|
|
}) |
|
|
|
|
|
|
|
|
log.Printf("Successfully stored session: %s", sessionID) |
|
|
|
|
|
|
|
|
|
|
|
client = nil |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{"sessionId": sessionID}) |
|
|
} |
|
|
|
|
|
|
|
|
func torrentHandler(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
log.Printf("Torrent handler called with path: %s", r.URL.Path) |
|
|
|
|
|
|
|
|
parts := strings.Split(r.URL.Path, "/") |
|
|
|
|
|
|
|
|
log.Printf("Path parts: %v (length: %d)", parts, len(parts)) |
|
|
|
|
|
|
|
|
if len(parts) < 5 { |
|
|
log.Printf("Invalid path: not enough parts") |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid path"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
sessionID := parts[4] |
|
|
|
|
|
log.Printf("Looking for session with ID: %s", sessionID) |
|
|
|
|
|
|
|
|
var sessionKeys []string |
|
|
sessions.Range(func(key, value interface{}) bool { |
|
|
keyStr, ok := key.(string) |
|
|
if ok { |
|
|
sessionKeys = append(sessionKeys, keyStr) |
|
|
} |
|
|
return true |
|
|
}) |
|
|
log.Printf("Available sessions: %v", sessionKeys) |
|
|
|
|
|
|
|
|
sessionValue, ok := sessions.Load(sessionID) |
|
|
if !ok { |
|
|
log.Printf("Session not found with ID: %s", sessionID) |
|
|
respondWithJSON(w, http.StatusNotFound, map[string]string{ |
|
|
"error": "Session not found", |
|
|
"id": sessionID, |
|
|
"available_sessions": strings.Join(sessionKeys, ", "), |
|
|
}) |
|
|
return |
|
|
} |
|
|
|
|
|
log.Printf("Found session with ID: %s", sessionID) |
|
|
session := sessionValue.(*TorrentSession) |
|
|
session.LastUsed = time.Now() |
|
|
|
|
|
|
|
|
if len(parts) > 5 && parts[5] == "stream" { |
|
|
if len(parts) < 7 { |
|
|
http.Error(w, "Invalid stream path", http.StatusBadRequest) |
|
|
return |
|
|
} |
|
|
|
|
|
fileIndexString := parts[6] |
|
|
|
|
|
fileIndexString = strings.TrimSuffix(fileIndexString, ".vtt") |
|
|
|
|
|
fileIndex, err := strconv.Atoi(fileIndexString) |
|
|
|
|
|
if err != nil { |
|
|
http.Error(w, "Invalid file index", http.StatusBadRequest) |
|
|
return |
|
|
} |
|
|
|
|
|
if fileIndex < 0 || fileIndex >= len(session.Torrent.Files()) { |
|
|
http.Error(w, "File index out of range", http.StatusBadRequest) |
|
|
return |
|
|
} |
|
|
|
|
|
file := session.Torrent.Files()[fileIndex] |
|
|
|
|
|
|
|
|
fileName := file.DisplayPath() |
|
|
extension := strings.ToLower(filepath.Ext(fileName)) |
|
|
|
|
|
log.Printf("Streaming file: %s (type: %s)", fileName, extension) |
|
|
|
|
|
switch extension { |
|
|
case ".mp4": |
|
|
w.Header().Set("Content-Type", "video/mp4") |
|
|
case ".webm": |
|
|
w.Header().Set("Content-Type", "video/webm") |
|
|
case ".mkv": |
|
|
w.Header().Set("Content-Type", "video/x-matroska") |
|
|
case ".avi": |
|
|
w.Header().Set("Content-Type", "video/x-msvideo") |
|
|
case ".srt": |
|
|
|
|
|
if r.URL.Query().Get("format") == "vtt" { |
|
|
w.Header().Set("Content-Type", "text/vtt") |
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
|
|
|
|
|
|
reader := file.NewReader() |
|
|
|
|
|
limitReader := io.LimitReader(reader, 10*1024*1024) |
|
|
srtBytes, err := io.ReadAll(limitReader) |
|
|
if err != nil { |
|
|
http.Error(w, "Failed to read subtitle file", http.StatusInternalServerError) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
vttBytes := convertSRTtoVTT(srtBytes) |
|
|
w.Write(vttBytes) |
|
|
return |
|
|
} else { |
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
} |
|
|
case ".vtt": |
|
|
w.Header().Set("Content-Type", "text/vtt") |
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
case ".sub": |
|
|
w.Header().Set("Content-Type", "text/plain") |
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
default: |
|
|
w.Header().Set("Content-Type", "application/octet-stream") |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
reader := file.NewReader() |
|
|
|
|
|
|
|
|
defer func() { |
|
|
if closer, ok := reader.(io.Closer); ok { |
|
|
closer.Close() |
|
|
println("Closed reader***************************************") |
|
|
} |
|
|
}() |
|
|
println("Serving content*****************************************") |
|
|
http.ServeContent(w, r, fileName, time.Time{}, reader) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
var files []map[string]interface{} |
|
|
for i, file := range session.Torrent.Files() { |
|
|
files = append(files, map[string]interface{}{ |
|
|
"index": i, |
|
|
"name": file.DisplayPath(), |
|
|
"size": file.Length(), |
|
|
}) |
|
|
} |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, files) |
|
|
} |
|
|
|
|
|
|
|
|
func convertSRTtoVTT(srtBytes []byte) []byte { |
|
|
srtContent := string(srtBytes) |
|
|
|
|
|
|
|
|
vttContent := "WEBVTT\n\n" |
|
|
|
|
|
|
|
|
|
|
|
lines := strings.Split(srtContent, "\n") |
|
|
|
|
|
for i := 0; i < len(lines); i++ { |
|
|
line := lines[i] |
|
|
|
|
|
|
|
|
if _, err := strconv.Atoi(strings.TrimSpace(line)); err == nil { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if strings.Contains(line, " --> ") { |
|
|
|
|
|
|
|
|
line = strings.Replace(line, ",", ".", -1) |
|
|
vttContent += line + "\n" |
|
|
} else { |
|
|
vttContent += line + "\n" |
|
|
} |
|
|
} |
|
|
|
|
|
return []byte(vttContent) |
|
|
} |
|
|
|
|
|
|
|
|
func respondWithJSON(w http.ResponseWriter, status int, data interface{}) { |
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
w.WriteHeader(status) |
|
|
json.NewEncoder(w).Encode(data) |
|
|
} |
|
|
|
|
|
|
|
|
func cleanupSessions() { |
|
|
ticker := time.NewTicker(5 * time.Minute) |
|
|
defer ticker.Stop() |
|
|
|
|
|
for range ticker.C { |
|
|
log.Printf("Checking for unused sessions...") |
|
|
sessions.Range(func(key, value interface{}) bool { |
|
|
session := value.(*TorrentSession) |
|
|
|
|
|
if time.Since(session.LastUsed) > 15*time.Minute { |
|
|
releasePort(session.Port) |
|
|
session.Torrent.Drop() |
|
|
session.Client.Close() |
|
|
sessions.Delete(key) |
|
|
log.Printf("Removed unused session: %s", key) |
|
|
} |
|
|
return true |
|
|
}) |
|
|
runtime.GC() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
func testProwlarrConnection(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
|
|
|
|
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
|
|
|
var settings ProwlarrSettings |
|
|
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) |
|
|
return |
|
|
} |
|
|
|
|
|
prowlarrHost := settings.ProwlarrHost |
|
|
prowlarrApiKey := settings.ProwlarrApiKey |
|
|
|
|
|
if prowlarrHost == "" || prowlarrApiKey == "" { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Prowlarr host or API key not set"}) |
|
|
return |
|
|
} |
|
|
|
|
|
client := createSelectiveProxyClient() |
|
|
testURL := fmt.Sprintf("%s/api/v1/system/status", prowlarrHost) |
|
|
|
|
|
req, err := http.NewRequest("GET", testURL, nil) |
|
|
if err != nil { |
|
|
log.Printf("Error creating request: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
req.Header.Set("X-Api-Key", prowlarrApiKey) |
|
|
resp, err := client.Do(req) |
|
|
if err != nil { |
|
|
log.Printf("Error making request to Prowlarr: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Prowlarr: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
defer resp.Body.Close() |
|
|
|
|
|
if resp.StatusCode != http.StatusOK { |
|
|
respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Prowlarr returned status %d", resp.StatusCode)}) |
|
|
return |
|
|
} |
|
|
|
|
|
responseBody, err := io.ReadAll(resp.Body) |
|
|
if err != nil { |
|
|
log.Printf("Error reading response: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Prowlarr response"}) |
|
|
return |
|
|
} |
|
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
w.WriteHeader(http.StatusOK) |
|
|
w.Write(responseBody) |
|
|
} |
|
|
|
|
|
|
|
|
func searchFromProwlarr(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Prowlarr-Host, X-Api-Key") |
|
|
|
|
|
|
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
|
|
|
if r.Method != http.MethodPost { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
} |
|
|
|
|
|
query := r.URL.Query().Get("q") |
|
|
if query == "" { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "No search query provided"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
settingsMutex.RLock() |
|
|
prowlarrHost := currentSettings.ProwlarrHost |
|
|
prowlarrApiKey := currentSettings.ProwlarrApiKey |
|
|
settingsMutex.RUnlock() |
|
|
|
|
|
if prowlarrHost == "" || prowlarrApiKey == "" { |
|
|
http.Error(w, "Prowlarr host or API key not set", http.StatusBadRequest) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
client := createSelectiveProxyClient() |
|
|
|
|
|
|
|
|
searchURL := fmt.Sprintf("%s/api/v1/search?query=%s&limit=10", prowlarrHost, url.QueryEscape(query)) |
|
|
|
|
|
req, err := http.NewRequest("GET", searchURL, nil) |
|
|
if err != nil { |
|
|
log.Printf("Error creating request: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
req.Header.Set("X-Api-Key", prowlarrApiKey) |
|
|
resp, err := client.Do(req) |
|
|
if err != nil { |
|
|
log.Printf("Error making request to Prowlarr: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Prowlarr: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body) |
|
|
if err != nil { |
|
|
log.Printf("Error reading response: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Prowlarr response"}) |
|
|
return |
|
|
} |
|
|
|
|
|
if resp.StatusCode != http.StatusOK { |
|
|
respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Prowlarr returned status %d: %s", resp.StatusCode, string(body))}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
var results []map[string]interface{} |
|
|
if err := json.Unmarshal(body, &results); err != nil { |
|
|
log.Printf("Error parsing JSON: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to parse Prowlarr response"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
var processedResults []map[string]interface{} |
|
|
for _, result := range results { |
|
|
|
|
|
title, hasTitle := result["title"].(string) |
|
|
downloadUrl, hasDownloadUrl := result["downloadUrl"].(string) |
|
|
|
|
|
|
|
|
magnetUrl, hasMagnet := result["magnetUrl"].(string) |
|
|
|
|
|
if !hasTitle || title == "" { |
|
|
|
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if (!hasDownloadUrl || downloadUrl == "") && (!hasMagnet || magnetUrl == "") { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
processedResult := map[string]interface{}{ |
|
|
"title": title, |
|
|
} |
|
|
|
|
|
|
|
|
if hasMagnet && magnetUrl != "" { |
|
|
processedResult["magnetUrl"] = magnetUrl |
|
|
processedResult["directMagnet"] = true |
|
|
} else if hasDownloadUrl && downloadUrl != "" { |
|
|
processedResult["downloadUrl"] = downloadUrl |
|
|
processedResult["directMagnet"] = false |
|
|
} |
|
|
|
|
|
|
|
|
if size, ok := result["size"].(float64); ok { |
|
|
processedResult["size"] = formatSize(size) |
|
|
} |
|
|
|
|
|
if seeders, ok := result["seeders"].(float64); ok { |
|
|
processedResult["seeders"] = seeders |
|
|
} |
|
|
|
|
|
if leechers, ok := result["leechers"].(float64); ok { |
|
|
processedResult["leechers"] = leechers |
|
|
} |
|
|
|
|
|
if indexer, ok := result["indexer"].(string); ok { |
|
|
processedResult["indexer"] = indexer |
|
|
} |
|
|
|
|
|
if publishDate, ok := result["publishDate"].(string); ok { |
|
|
processedResult["publishDate"] = publishDate |
|
|
} |
|
|
|
|
|
if category, ok := result["category"].(string); ok { |
|
|
processedResult["category"] = category |
|
|
} |
|
|
|
|
|
processedResults = append(processedResults, processedResult) |
|
|
} |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, processedResults) |
|
|
} |
|
|
|
|
|
|
|
|
func testJackettConnection(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
|
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
|
|
|
var settings JackettSettings |
|
|
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) |
|
|
return |
|
|
} |
|
|
|
|
|
jackettHost := settings.JackettHost |
|
|
jackettApiKey := settings.JackettApiKey |
|
|
|
|
|
if jackettHost == "" || jackettApiKey == "" { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Jackett host or API key not set"}) |
|
|
return |
|
|
} |
|
|
|
|
|
client := createSelectiveProxyClient() |
|
|
testURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?apikey=%s", jackettHost, jackettApiKey) |
|
|
req, err := http.NewRequest("GET", testURL, nil) |
|
|
if err != nil { |
|
|
log.Printf("Error creating request: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) |
|
|
return |
|
|
} |
|
|
resp, err := client.Do(req) |
|
|
if err != nil { |
|
|
log.Printf("Error making request to Jackett: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Jackett: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
defer resp.Body.Close() |
|
|
if resp.StatusCode != http.StatusOK { |
|
|
respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Jackett returned status %d", resp.StatusCode)}) |
|
|
return |
|
|
} |
|
|
responseBody, err := io.ReadAll(resp.Body) |
|
|
if err != nil { |
|
|
log.Printf("Error reading response: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Jackett response"}) |
|
|
return |
|
|
} |
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
w.WriteHeader(http.StatusOK) |
|
|
w.Write(responseBody) |
|
|
} |
|
|
|
|
|
|
|
|
func searchFromJackett(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
|
|
|
|
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
|
|
|
if r.Method != http.MethodPost { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
} |
|
|
|
|
|
query := r.URL.Query().Get("q") |
|
|
if query == "" { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "No search query provided"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
settingsMutex.RLock() |
|
|
jackettHost := currentSettings.JackettHost |
|
|
jackettApiKey := currentSettings.JackettApiKey |
|
|
settingsMutex.RUnlock() |
|
|
|
|
|
if jackettHost == "" || jackettApiKey == "" { |
|
|
http.Error(w, "Jackett host or API key not set", http.StatusBadRequest) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
client := createSelectiveProxyClient() |
|
|
|
|
|
|
|
|
searchURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?Query=%s&apikey=%s", jackettHost, url.QueryEscape(query), jackettApiKey) |
|
|
|
|
|
req, err := http.NewRequest("GET", searchURL, nil) |
|
|
if err != nil { |
|
|
log.Printf("Error creating request: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
resp, err := client.Do(req) |
|
|
if err != nil { |
|
|
log.Printf("Error making request to Jackett: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to connect to Jackett: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body) |
|
|
if err != nil { |
|
|
log.Printf("Error reading response: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read Jackett response"}) |
|
|
return |
|
|
} |
|
|
|
|
|
if resp.StatusCode != http.StatusOK { |
|
|
respondWithJSON(w, resp.StatusCode, map[string]string{"error": fmt.Sprintf("Jackett returned status %d: %s", resp.StatusCode, string(body))}) |
|
|
return |
|
|
} |
|
|
|
|
|
var jacketResponse struct { |
|
|
Results []map[string]interface{} `json:"Results"` |
|
|
} |
|
|
|
|
|
|
|
|
if err := json.Unmarshal(body, &jacketResponse); err != nil { |
|
|
log.Printf("Error parsing JSON: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to parse Jackett response"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
var processedResults []map[string]interface{} |
|
|
for _, result := range jacketResponse.Results { |
|
|
|
|
|
title, hasTitle := result["Title"].(string) |
|
|
downloadUrl, hasDownloadUrl := result["Link"].(string) |
|
|
|
|
|
|
|
|
magnetUrl, hasMagnet := result["MagnetUri"].(string) |
|
|
|
|
|
if !hasTitle || title == "" { |
|
|
|
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
if (!hasDownloadUrl || downloadUrl == "") && (!hasMagnet || magnetUrl == "") { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
processedResult := map[string]interface{}{ |
|
|
"title": title, |
|
|
} |
|
|
|
|
|
|
|
|
if hasMagnet && magnetUrl != "" && strings.HasPrefix(magnetUrl, "magnet:") { |
|
|
processedResult["magnetUrl"] = magnetUrl |
|
|
processedResult["directMagnet"] = true |
|
|
} else if hasDownloadUrl && downloadUrl != "" { |
|
|
processedResult["downloadUrl"] = downloadUrl |
|
|
processedResult["directMagnet"] = false |
|
|
} |
|
|
|
|
|
|
|
|
if size, ok := result["Size"].(float64); ok { |
|
|
processedResult["size"] = formatSize(size) |
|
|
} |
|
|
|
|
|
if seeders, ok := result["Seeders"].(float64); ok { |
|
|
processedResult["seeders"] = seeders |
|
|
} |
|
|
|
|
|
if leechers, ok := result["Peers"].(float64); ok { |
|
|
processedResult["leechers"] = leechers |
|
|
} |
|
|
|
|
|
if indexer, ok := result["Tracker"].(string); ok { |
|
|
processedResult["indexer"] = indexer |
|
|
} |
|
|
|
|
|
if publishDate, ok := result["PublishDate"].(string); ok { |
|
|
processedResult["publishDate"] = publishDate |
|
|
} |
|
|
|
|
|
if category, ok := result["category"].(string); ok { |
|
|
processedResult["category"] = category |
|
|
} |
|
|
|
|
|
processedResults = append(processedResults, processedResult) |
|
|
} |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, processedResults) |
|
|
} |
|
|
|
|
|
|
|
|
func testProxyConnection(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
|
|
|
|
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
|
|
|
if r.Method != http.MethodPost { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
} |
|
|
|
|
|
var settings ProxySettings |
|
|
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) |
|
|
return |
|
|
} |
|
|
|
|
|
proxyURL := settings.ProxyURL |
|
|
|
|
|
if proxyURL == "" { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Proxy URL not set"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
parsedProxyURL, err := url.Parse(proxyURL) |
|
|
if err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid proxy URL: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
transport := &http.Transport{ |
|
|
Proxy: http.ProxyURL(parsedProxyURL), |
|
|
} |
|
|
|
|
|
|
|
|
client := &http.Client{ |
|
|
Transport: transport, |
|
|
Timeout: 10 * time.Second, |
|
|
} |
|
|
|
|
|
testURL := "https://httpbin.org/ip" |
|
|
req, err := http.NewRequest("GET", testURL, nil) |
|
|
if err != nil { |
|
|
log.Printf("Error creating request: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
resp, err := client.Do(req) |
|
|
if err != nil { |
|
|
log.Printf("Error making request through proxy: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Proxy connection failed: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
defer resp.Body.Close() |
|
|
|
|
|
responseBody, err := io.ReadAll(resp.Body) |
|
|
if err != nil { |
|
|
log.Printf("Error reading response: %v", err) |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read proxy response"}) |
|
|
return |
|
|
} |
|
|
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
|
w.WriteHeader(http.StatusOK) |
|
|
w.Write(responseBody) |
|
|
} |
|
|
|
|
|
|
|
|
func saveSettingsToFile() error { |
|
|
|
|
|
if err := os.MkdirAll("config", 0755); err != nil { |
|
|
log.Fatalf("Failed to create config directory: %v", err) |
|
|
} |
|
|
|
|
|
file, err := os.Create("config/settings.json") |
|
|
if err != nil { |
|
|
return err |
|
|
} |
|
|
defer file.Close() |
|
|
|
|
|
encoder := json.NewEncoder(file) |
|
|
encoder.SetIndent("", " ") |
|
|
if err := encoder.Encode(currentSettings); err != nil { |
|
|
return err |
|
|
} |
|
|
|
|
|
return nil |
|
|
} |
|
|
|
|
|
|
|
|
func saveProxySettingsHandler(w http.ResponseWriter, r *http.Request) { |
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
if r.Method != http.MethodPost { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
} |
|
|
|
|
|
var newSettings ProxySettings |
|
|
if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) |
|
|
return |
|
|
} |
|
|
|
|
|
settingsMutex.RLock() |
|
|
currentSettings.EnableProxy = newSettings.EnableProxy |
|
|
currentSettings.ProxyURL = newSettings.ProxyURL |
|
|
defer settingsMutex.RUnlock() |
|
|
|
|
|
if err := saveSettingsToFile(); err != nil { |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to save settings: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
println("Proxy settings saved successfully") |
|
|
|
|
|
setGlobalProxy() |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{"message": "Proxy settings saved successfully"}) |
|
|
} |
|
|
|
|
|
|
|
|
func saveProwlarrSettingsHandler(w http.ResponseWriter, r *http.Request) { |
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
if r.Method != http.MethodPost { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
} |
|
|
|
|
|
var newSettings ProwlarrSettings |
|
|
if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) |
|
|
return |
|
|
} |
|
|
|
|
|
settingsMutex.RLock() |
|
|
currentSettings.EnableProwlarr = newSettings.EnableProwlarr |
|
|
currentSettings.ProwlarrHost = newSettings.ProwlarrHost |
|
|
currentSettings.ProwlarrApiKey = newSettings.ProwlarrApiKey |
|
|
defer settingsMutex.RUnlock() |
|
|
|
|
|
if err := saveSettingsToFile(); err != nil { |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to save settings: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{"message": "Prowlarr settings saved successfully"}) |
|
|
} |
|
|
|
|
|
|
|
|
func saveJackettSettingsHandler(w http.ResponseWriter, r *http.Request) { |
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
if r.Method != http.MethodPost { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
} |
|
|
|
|
|
var newSettings JackettSettings |
|
|
if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) |
|
|
return |
|
|
} |
|
|
|
|
|
settingsMutex.RLock() |
|
|
currentSettings.EnableJackett = newSettings.EnableJackett |
|
|
currentSettings.JackettHost = newSettings.JackettHost |
|
|
currentSettings.JackettApiKey = newSettings.JackettApiKey |
|
|
defer settingsMutex.RUnlock() |
|
|
|
|
|
if err := saveSettingsToFile(); err != nil { |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to save settings: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{"message": "Jackett settings saved successfully"}) |
|
|
} |
|
|
|
|
|
|
|
|
func convertTorrentToMagnetHandler(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type") |
|
|
|
|
|
if r.Method == "OPTIONS" { |
|
|
return |
|
|
} |
|
|
|
|
|
if r.Method != http.MethodPost { |
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
const maxUploadSize = 10 << 20 |
|
|
if err := r.ParseMultipartForm(maxUploadSize); err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Failed to parse form: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
file, header, err := r.FormFile("torrent") |
|
|
if err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing torrent file"}) |
|
|
return |
|
|
} |
|
|
defer file.Close() |
|
|
|
|
|
|
|
|
if header.Size > maxUploadSize { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "File too large"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
fileBytes, err := io.ReadAll(file) |
|
|
if err != nil { |
|
|
respondWithJSON(w, http.StatusInternalServerError, map[string]string{"error": "Failed to read file"}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
mi, err := metainfo.Load(bytes.NewReader(fileBytes)) |
|
|
if err != nil { |
|
|
respondWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid torrent file: " + err.Error()}) |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
infoHash := mi.HashInfoBytes().String() |
|
|
|
|
|
|
|
|
magnet := fmt.Sprintf("magnet:?xt=urn:btih:%s", infoHash) |
|
|
|
|
|
|
|
|
info, err := mi.UnmarshalInfo() |
|
|
if err == nil { |
|
|
magnet += fmt.Sprintf("&dn=%s", url.QueryEscape(info.Name)) |
|
|
} |
|
|
|
|
|
|
|
|
for _, tier := range mi.AnnounceList { |
|
|
for _, tracker := range tier { |
|
|
magnet += fmt.Sprintf("&tr=%s", url.QueryEscape(tracker)) |
|
|
} |
|
|
} |
|
|
|
|
|
respondWithJSON(w, http.StatusOK, map[string]string{ |
|
|
"magnet": magnet, |
|
|
}) |
|
|
} |
|
|
|