From b56e6245b6878f9d8d082a044de301ece9f7210e Mon Sep 17 00:00:00 2001 From: Book Father Date: Wed, 21 Aug 2024 18:20:19 +0000 Subject: [PATCH] Update 6 files - /build-for-synology.sh - /Dockerfile - /main.go - /go.sum - /go.mod - /README.md --- Dockerfile | 28 ++-- README.md | 110 +++++++-------- build-for-synology.sh | 21 --- go.mod | 18 +-- go.sum | 57 +------- main.go | 311 +++++++++++++++++++----------------------- 6 files changed, 213 insertions(+), 332 deletions(-) delete mode 100755 build-for-synology.sh diff --git a/Dockerfile b/Dockerfile index ce26bd5..30966bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,18 @@ -FROM golang:1.17-alpine +FROM golang:1.22-alpine -ADD . /app WORKDIR /app -RUN echo "export export GO111MODULE=auto" >> /root/.bashrc -RUN go mod init github.com/agunal/krantor -RUN apk add git \ - && go get github.com/putdotio/go-putio@latest \ - && go get github.com/fsnotify/fsnotify@latest \ - && go get golang.org/x/oauth2@latest \ - && go mod tidy - -RUN go build -o krantor -CMD ["/app/krantor"] + +# Copy go.mod and go.sum files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy the rest of the application's source code +COPY . . + +# Build the application +RUN go build -o krantorbox + +# Run the application +CMD ["/app/krantorbox"] \ No newline at end of file diff --git a/README.md b/README.md index 36b8498..3a46325 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,29 @@ -# Krantor +# KranTorbox -Watches on local directory for new .torrent or .magnet files. When any are added, it uploads to a single put.io folder. +Watches a local directory for new .torrent, .magnet, .usenet files and uploads them to TorBox. +This serverse as a focussed, lightweight alternative to [West's Blackhole Script](https://github.com/westsurname/scripts). -## Changelog +Forked from [Paul Irish's Krantor](https://gitlab.com/paulirish/krantor), itself a fork of [Krantor by klippz](https://gitlab.com/klippz/krantor) - look to those for put.io support. -* 2023 Oct (paulirish): Add retries to fix 'context deadline exceeded' timeout on uploads. Update deps -* 2023 July ([agunal](https://github.com/agunal/krantor)) Fix dockerfile, add threading, timeout -* 2023 May ([paulirish](https://gitlab.com/paulirish/krantor)): Switch to inotify, using less CPU/energy (from polling every 100ms to waiting on events). Handle multiple files being added at once. -* 2020 Sept ([klippz](https://gitlab.com/klippz/krantor)): Original commits +## Differences from Krantor + + +**TorBox** ✅ + +**put.io** ❌ + + + +**Added**: Usenet | Option to delete .torrent, .magnet, .usenet files after upload. + + +**Removed**: Synology build script (I'm unable to test). ## Table of Contents +* [Requirements](#requirements) * [Installation](#installation) * [Configuration](#configuration) * [Advanced Usage](#advanced-usage) @@ -21,55 +32,41 @@ Watches on local directory for new .torrent or .magnet files. When any are added * [How to use with Sonarr/Radarr](#how-to-use-with-sonarr/radarr) * [Example](#example) -## Installation +## Requirements + +Docker, Go -Just build the image with the given Dockerfile: +## Installation - docker build --no-cache -t krantor . +Build the image with the given Dockerfile: -Or build the binary for your target platform. + docker build --no-cache -t krantorbox . ## Configuration -To make it run, you need to set 3 ENV variables: +2 ENV variables are required - **copy your API Key from [torbox.app/settings](https://torbox.app/settings)**: ``` -PUTIO_TOKEN [Putio Token to communication with their APIs] -PUTIO_WATCH_FOLDER [Folder to watch for new files] -PUTIO_DOWNLOAD_FOLDER_ID [Go into your put.io folder in your browser and copy the number in the URL: -https://app.put.io/files/ ] +TORBOX_API_KEY [Key for TorBox's API] +TORBOX_WATCH_FOLDER [Folder to watch for new files] ``` -For oauth token (API Key): https://help.put.io/en/articles/5972538-how-to-get-an-oauth-token-from-put-io -If you need to watch multiple folders (TV, Movies, etc), you'll have to run multiple times. +1 ENV is optional (and defaults to `false`): +``` +DELETE_AFTER_UPLOAD [Delete original .torrent, .magnet, .usenet file after upload] +``` +### Use within Sonarr / Radarr -### How to use with Sonarr/Radarr -What you have to do is: - * Go to your Radarr/Sonarr configuration - * `Download Client` tab - * Add a new `torrent blackhole` client - * Chose a name - * In torrent & watch folder, put the same folder you set as `PUTIO_WATCH_FOLDER` - * If for `PUTIO_WATCH_FOLDER` you set `/torrent`, you should put the same in torrent & watch folder - * Save magnet file !! - * Done ! + * In Radarr / Sonarr go to `Settings` -> `Download Clients` - *you need to do this for both Radarr and Sonarr separately* -### Integration + * Add `Torrent Blackhole` or `Usenet Blackhole` - *setting up both will work, so repeat the steps if you want to watch for torrents, magnets **and** usenet files* -[From reddit thread](https://www.reddit.com/r/putdotio/comments/136u8r2/comment/jisszuf/)... + * Chose a suitable name e.g. `Torrent Blackhole` ->Use Krantor https://gitlab.com/klippz/krantor This gets torrents from sonarr into put.io Set up as a Download Client > Torrent blackhole. Follow readme instructions, however I do separate local folders for Torrent and Watch. My putio download folder is named `/dropzone/TV``. (I also follow the TRaSH guide for hardlinks) -> ->You need something to automatically download from put.io into your local "downloads" folder. (Probably.) I use `rclone`. Set up rclone and add a putio remote. Test it with rclone ls and stuff. Here's the rclone command that'll move (copy and delete) files from putio to your machine: `rclone -v --config="pathto/rclone.conf" --log-file="pathto/rclone.log" move putio:dropzone/TV /data/Downloads/TV/ --delete-empty-src-dirs` I run this every 30 minutes. You probably want to ensure a second invocation doesn't overlap, so.. handle that with your task scheduler mechanism or manually with `flock``. -> ->I personally never understood how people use Sonarr when all indexers are paid/private except for rarbg (RIP). I found a solution with **Jackett**. In there, I added EZTV, 1337, TPB.. and then hooked Sonarr up to those. Finally both search and rss both work effectively. -> ->If using radarr, repeat all the above with it for movies. I personally get a lot of value from Sonarr, but for movies the chill.institute + download (manually, ftp, rclone, etc) seems fine and radarr seems kinda overkill. But to each their own. :) + * Set `Torrent/Usenet Folder` to your chosen directory e.g. `/blackhole` - *must be the same as your `TORBOX_WATCH_FOLDER`* -### Example -![alt text](https://i.imgur.com/1jUU1xn.png "Example of logs given by Krantor") ## Advanced Usage @@ -77,35 +74,28 @@ What you have to do is: ``` docker create \ - --name=krantor \ - -e PUTIO_TOKEN=xxx \ - -e PUTIO_WATCH_FOLDER=/torrents \ - -e PUTIO_DOWNLOAD_FOLDER_ID=0 \ - -v /path/to/torrent:/torrents \ + --name=krantorbox \ + -e TORBOX_API_KEY=xxx \ + -e TORBOX_WATCH_FOLDER=/blackhole \ + -e DELETE_AFTER_UPLOAD=true \ + -v ./blackhole:/blackhole \ --restart unless-stopped \ - krantor + krantorbox ``` ### Docker-compose ``` ---- -version: "3.7" services: - putio: - image: krantor - container_name: krantor + + krantorbox: + container_name: krantorbox + image: krantorbox:local environment: - - PUTIO_TOKEN=xxx - - PUTIO_WATCH_FOLDER=/torrents - - PUTIO_DOWNLOAD_FOLDER_ID=0 + - TORBOX_API_KEY=xxx + - TORBOX_WATCH_FOLDER=/blackhole + - DELETE_AFTER_UPLOAD=true volumes: - - /path/to/torrent:/torrents + - ./blackhole:/blackhole restart: unless-stopped ``` - -## Hacking - -* Initial setup: `go mod init gitlab.com/paulirish/krantor` -* Dependency update: for direct deps in go.mod, replace versions with `latest` then run `go mod tidy`. -* Running: `PUTIO_TOKEN= PUTIO_WATCH_FOLDER= PUTIO_DOWNLOAD_FOLDER_ID= go run main.go` diff --git a/build-for-synology.sh b/build-for-synology.sh deleted file mode 100755 index d5d2aae..0000000 --- a/build-for-synology.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env sh - -set -eux - -# Run to build a binary for Synology - -# THANK YOU DUDE. https://www.afox.dev/posts/compiling-go-for-synology-nas -# -# brew install FiloSottile/musl-cross/musl-cross - -command rm ./krantor - -CC=x86_64-linux-musl-gcc \ - CXX=x86_64-linux-musl-g++ \ - GOARCH=amd64 \ - GOOS=linux \ - CGO_ENABLED=1 \ - go build -ldflags "-linkmode external -extldflags -static" - -echo "done." -file ./krantor \ No newline at end of file diff --git a/go.mod b/go.mod index 7f82ed0..5194a55 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,7 @@ -module gitlab.com/paulirish/krantor +module krantorbox -go 1.20 +go 1.22 -require ( - github.com/fsnotify/fsnotify v1.7.0 - github.com/putdotio/go-putio v1.7.1 - golang.org/x/oauth2 v0.13.0 -) +require github.com/fsnotify/fsnotify v1.7.0 -require ( - github.com/golang/protobuf v1.5.3 // indirect - golang.org/x/net v0.16.0 // indirect - golang.org/x/sys v0.13.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect -) +require golang.org/x/sys v0.22.0 // indirect diff --git a/go.sum b/go.sum index be7fc66..57e4964 100644 --- a/go.sum +++ b/go.sum @@ -1,57 +1,4 @@ -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/putdotio/go-putio v1.7.1 h1:316PpOMO2a7H73foRxlpHmekeLso07et26Z00YlwQ2A= -github.com/putdotio/go-putio v1.7.1/go.mod h1:QhjpLhn3La/ea4FeJlp1qsiaFZDC0EIO8VUe8VEKMV0= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= \ No newline at end of file diff --git a/main.go b/main.go index 15a8c97..792edd3 100644 --- a/main.go +++ b/main.go @@ -1,254 +1,225 @@ package main import ( - "context" - "errors" + "bytes" + "encoding/json" "fmt" - "io/ioutil" + "io" "log" + "mime/multipart" + "net/http" "os" - "strconv" + "path/filepath" "strings" "time" "github.com/fsnotify/fsnotify" - "github.com/putdotio/go-putio" - "golang.org/x/oauth2" ) var ( - folderPath string = os.Getenv("PUTIO_WATCH_FOLDER") - putioToken string = os.Getenv("PUTIO_TOKEN") - downloadFolderID string = os.Getenv("PUTIO_DOWNLOAD_FOLDER_ID") + folderPath = os.Getenv("TORBOX_WATCH_FOLDER") + torboxAPIKey = os.Getenv("TORBOX_API_KEY") + torboxAPIBase = os.Getenv("TORBOX_API_BASE") + torboxAPIVer = os.Getenv("TORBOX_API_VERSION") + deleteAfterUpload bool ) -func connectToPutio() (*putio.Client, error) { - tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: putioToken}) - oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource) +const ( + maxRetries = 3 + retryDelay = 5 * time.Second +) - client := putio.NewClient(oauthClient) +type TorBoxResponse struct { + Success bool `json:"success"` + Detail string `json:"detail"` +} - return client, nil +func init() { + deleteAfterUpload = strings.ToLower(os.Getenv("DELETE_AFTER_UPLOAD")) == "true" } -func folderIDConvert() (int64, error) { - folderID, err := strconv.ParseInt(downloadFolderID, 10, 32) +func uploadToTorBox(filename string) error { + file, err := os.Open(filename) if err != nil { - str := fmt.Sprintf("strconv err: %v", err) - err := errors.New(str) - return 0, err + return fmt.Errorf("error opening file: %v", err) + } + defer file.Close() + + for attempt := 0; attempt < maxRetries; attempt++ { + var err error + if strings.HasSuffix(filename, ".nzb") { + err = tryUploadUsenet(file, filename) + } else { + err = tryUploadTorrent(file, filename) + } + + if err == nil { + log.Printf("Successfully uploaded %s to TorBox", filename) + if deleteAfterUpload { + if err := os.Remove(filename); err != nil { + log.Printf("Warning: Failed to delete file %s: %v", filename, err) + } else { + log.Printf("Deleted file: %s", filename) + } + } + return nil + } + + log.Printf("Attempt %d failed: %v. Retrying in %v...", attempt+1, err, retryDelay) + time.Sleep(retryDelay) + + // Rewind the file for the next attempt + _, err = file.Seek(0, 0) + if err != nil { + return fmt.Errorf("error rewinding file: %v", err) + } } - return folderID, nil + + return fmt.Errorf("failed to upload after %d attempts", maxRetries) } -func uploadTorrentToPutio(filename string, client *putio.Client) error { - // putio client's default timeout is 30sec. We'll allow a tad more. - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*32)) - defer cancel() +func tryUploadTorrent(file *os.File, filename string) error { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) - // Convert FolderID from string to int to use with Files.Upload - folderID, err := folderIDConvert() + part, err := writer.CreateFormFile("file", filepath.Base(filename)) if err != nil { - return err + return fmt.Errorf("error creating form file: %v", err) } - - // Using open since Upload need an *os.File variable - file, err := os.Open(filename) + _, err = io.Copy(part, file) if err != nil { - str := fmt.Sprintf("Openfile err: %v", err) - err := errors.New(str) - return err + return fmt.Errorf("error copying file to form: %v", err) } - // Uploading file to Putio - log.Println("Read torrent file. Uploading...") - result, err := client.Files.Upload(ctx, file, filename, folderID) + writer.WriteField("seed", "1") + writer.WriteField("allow_zip", "true") + + err = writer.Close() if err != nil { - str := fmt.Sprintf("Upload to Putio err: %v", err) - err := errors.New(str) - return err + return fmt.Errorf("error closing multipart writer: %v", err) } - fmt.Printf("Transferred to putio: %v at %v\n-------------------\n", filename, result.Transfer.CreatedAt) - return nil + url := fmt.Sprintf("%s/%s/api/torrents/createtorrent", torboxAPIBase, torboxAPIVer) + return sendRequest(url, writer.FormDataContentType(), body) } -func transferMagnetToPutio(filename string, client *putio.Client) error { - // putio client's default timeout is 30sec. We'll allow a tad more. - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*32)) - defer cancel() +func tryUploadUsenet(file *os.File, filename string) error { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) - // Convert FolderID from string to int to use with Files.Upload - folderID, err := folderIDConvert() + part, err := writer.CreateFormFile("file", filepath.Base(filename)) + if err != nil { + return fmt.Errorf("error creating form file: %v", err) + } + _, err = io.Copy(part, file) if err != nil { - return err + return fmt.Errorf("error copying file to form: %v", err) } - // Reading the link inside the magnet file to give to Putio - log.Println("Reading...") - magnetData, err := ioutil.ReadFile(filename) + // Use the filename without the path and .nzb extension + name := strings.TrimSuffix(filepath.Base(filename), ".nzb") + err = writer.WriteField("name", name) if err != nil { - str := fmt.Sprintf("Couldn't read file %v: %v", filename, err) - err := errors.New(str) - return err + return fmt.Errorf("error writing name field: %v", err) } - log.Println("magnetData: ", string(magnetData)) - // Using Transfer to DL file via magnet file - result, err := client.Transfers.Add(ctx, string(magnetData), folderID, "") + err = writer.Close() if err != nil { - str := fmt.Sprintf("Transfer to putio err: %v", err) - err := errors.New(str) - return err + return fmt.Errorf("error closing multipart writer: %v", err) } - fmt.Printf("Transferred to putio: %v at %v\n-------------------\n", filename, result.CreatedAt) - // TODO: should we delete (or move) the file after successful uploading? - // To prevent accidental reuploads if someone moves files around? - return nil + url := fmt.Sprintf("%s/%s/api/usenet/createusenetdownload", torboxAPIBase, torboxAPIVer) + return sendRequest(url, writer.FormDataContentType(), body) } -// https://stackoverflow.com/questions/67069723/keep-retrying-a-function-in-golang -func retryIfNeeded(attempts int, sleep time.Duration, f func() error) (err error) { - for i := 0; i < attempts; i++ { - if i > 0 { - log.Println("retrying after error:", err) - time.Sleep(sleep) - sleep *= 2 - } - err = f() - if err == nil { - return nil - } else if !strings.Contains(err.Error(), "context deadline") { - // Don't retry if its not a context deadline error - return err - } +func sendRequest(url, contentType string, body *bytes.Buffer) error { + req, err := http.NewRequest("POST", url, body) + if err != nil { + return fmt.Errorf("error creating request: %v", err) } - return fmt.Errorf("retries failed. After %d attempts, last error: %s", attempts, err) -} -func checkFileType(filename string) (string, error) { - // Checking what's at the end of the string - isMagnet := strings.HasSuffix(filename, ".magnet") - isTorrent := strings.HasSuffix(filename, ".torrent") + req.Header.Set("Content-Type", contentType) + req.Header.Set("Authorization", "Bearer "+torboxAPIKey) - if isMagnet { - return "magnet", nil - } else if isTorrent { - return "torrent", nil - } else { - str := fmt.Sprintf("File isn't a torrent or magnet file: %v", filename) - err := errors.New(str) - return "", err + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error sending request: %v", err) } -} - -func prepareFile(event fsnotify.Event, client *putio.Client) { - time.Sleep(100 * time.Millisecond) // wait for WRITE event(s) to finish - - var err error - var fileType string + defer resp.Body.Close() - filename := event.Name + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } - // Checking if the file is a torrent of a magnet file - torrentOrMagnet, err := checkFileType(filename) + var torboxResp TorBoxResponse + err = json.NewDecoder(resp.Body).Decode(&torboxResp) if err != nil { - log.Println(err) - } else { - fileType = torrentOrMagnet + return fmt.Errorf("error decoding response: %v", err) } - fmt.Printf("Detected new file in watch folder: %v\n", filename) - // Retry the upload up to 3 times, in case of "context deadline exceeded" aka Timeout on the http POST - sleepBetweenRetry := time.Duration(60) * time.Second - - if fileType == "torrent" { - err = retryIfNeeded(3, sleepBetweenRetry, func() (err error) { - return uploadTorrentToPutio(filename, client) - }) - if err != nil { - log.Println("ERROR: ", err) - } - } else if fileType == "magnet" { - err = retryIfNeeded(3, sleepBetweenRetry, func() (err error) { - return transferMagnetToPutio(filename, client) - }) - if err != nil { - log.Println("ERROR: ", err) - } + if !torboxResp.Success { + return fmt.Errorf("TorBox API error: %s", torboxResp.Detail) } + + return nil } -func watchFolder(client *putio.Client) { - // https://pkg.go.dev/github.com/fsnotify/fsnotify - w, err := fsnotify.NewWatcher() +func watchFolder() { + watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } + defer watcher.Close() + done := make(chan bool) go func() { for { select { - case event, ok := <-w.Events: + case event, ok := <-watcher.Events: if !ok { return } - log.Println("event:", event) // verbose logging of fsnotify events… - if event.Has(fsnotify.Create) { // However CREATE is the only one we take action on - // run in separate thread - go prepareFile(event, client) + if event.Op&fsnotify.Create == fsnotify.Create { + if strings.HasSuffix(event.Name, ".torrent") || strings.HasSuffix(event.Name, ".magnet") || strings.HasSuffix(event.Name, ".nzb") { + log.Println("New file detected:", event.Name) + err := uploadToTorBox(event.Name) + if err != nil { + log.Printf("Error uploading %s: %v", event.Name, err) + } + } } - case err, ok := <-w.Errors: + case err, ok := <-watcher.Errors: if !ok { return } - log.Fatalln(err) + log.Println("Error:", err) } } }() - // Watch this folder for changes. - if err := w.Add(folderPath); err != nil { - log.Fatalln(err) - } - log.Println("Watching", folderPath) - - <-make(chan struct{}) -} - -func checkEnvVariables() error { - var envToSet string - - if folderPath == "" { - envToSet = "PUTIO_WATCH_FOLDER is not set / " - } - if downloadFolderID == "" { - envToSet = envToSet + "PUTIO_DOWNLOAD_FOLDER_ID is not set / " - } - if putioToken == "" { - envToSet = envToSet + "PUTIO_TOKEN is not set / " - } - if envToSet != "" { - return errors.New(envToSet) + err = watcher.Add(folderPath) + if err != nil { + log.Fatal(err) } - return nil + log.Printf("Watching folder: %s", folderPath) + <-done } func main() { - log.Println("Krantor Started") - - client, err := connectToPutio() - if err != nil { - log.Fatalln("connection to Putio err: ", err) + log.Println("Krantorbox Auto-Upload Started") + log.Printf("Watching folder: %s", folderPath) + log.Printf("TorBox API Base: %s", torboxAPIBase) + log.Printf("TorBox API Version: %s", torboxAPIVer) + if deleteAfterUpload { + log.Println("File deletion after upload is enabled") + } else { + log.Println("File deletion after upload is disabled") } - // We check that the env variable are set to avoid issues - err = checkEnvVariables() - if err != nil { - log.Fatal(err) + if folderPath == "" || torboxAPIKey == "" || torboxAPIBase == "" || torboxAPIVer == "" { + log.Fatal("Please set all required environment variables") } - // We start watching the folders - watchFolder(client) -} + watchFolder() +} \ No newline at end of file