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