initial commit

This commit is contained in:
Soner Sayakci 2023-11-16 23:03:41 +01:00
commit 347b85eb17
No known key found for this signature in database
9 changed files with 487 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.idea

11
Dockerfile Normal file
View File

@ -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"]

40
download.go Normal file
View File

@ -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
}

16
go.mod Normal file
View File

@ -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
)

27
go.sum Normal file
View File

@ -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=

164
main.go Normal file
View File

@ -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
}
}

174
realdebrid/client.go Normal file
View File

@ -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
}

39
realdebrid/request.go Normal file
View File

@ -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
}

15
realdebrid/struct.go Normal file
View File

@ -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"
}