aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Spek <p.spek@tyil.nl>2024-11-29 09:03:52 +0100
committerPatrick Spek <p.spek@tyil.nl>2024-11-29 09:03:52 +0100
commit69a29e20673eba9afcfeb75474f561e2e64b120d (patch)
treef22a4e2239e82449e89510d1416bbfee5cb4c680
downloadtransmission-69a29e20673eba9afcfeb75474f561e2e64b120d.tar.gz
transmission-69a29e20673eba9afcfeb75474f561e2e64b120d.tar.bz2
Initial commitv0.1.0
-rw-r--r--CHANGELOG.md10
-rw-r--r--README.md4
-rw-r--r--api.go26
-rw-r--r--api_session_stats.go25
-rw-r--r--api_torrent_get.go36
-rw-r--r--client.go112
-rw-r--r--go.mod3
7 files changed, 216 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..bf3932b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.1.0] - 2024-11-29
+
+Initial release
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9382964
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# transmission
+
+A Go-native library for communicating with the [Transmission RPC
+interface](https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md).
diff --git a/api.go b/api.go
new file mode 100644
index 0000000..883eb5f
--- /dev/null
+++ b/api.go
@@ -0,0 +1,26 @@
+package transmission
+
+var TorrentStatus = map[int]string{
+ 0: "stopped",
+ 1: "verify-pending",
+ 2: "verifying",
+ 3: "download-pending",
+ 4: "downloading",
+ 5: "seed-pending",
+ 6: "seeding",
+}
+
+type Torrent struct {
+ Status int `json:"status"`
+ Id int `json:"id"`
+ Name string `json:"name"`
+ HashString string `json:"hashString"`
+}
+
+type StatsObject struct {
+ UploadedBytes uint `json:"uploadedBytes"`
+ DownloadedBytes uint `json:"downloadedBytes"`
+ FilesAdded uint `json:"filesAdded"`
+ SessionCount uint `json:"sessionCount"`
+ SecondsActive uint `json:"secondsActive"`
+}
diff --git a/api_session_stats.go b/api_session_stats.go
new file mode 100644
index 0000000..9e05dd7
--- /dev/null
+++ b/api_session_stats.go
@@ -0,0 +1,25 @@
+package transmission
+
+type CallSessionStatsResponse struct {
+ Result string `json:"result"`
+ Arguments CallSessionStatsResponseArguments `json:"arguments"`
+ Tag uint `json:"tag"`
+}
+
+type CallSessionStatsResponseArguments struct {
+ ActiveTorrentCount uint `json:"activeTorrentCount"`
+ DownloadSpeed uint `json:"downloadSpeed"`
+ PausedTorrentCount uint `json:"pausedTorrentCount"`
+ TorrentCount uint `json:"torrentCount"`
+ UploadSpeed uint `json:"uploadSpeed"`
+ CumulativeStats StatsObject `json:"cumulative-stats"`
+ CurrentStats StatsObject `json:"current-stats"`
+}
+
+func (this *Client) CallSessionStats() (CallSessionStatsResponse, error) {
+ var response CallSessionStatsResponse
+
+ err := this.Call("session-stats", nil, this.makeTag(), &response)
+
+ return response, err
+}
diff --git a/api_torrent_get.go b/api_torrent_get.go
new file mode 100644
index 0000000..7c4859a
--- /dev/null
+++ b/api_torrent_get.go
@@ -0,0 +1,36 @@
+package transmission
+
+type CallTorrentGetArguments struct {
+ Ids []uint `json:"ids"`
+ Fields []string `json:"fields"`
+}
+
+type CallTorrentGetAllArguments struct {
+ Fields []string `json:"fields"`
+}
+
+type CallTorrentGetResponse struct {
+ Result string `json:"result"`
+ Arguments CallTorrentGetResponseArguments `json:"arguments"`
+ Tag uint `json:"tag"`
+}
+
+type CallTorrentGetResponseArguments struct {
+ Torrents []Torrent `json:"torrents"`
+}
+
+func (this *Client) CallTorrentGet(arguments CallTorrentGetArguments) (CallTorrentGetResponse, error) {
+ var response CallTorrentGetResponse
+
+ err := this.Call("torrent-get", arguments, this.makeTag(), &response)
+
+ return response, err
+}
+
+func (this *Client) CallTorrentGetAll(arguments CallTorrentGetAllArguments) (CallTorrentGetResponse, error) {
+ var response CallTorrentGetResponse
+
+ err := this.Call("torrent-get", arguments, this.makeTag(), &response)
+
+ return response, err
+}
diff --git a/client.go b/client.go
new file mode 100644
index 0000000..d8303ab
--- /dev/null
+++ b/client.go
@@ -0,0 +1,112 @@
+// The transmission package provides a Go-native interface for communicating
+// with the Transmission RPC interface.
+package transmission
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "log/slog"
+ "math/rand"
+ "net/http"
+ "strconv"
+)
+
+type CallArguments interface {
+ CallTorrentGetArguments | interface{}
+}
+
+// The base struct for a Transmission client to perform RPC calls with.
+type Client struct {
+ Hostname string
+ Port int
+ Username string
+ Password string
+
+ httpClient http.Client
+ sessionId string
+}
+
+// Call a given method on the Transmission RPC interface, and pass it the given arguments in JSON.
+func (this *Client) Call(method string, arguments CallArguments, tag uint, response interface{}) error {
+ // Create HTTP client to work with
+ httpClient := http.Client{}
+
+ // Create the Request object
+ req, _ := this.makeRequest(method, arguments, tag)
+
+ slog.Debug("Sending request", "address", req.URL.String(), "session-id", this.sessionId, "tag", tag)
+ res, err := httpClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ // Read the response
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ slog.Debug("Got response", "status", res.StatusCode, "body", body, "tag", tag)
+
+ // Check if we're missing the session-id
+ if res.StatusCode == 409 {
+ slog.Debug("Got new session-id", "tag", tag)
+ this.sessionId = res.Header["X-Transmission-Session-Id"][0]
+ return this.Call(method, arguments, tag, response)
+ }
+
+ // Unmarshal it into the response
+ json.Unmarshal(body, &response)
+
+ // Return the response
+ return nil
+}
+
+// Convenience function to generate the URL to post RPC calls to.
+func (this *Client) connectionString() string {
+ url := "http://" + this.Hostname
+
+ if this.Port != 0 {
+ url += ":" + strconv.Itoa(this.Port)
+ } else {
+ url += ":9091"
+ }
+
+ url += "/transmission/rpc"
+
+ return url
+}
+
+// makeRequest provides a convenience function to make a new http.Request
+// object. It sets the URL, headers, and body of the request.
+func (this *Client) makeRequest(method string, arguments CallArguments, tag uint) (*http.Request, error) {
+ body, _ := json.Marshal(map[string]interface{}{
+ "method": method,
+ "arguments": arguments,
+ "tag": tag,
+ })
+
+ slog.Debug("Making request", "body", body, "tag", tag)
+
+ req, err := http.NewRequest("POST", this.connectionString(), bytes.NewBuffer([]byte(body)))
+ if err != nil {
+ return nil, err
+ }
+
+ if this.Username != "" {
+ slog.Debug("Adding Authorization header", "tag", tag)
+ req.SetBasicAuth(this.Username, this.Password)
+ }
+
+ req.Header.Add("X-Transmission-Session-Id", this.sessionId)
+
+ return req, nil
+}
+
+// Generate a tag ID. Since the maximum value is not documented, this allows
+// for easy bug-fixing later in life.
+func (this *Client) makeTag() uint {
+ return uint(rand.Uint32())
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c3ddf92
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module git.tyil.nl/go/transmission.git
+
+go 1.23.2