From 347b85eb1769d4c4292d9dd10c9c9a9613dc379c Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 16 Nov 2023 23:03:41 +0100 Subject: [PATCH] initial commit --- .gitignore | 1 + Dockerfile | 11 +++ download.go | 40 ++++++++++ go.mod | 16 ++++ go.sum | 27 +++++++ main.go | 164 +++++++++++++++++++++++++++++++++++++++ realdebrid/client.go | 174 ++++++++++++++++++++++++++++++++++++++++++ realdebrid/request.go | 39 ++++++++++ realdebrid/struct.go | 15 ++++ 9 files changed, 487 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 download.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 realdebrid/client.go create mode 100644 realdebrid/request.go create mode 100644 realdebrid/struct.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..757fee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c838ea --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/download.go b/download.go new file mode 100644 index 0000000..381d452 --- /dev/null +++ b/download.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dbac2e0 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1011085 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e1e668b --- /dev/null +++ b/main.go @@ -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 + } +} diff --git a/realdebrid/client.go b/realdebrid/client.go new file mode 100644 index 0000000..2fe4e0b --- /dev/null +++ b/realdebrid/client.go @@ -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 +} diff --git a/realdebrid/request.go b/realdebrid/request.go new file mode 100644 index 0000000..8b1fbc1 --- /dev/null +++ b/realdebrid/request.go @@ -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 +} diff --git a/realdebrid/struct.go b/realdebrid/struct.go new file mode 100644 index 0000000..8240c0f --- /dev/null +++ b/realdebrid/struct.go @@ -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" +}