From 6470a6882be0751ba0e7a9fed37478ad4884e806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Doma=C5=84ski?= Date: Wed, 17 Sep 2025 20:25:56 +0200 Subject: [PATCH] initial push --- .gitignore | 25 ++++++++ Dockerfile | 18 ++++++ docker-compose.yaml | 10 ++++ env.template | 3 + go.mod | 3 + main.go | 140 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 199 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker-compose.yaml create mode 100644 env.template create mode 100644 go.mod create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f72f89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c7d85b0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM golang:alpine as builder + +# build app +WORKDIR /opt/torrentchecker/src +COPY . /opt/torrentchecker/src + +RUN go build -o /opt/torrentchecker/torrentchecker main.go +RUN chmod a+x /opt/torrentchecker/torrentchecker + +# target image +FROM golang:alpine +RUN mkdir -p /opt/torrentchecker + +COPY --from=builder /opt/torrentchecker/torrentchecker /opt/torrentchecker/torrentchecker +RUN crontab -l | { cat; echo "0 * * * * /opt/torrentchecker/torrentchecker"; } | crontab - + + +CMD ["crond", "-f", "-l", "2"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..26e3d5f --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + app: + image: registry.domandoman.xyz/utils/torrentchecker + restart: unless-stopped + build: + context: . + dockerfile: Dockerfile + env_file: + - .env + platform: "linux/amd64" diff --git a/env.template b/env.template new file mode 100644 index 0000000..1e492f0 --- /dev/null +++ b/env.template @@ -0,0 +1,3 @@ +TORRENT_BASEPATH="" +TORRENT_USERNAME="" +TORRENT_PASSWORD="" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c42a2e7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.domandoman.xyz/doman/tchecker + +go 1.24.1 diff --git a/main.go b/main.go new file mode 100644 index 0000000..fdcbc46 --- /dev/null +++ b/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/cookiejar" + "net/url" + "os" +) + +type HTTPClient struct { + client *http.Client + basePath string + jar *cookiejar.Jar +} + +func NewHTTPClient(basePath string) *HTTPClient { + jar, err := cookiejar.New(nil) + + if err != nil { + panic(err) + } + + return &HTTPClient{ + client: &http.Client{Jar: jar}, + basePath: basePath, + jar: jar, + } +} + +func (c *HTTPClient) Get(subPath string, query map[string]string) (*http.Response, error) { + combined, err := url.JoinPath(c.basePath, subPath) + + if err != nil { + return nil, err + } + + parsed, err := url.Parse(combined) + + if err != nil { + return nil, err + } + + if query != nil { + q := parsed.Query() + + for k, v := range query { + q.Set(k, v) + } + + parsed.RawQuery = q.Encode() + } + + return c.client.Get(parsed.String()) +} + +func (c *HTTPClient) PostForm(subPath string, data map[string][]string) (*http.Response, error) { + url, err := url.JoinPath(c.basePath, subPath) + + if err != nil { + return nil, err + } + + return c.client.PostForm(url, data) +} + +func (c *HTTPClient) Login(username string, password string) error { + resp, err := c.PostForm("/api/v2/auth/login", map[string][]string{"username": {username}, "password": {password}}) + + if err != nil { + return err + } + + if resp.StatusCode != 200 { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + setCookie := resp.Header.Get("Set-Cookie") + + if setCookie == "" { + return fmt.Errorf("no Set-Cookie in response header") + } + + jarCookie := c.jar.Cookies(resp.Request.URL) + + if len(jarCookie) == 0 { + return fmt.Errorf("no cookie saved to cookiejar") + } + + return nil +} + +func main() { + basePath := os.Getenv("TORRENT_BASEPATH") + username := os.Getenv("TORRENT_USERNAME") + password := os.Getenv("TORRENT_PASSWORD") + + client := NewHTTPClient(basePath) + + err := client.Login(username, password) + + if err != nil { + panic(err) + } + + resp, err := client.Get("/api/v2/torrents/info", map[string]string{"filter": "seeding"}) + + if err != nil { + panic(err) + } + + if resp.StatusCode != 200 { + panic(fmt.Errorf("unexpected status code: %d", resp.StatusCode)) + } + + decoder := json.NewDecoder(resp.Body) + + var torrents []map[string]interface{} + + err = decoder.Decode(&torrents) + + if err != nil { + panic(err) + } + + for _, torrent := range torrents { + resp, err := client.PostForm("/api/v2/torrents/stop", map[string][]string{"hashes": {torrent["hash"].(string)}}) + + if err != nil { + panic(err) + } + + if resp.StatusCode != 200 { + panic(fmt.Errorf("unexpected status code: %d", resp.StatusCode)) + } + } + + fmt.Println("done") +}