feat: restructure code; add size and ext checks (#39)
- Refractor code - Add file size and extension checkers - Change repair workflow to use zurg
This commit is contained in:
@@ -1,172 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type DebridConfig struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
APIKey string `json:"api_key"`
|
||||
Folder string `json:"folder"`
|
||||
DownloadUncached bool `json:"download_uncached"`
|
||||
CheckCached bool `json:"check_cached"`
|
||||
RateLimit string `json:"rate_limit"` // 200/minute or 10/second
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
Port string `json:"port"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LogLevel string `json:"log_level"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
CachedOnly *bool `json:"cached_only"`
|
||||
}
|
||||
|
||||
type QBitTorrentConfig struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Port string `json:"port"`
|
||||
LogLevel string `json:"log_level"`
|
||||
DownloadFolder string `json:"download_folder"`
|
||||
Categories []string `json:"categories"`
|
||||
RefreshInterval int `json:"refresh_interval"`
|
||||
}
|
||||
|
||||
type ArrConfig struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type RepairConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Interval string `json:"interval"`
|
||||
RunOnStart bool `json:"run_on_start"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
LogLevel string `json:"log_level"`
|
||||
Debrid DebridConfig `json:"debrid"`
|
||||
Debrids []DebridConfig `json:"debrids"`
|
||||
Proxy ProxyConfig `json:"proxy"`
|
||||
MaxCacheSize int `json:"max_cache_size"`
|
||||
QBitTorrent QBitTorrentConfig `json:"qbittorrent"`
|
||||
Arrs []ArrConfig `json:"arrs"`
|
||||
Repair RepairConfig `json:"repair"`
|
||||
}
|
||||
|
||||
func validateDebrids(debrids []DebridConfig) error {
|
||||
if len(debrids) == 0 {
|
||||
return errors.New("no debrids configured")
|
||||
}
|
||||
|
||||
errChan := make(chan error, len(debrids))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, debrid := range debrids {
|
||||
// Basic field validation
|
||||
if debrid.Host == "" {
|
||||
return errors.New("debrid host is required")
|
||||
}
|
||||
if debrid.APIKey == "" {
|
||||
return errors.New("debrid api key is required")
|
||||
}
|
||||
if debrid.Folder == "" {
|
||||
return errors.New("debrid folder is required")
|
||||
}
|
||||
|
||||
// Check folder existence concurrently
|
||||
wg.Add(1)
|
||||
go func(folder string) {
|
||||
defer wg.Done()
|
||||
if _, err := os.Stat(folder); os.IsNotExist(err) {
|
||||
errChan <- fmt.Errorf("debrid folder does not exist: %s", folder)
|
||||
}
|
||||
}(debrid.Folder)
|
||||
}
|
||||
|
||||
// Wait for all checks to complete
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
// Return first error if any
|
||||
if err := <-errChan; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateQbitTorrent(config *QBitTorrentConfig) error {
|
||||
if config.DownloadFolder == "" {
|
||||
return errors.New("qbittorent download folder is required")
|
||||
}
|
||||
if _, err := os.Stat(config.DownloadFolder); os.IsNotExist(err) {
|
||||
return errors.New("qbittorent download folder does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConfig(config *Config) error {
|
||||
// Run validations concurrently
|
||||
errChan := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
errChan <- validateDebrids(config.Debrids)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
errChan <- validateQbitTorrent(&config.QBitTorrent)
|
||||
}()
|
||||
|
||||
// Check for errors
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
// Load the config file
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}(file)
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
config := &Config{}
|
||||
err = decoder.Decode(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.Debrid.Name != "" {
|
||||
config.Debrids = append(config.Debrids, config.Debrid)
|
||||
}
|
||||
|
||||
// Validate the config
|
||||
//if err := validateConfig(config); err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
var CONFIG *Config = nil
|
||||
@@ -1,80 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetLogPath() string {
|
||||
logsDir := os.Getenv("LOG_PATH")
|
||||
if logsDir == "" {
|
||||
// Create the logs directory if it doesn't exist
|
||||
logsDir = "logs"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
panic(fmt.Sprintf("Failed to create logs directory: %v", err))
|
||||
}
|
||||
|
||||
return filepath.Join(logsDir, "decypharr.log")
|
||||
}
|
||||
|
||||
func NewLogger(prefix string, level string, output *os.File) zerolog.Logger {
|
||||
|
||||
rotatingLogFile := &lumberjack.Logger{
|
||||
Filename: GetLogPath(),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 2,
|
||||
MaxAge: 28,
|
||||
Compress: true,
|
||||
}
|
||||
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: output,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: false, // Set to true if you don't want colors
|
||||
FormatLevel: func(i interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||
},
|
||||
FormatMessage: func(i interface{}) string {
|
||||
return fmt.Sprintf("[%s] %v", prefix, i)
|
||||
},
|
||||
}
|
||||
|
||||
fileWriter := zerolog.ConsoleWriter{
|
||||
Out: rotatingLogFile,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: true, // No colors in file output
|
||||
FormatLevel: func(i interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||
},
|
||||
FormatMessage: func(i interface{}) string {
|
||||
return fmt.Sprintf("[%s] %v", prefix, i)
|
||||
},
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(consoleWriter, fileWriter)
|
||||
|
||||
logger := zerolog.New(multi).
|
||||
With().
|
||||
Timestamp().
|
||||
Logger().
|
||||
Level(zerolog.InfoLevel)
|
||||
|
||||
// Set the log level
|
||||
switch level {
|
||||
case "debug":
|
||||
logger = logger.Level(zerolog.DebugLevel)
|
||||
case "info":
|
||||
logger = logger.Level(zerolog.InfoLevel)
|
||||
case "warn":
|
||||
logger = logger.Level(zerolog.WarnLevel)
|
||||
case "error":
|
||||
logger = logger.Level(zerolog.ErrorLevel)
|
||||
}
|
||||
return logger
|
||||
}
|
||||
+5
-15
@@ -7,12 +7,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
VIDEOMATCH = "(?i)(\\.)(YUV|WMV|WEBM|VOB|VIV|SVI|ROQ|RMVB|RM|OGV|OGG|NSV|MXF|MPG|MPEG|M2V|MP2|MPE|MPV|MP4|M4P|M4V|MOV|QT|MNG|MKV|FLV|DRC|AVI|ASF|AMV|MKA|F4V|3GP|3G2|DIVX|X264|X265)$"
|
||||
MUSICMATCH = "(?i)(\\.)(?:MP3|WAV|FLAC|AAC|OGG|WMA|AIFF|ALAC|M4A|APE|AC3|DTS|M4P|MID|MIDI|MKA|MP2|MPA|RA|VOC|WV|AMR)$"
|
||||
SUBMATCH = "(?i)(\\.)(SRT|SUB|SBV|ASS|VTT|TTML|DFXP|STL|SCC|CAP|SMI|TTXT|TDS|USF|JSS|SSA|PSB|RT|LRC|SSB)$"
|
||||
SAMPLEMATCH = `(?i)(^|[\\/]|[._-])(sample|trailer|thumb)s?([._-]|$)`
|
||||
VIDEOMATCH = "(?i)(\\.)(YUV|WMV|WEBM|VOB|VIV|SVI|ROQ|RMVB|RM|OGV|OGG|NSV|MXF|MPG|MPEG|M2V|MP2|MPE|MPV|MP4|M4P|M4V|MOV|QT|MNG|MKV|FLV|DRC|AVI|ASF|AMV|MKA|F4V|3GP|3G2|DIVX|X264|X265)$"
|
||||
MUSICMATCH = "(?i)(\\.)(?:MP3|WAV|FLAC|AAC|OGG|WMA|AIFF|ALAC|M4A|APE|AC3|DTS|M4P|MID|MIDI|MKA|MP2|MPA|RA|VOC|WV|AMR)$"
|
||||
)
|
||||
|
||||
var SAMPLEMATCH = `(?i)(^|[\\/]|[._-])(sample|trailer|thumb)s?([._-]|$)`
|
||||
|
||||
func RegexMatch(regex string, value string) bool {
|
||||
re := regexp.MustCompile(regex)
|
||||
return re.MatchString(value)
|
||||
@@ -37,7 +37,7 @@ func RemoveInvalidChars(value string) string {
|
||||
}
|
||||
|
||||
func RemoveExtension(value string) string {
|
||||
re := regexp.MustCompile(VIDEOMATCH + "|" + SUBMATCH + "|" + SAMPLEMATCH + "|" + MUSICMATCH)
|
||||
re := regexp.MustCompile(VIDEOMATCH + "|" + SAMPLEMATCH + "|" + MUSICMATCH)
|
||||
|
||||
// Find the last index of the matched extension
|
||||
loc := re.FindStringIndex(value)
|
||||
@@ -47,13 +47,3 @@ func RemoveExtension(value string) string {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func RegexFind(regex string, value string) string {
|
||||
re := regexp.MustCompile(regex)
|
||||
match := re.FindStringSubmatch(value)
|
||||
if len(match) > 0 {
|
||||
return match[0]
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"golang.org/x/time/rate"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RLHTTPClient struct {
|
||||
client *http.Client
|
||||
Ratelimiter *rate.Limiter
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
func (c *RLHTTPClient) Doer(req *http.Request) (*http.Response, error) {
|
||||
if c.Ratelimiter != nil {
|
||||
err := c.Ratelimiter.Wait(req.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *RLHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
backoff := time.Millisecond * 500
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
resp, err = c.Doer(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusTooManyRequests {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Close the response body to prevent resource leakage
|
||||
resp.Body.Close()
|
||||
|
||||
// Wait for the backoff duration before retrying
|
||||
time.Sleep(backoff)
|
||||
|
||||
// Exponential backoff
|
||||
backoff *= 2
|
||||
}
|
||||
|
||||
return resp, fmt.Errorf("max retries exceeded")
|
||||
}
|
||||
|
||||
func (c *RLHTTPClient) MakeRequest(req *http.Request) ([]byte, error) {
|
||||
if c.Headers != nil {
|
||||
for key, value := range c.Headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, _ := io.ReadAll(res.Body)
|
||||
statusOk := strconv.Itoa(res.StatusCode)[0] == '2'
|
||||
if !statusOk {
|
||||
// Add status code error to the body
|
||||
b = append(b, []byte(fmt.Sprintf("\nstatus code: %d", res.StatusCode))...)
|
||||
return nil, fmt.Errorf(string(b))
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}(res.Body)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func NewRLHTTPClient(rl *rate.Limiter, headers map[string]string) *RLHTTPClient {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
c := &RLHTTPClient{
|
||||
client: &http.Client{
|
||||
Transport: tr,
|
||||
},
|
||||
Ratelimiter: rl,
|
||||
Headers: headers,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func ParseRateLimit(rateStr string) *rate.Limiter {
|
||||
if rateStr == "" {
|
||||
return nil
|
||||
}
|
||||
re := regexp.MustCompile(`(\d+)/(minute|second)`)
|
||||
matches := re.FindStringSubmatch(rateStr)
|
||||
if len(matches) != 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
count, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
unit := matches[2]
|
||||
switch unit {
|
||||
case "minute":
|
||||
reqsPerSecond := float64(count) / 60.0
|
||||
return rate.NewLimiter(rate.Limit(reqsPerSecond), 5)
|
||||
case "second":
|
||||
return rate.NewLimiter(rate.Limit(float64(count)), 5)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
-284
@@ -1,284 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base32"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
)
|
||||
|
||||
type Magnet struct {
|
||||
Name string
|
||||
InfoHash string
|
||||
Size int64
|
||||
Link string
|
||||
}
|
||||
|
||||
func GetMagnetFromFile(file io.Reader, filePath string) (*Magnet, error) {
|
||||
if filepath.Ext(filePath) == ".torrent" {
|
||||
torrentData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetMagnetFromBytes(torrentData)
|
||||
} else {
|
||||
// .magnet file
|
||||
magnetLink := ReadMagnetFile(file)
|
||||
return GetMagnetInfo(magnetLink)
|
||||
}
|
||||
}
|
||||
|
||||
func GetMagnetFromUrl(url string) (*Magnet, error) {
|
||||
if strings.HasPrefix(url, "magnet:") {
|
||||
return GetMagnetInfo(url)
|
||||
} else if strings.HasPrefix(url, "http") {
|
||||
return OpenMagnetHttpURL(url)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid url")
|
||||
}
|
||||
|
||||
func GetMagnetFromBytes(torrentData []byte) (*Magnet, error) {
|
||||
// Create a scanner to read the file line by line
|
||||
mi, err := metainfo.Load(bytes.NewReader(torrentData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := mi.HashInfoBytes()
|
||||
infoHash := hash.HexString()
|
||||
info, err := mi.UnmarshalInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Println("InfoHash: ", infoHash)
|
||||
magnet := &Magnet{
|
||||
InfoHash: infoHash,
|
||||
Name: info.Name,
|
||||
Size: info.Length,
|
||||
Link: mi.Magnet(&hash, &info).String(),
|
||||
}
|
||||
return magnet, nil
|
||||
}
|
||||
|
||||
func OpenMagnetFile(filePath string) string {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Println("Error opening file:", err)
|
||||
return ""
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(file) // Ensure the file is closed after the function ends
|
||||
return ReadMagnetFile(file)
|
||||
}
|
||||
|
||||
func ReadMagnetFile(file io.Reader) string {
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
content := scanner.Text()
|
||||
if content != "" {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any errors during scanning
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Println("Error reading file:", err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func OpenMagnetHttpURL(magnetLink string) (*Magnet, error) {
|
||||
resp, err := http.Get(magnetLink)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making GET request: %v", err)
|
||||
}
|
||||
defer func(resp *http.Response) {
|
||||
err := resp.Body.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(resp) // Ensure the response is closed after the function ends
|
||||
torrentData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response body: %v", err)
|
||||
}
|
||||
return GetMagnetFromBytes(torrentData)
|
||||
}
|
||||
|
||||
func GetMagnetInfo(magnetLink string) (*Magnet, error) {
|
||||
if magnetLink == "" {
|
||||
return nil, fmt.Errorf("error getting magnet from file")
|
||||
}
|
||||
|
||||
magnetURI, err := url.Parse(magnetLink)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing magnet link")
|
||||
}
|
||||
|
||||
query := magnetURI.Query()
|
||||
xt := query.Get("xt")
|
||||
dn := query.Get("dn")
|
||||
|
||||
// Extract BTIH
|
||||
parts := strings.Split(xt, ":")
|
||||
btih := ""
|
||||
if len(parts) > 2 {
|
||||
btih = parts[2]
|
||||
}
|
||||
magnet := &Magnet{
|
||||
InfoHash: btih,
|
||||
Name: dn,
|
||||
Size: 0,
|
||||
Link: magnetLink,
|
||||
}
|
||||
return magnet, nil
|
||||
}
|
||||
|
||||
func RandomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func ExtractInfoHash(magnetDesc string) string {
|
||||
const prefix = "xt=urn:btih:"
|
||||
start := strings.Index(magnetDesc, prefix)
|
||||
if start == -1 {
|
||||
return ""
|
||||
}
|
||||
hash := ""
|
||||
start += len(prefix)
|
||||
end := strings.IndexAny(magnetDesc[start:], "&#")
|
||||
if end == -1 {
|
||||
hash = magnetDesc[start:]
|
||||
} else {
|
||||
hash = magnetDesc[start : start+end]
|
||||
}
|
||||
hash, _ = processInfoHash(hash) // Convert to hex if needed
|
||||
return hash
|
||||
}
|
||||
|
||||
func processInfoHash(input string) (string, error) {
|
||||
// Regular expression for a valid 40-character hex infohash
|
||||
hexRegex := regexp.MustCompile("^[0-9a-fA-F]{40}$")
|
||||
|
||||
// If it's already a valid hex infohash, return it as is
|
||||
if hexRegex.MatchString(input) {
|
||||
return strings.ToLower(input), nil
|
||||
}
|
||||
|
||||
// If it's 32 characters long, it might be Base32 encoded
|
||||
if len(input) == 32 {
|
||||
// Ensure the input is uppercase and remove any padding
|
||||
input = strings.ToUpper(strings.TrimRight(input, "="))
|
||||
|
||||
// Try to decode from Base32
|
||||
decoded, err := base32.StdEncoding.DecodeString(input)
|
||||
if err == nil && len(decoded) == 20 {
|
||||
// If successful and the result is 20 bytes, encode to hex
|
||||
return hex.EncodeToString(decoded), nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, it's not a valid infohash and we couldn't convert it
|
||||
return "", fmt.Errorf("invalid infohash: %s", input)
|
||||
}
|
||||
|
||||
func GetInfohashFromURL(url string) (string, error) {
|
||||
// Download the torrent file
|
||||
var magnetLink string
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return fmt.Errorf("stopped after 3 redirects")
|
||||
}
|
||||
if strings.HasPrefix(req.URL.String(), "magnet:") {
|
||||
// Stop the redirect chain
|
||||
magnetLink = req.URL.String()
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if magnetLink != "" {
|
||||
return ExtractInfoHash(magnetLink), nil
|
||||
}
|
||||
|
||||
mi, err := metainfo.Load(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := mi.HashInfoBytes()
|
||||
infoHash := hash.HexString()
|
||||
return infoHash, nil
|
||||
}
|
||||
|
||||
func JoinURL(base string, paths ...string) (string, error) {
|
||||
// Split the last path component to separate query parameters
|
||||
lastPath := paths[len(paths)-1]
|
||||
parts := strings.Split(lastPath, "?")
|
||||
paths[len(paths)-1] = parts[0]
|
||||
|
||||
joined, err := url.JoinPath(base, paths...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add back query parameters if they exist
|
||||
if len(parts) > 1 {
|
||||
return joined + "?" + parts[1], nil
|
||||
}
|
||||
|
||||
return joined, nil
|
||||
}
|
||||
|
||||
func FileReady(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return !os.IsNotExist(err) // Returns true if the file exists
|
||||
}
|
||||
|
||||
func Remove[S ~[]E, E comparable](s S, values ...E) S {
|
||||
result := make(S, 0, len(s))
|
||||
outer:
|
||||
for _, item := range s {
|
||||
for _, v := range values {
|
||||
if item == v {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user