diff options
| author | Patrick Spek <p.spek@tyil.nl> | 2024-11-29 09:03:52 +0100 |
|---|---|---|
| committer | Patrick Spek <p.spek@tyil.nl> | 2024-11-29 09:03:52 +0100 |
| commit | 69a29e20673eba9afcfeb75474f561e2e64b120d (patch) | |
| tree | f22a4e2239e82449e89510d1416bbfee5cb4c680 | |
| download | transmission-69a29e20673eba9afcfeb75474f561e2e64b120d.tar.gz transmission-69a29e20673eba9afcfeb75474f561e2e64b120d.tar.bz2 | |
Initial commitv0.1.0
| -rw-r--r-- | CHANGELOG.md | 10 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | api.go | 26 | ||||
| -rw-r--r-- | api_session_stats.go | 25 | ||||
| -rw-r--r-- | api_torrent_get.go | 36 | ||||
| -rw-r--r-- | client.go | 112 | ||||
| -rw-r--r-- | go.mod | 3 |
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). @@ -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()) +} @@ -0,0 +1,3 @@ +module git.tyil.nl/go/transmission.git + +go 1.23.2 |
