initial commit
This commit is contained in:
commit
347b85eb17
|
@ -0,0 +1 @@
|
||||||
|
/.idea
|
|
@ -0,0 +1,11 @@
|
||||||
|
FROM golang:1.21 as build
|
||||||
|
|
||||||
|
WORKDIR /go/src/app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go mod download
|
||||||
|
RUN CGO_ENABLED=0 go build -o /go/bin/app
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static-debian12
|
||||||
|
COPY --from=build /go/bin/app /
|
||||||
|
ENTRYPOINT ["/app"]
|
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/schollz/progressbar/v3"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func downloadFile(url, path string) error {
|
||||||
|
output, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bar := progressbar.DefaultBytes(
|
||||||
|
response.ContentLength,
|
||||||
|
"downloading",
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err = io.Copy(io.MultiWriter(output, bar), response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := output.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := response.Body.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
module git.lumen.sh/shyim/realdebrid-torrent
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
|
github.com/schollz/progressbar/v3 v3.14.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
|
github.com/stretchr/testify v1.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.14.0 // indirect
|
||||||
|
golang.org/x/term v0.14.0 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,27 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI=
|
||||||
|
github.com/schollz/progressbar/v3 v3.14.1/go.mod h1:Zc9xXneTzWXF81TGoqL71u0sBPjULtEHYtj/WVgVy8E=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||||
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
||||||
|
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,164 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"git.lumen.sh/shyim/realdebrid-torrent/realdebrid"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var client realdebrid.Client
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
client = realdebrid.Client{
|
||||||
|
Token: os.Getenv("R_TOKEN"),
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
panic(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
go watchFiles(watcher)
|
||||||
|
|
||||||
|
if len(flag.Args()) == 0 {
|
||||||
|
log.Fatal("no files specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range flag.Args() {
|
||||||
|
err = watcher.Add(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("watching", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-make(chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchFiles(watcher *fsnotify.Watcher) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Has(fsnotify.Create) {
|
||||||
|
if filepath.Ext(event.Name) == ".torrent" {
|
||||||
|
go handleTorrent(event.Name)
|
||||||
|
} else if filepath.Ext(event.Name) == ".magnet" {
|
||||||
|
go handleMagnet(event.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTorrent(name string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
file, err := os.ReadFile(name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := client.AddTorrent(ctx, file)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error adding torrent: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("added torrent %s", id)
|
||||||
|
|
||||||
|
handleTorrentDownload(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMagnet(name string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
file, err := os.ReadFile(name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := client.AddMagnet(ctx, string(file))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error adding magnet: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("added magnet %s", id)
|
||||||
|
|
||||||
|
handleTorrentDownload(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTorrentDownload(ctx context.Context, id string) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if err := client.SelectFiles(ctx, id); err != nil {
|
||||||
|
log.Printf("error selecting files: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("selected files for torrent %s", id)
|
||||||
|
|
||||||
|
var status *realdebrid.TorrentStatus
|
||||||
|
|
||||||
|
for {
|
||||||
|
status, err = client.StatusTorrent(ctx, id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting status: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.IsDone() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("still downloading torrent %s, progress %d", status.FileName, status.Progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("finished downloading torrent %s", id)
|
||||||
|
|
||||||
|
downloadLink, err := client.UnrestrictLink(ctx, status.Links[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting download link: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("got download link %s", downloadLink)
|
||||||
|
|
||||||
|
if err := downloadFile(downloadLink, status.FileName); err != nil {
|
||||||
|
log.Printf("error downloading file: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("downloaded file %s", status.FileName)
|
||||||
|
|
||||||
|
if err := client.DeleteTorrent(ctx, id); err != nil {
|
||||||
|
log.Printf("error deleting torrent: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package realdebrid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type createResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) AddTorrent(ctx context.Context, torrent []byte) (string, error) {
|
||||||
|
req, err := c.createRequest(ctx, "/torrents/addTorrent", http.MethodPut, bytes.NewReader(torrent))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-bittorrent")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
return "", fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response createResponse
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) AddMagnet(ctx context.Context, torrent string) (string, error) {
|
||||||
|
req, err := c.createPostRequest(ctx, "/torrents/addMagnet", map[string]string{"magnet": torrent})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
return "", fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response createResponse
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) DeleteTorrent(ctx context.Context, id string) error {
|
||||||
|
req, err := c.createRequest(ctx, "/torrents/delete/"+id, http.MethodDelete, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
|
return fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) SelectFiles(ctx context.Context, id string) error {
|
||||||
|
req, err := c.createPostRequest(ctx, "/torrents/selectFiles/"+id, map[string]string{"files": "all"})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) StatusTorrent(ctx context.Context, id string) (*TorrentStatus, error) {
|
||||||
|
|
||||||
|
req, err := c.createRequest(ctx, "/torrents/info/"+id, http.MethodGet, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var status TorrentStatus
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) UnrestrictLink(ctx context.Context, link string) (string, error) {
|
||||||
|
req, err := c.createPostRequest(ctx, "/unrestrict/link", map[string]string{"link": link})
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Download string `json:"download"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Download, nil
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package realdebrid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Client) createRequest(ctx context.Context, path, method string, body io.Reader) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, "https://api.real-debrid.com/rest/1.0"+path, body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) createPostRequest(ctx context.Context, path string, data map[string]string) (*http.Request, error) {
|
||||||
|
values := url.Values{}
|
||||||
|
|
||||||
|
for key, value := range data {
|
||||||
|
values.Add(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.createRequest(ctx, path, http.MethodPost, strings.NewReader(values.Encode()))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package realdebrid
|
||||||
|
|
||||||
|
type TorrentStatus struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
FileName string `json:"filename"`
|
||||||
|
OriginalFileName string `json:"original_filename"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
Links []string `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (status TorrentStatus) IsDone() bool {
|
||||||
|
return status.Status == "downloaded"
|
||||||
|
}
|
Loading…
Reference in New Issue