Golang bindings for Transmission RPC API
Golang bindings to Transmission (bittorrent) RPC interface.
Even if there is some high level wrappers/helpers, the goal of this lib is to stay close to the original API in terms of methods and payloads while enhancing certain types to be more "golangish": timestamps are converted from/to time.Time, numeric durations in time.Duration, booleans in numeric form are converted to real bool, etc...
Also payload generation aims to be precise: when several values can be added to a payload, only instanciated values will be forwarded (and kept !) to the final payload. This means that the default JSON marshalling (with omitempty) can't always be used and therefor a manual, reflect based, approach is used to build the final payload and accurately send what the user have instanciated, even if a value is at its default type value.
Version v3 of this library is compatible with RPC version 17 (Transmission v4).
Install the v3 with:
go get github.com/hekmon/transmissionrpc/v3
First the main client object must be instantiated with New(). The library takes a parsed URL as input: it allows you to add any options you need to it (scheme, optional authentification, custom port, custom URI/prefix, etc...).
import (
"net/url"
"github.com/hekmon/transmissionrpc/v3"
)
endpoint, err := url.Parse("http://user:[email protected]:9091/transmission/rpc")
if err != nil {
panic(err)
}
tbt, err := transmissionrpc.New(endpoint, nil)
if err != nil {
panic(err)
}
The remote RPC version can be checked against this library before starting to operate:
ok, serverVersion, serverMinimumVersion, err := transmission.RPCVersion()
if err != nil {
panic(err)
}
if !ok {
panic(fmt.Sprintf("Remote transmission RPC version (v%d) is incompatible with the transmission library (v%d): remote needs at least v%d",
serverVersion, transmissionrpc.RPCVersion, serverMinimumVersion))
}
fmt.Printf("Remote transmission RPC version (v%d) is compatible with our transmissionrpc library (v%d)\n",
serverVersion, transmissionrpc.RPCVersion)
Each rpc methods here can work with ID list, hash list or recently-active
magic word. Therefor, there is 3 golang method variants for each of them.
transmissionbt.TorrentXXXXIDs(...)
transmissionbt.TorrentXXXXHashes(...)
transmissionbt.TorrentXXXXRecentlyActive()
Check TorrentStartIDs(), TorrentStartHashes() and TorrentStartRecentlyActive().
Ex:
err := transmissionbt.TorrentStartIDs(context.TODO(), []int64{55})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println("yay")
}
Check TorrentStartNowIDs(), TorrentStartNowHashes() and TorrentStartNowRecentlyActive().
Ex:
err := transmissionbt.TorrentStartNowHashes(context.TODO(), []string{"f07e0b0584745b7bcb35e98097488d34e68623d0"})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println("yay")
}
Check TorrentStopIDs(), TorrentStopHashes() and TorrentStopRecentlyActive().
Ex:
err := transmissionbt.TorrentStopIDs(context.TODO(), []int64{55})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println("yay")
}
Check TorrentVerifyIDs(), TorrentVerifyHashes() and TorrentVerifyRecentlyActive().
Ex:
err := transmissionbt.TorrentVerifyHashes(context.TODO(), []string{"f07e0b0584745b7bcb35e98097488d34e68623d0"})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println("yay")
}
Check TorrentReannounceIDs(), TorrentReannounceHashes() and TorrentReannounceRecentlyActive().
Ex:
err := transmissionbt.TorrentReannounceRecentlyActive(context.TODO())
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println("yay")
}
Mapped as TorrentSet().
Ex: apply a 1 MB/s limit to a torrent.
uploadLimited := true
uploadLimitKBps := int64(1000)
err := transmissionbt.TorrentSet(context.TODO(), transmissionrpc.TorrentSetPayload{
IDs: []int64{55},
UploadLimited: &uploadLimited,
UploadLimit: &uploadLimitKBps,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println("yay")
}
There is a lot more mutators available.
All fields for all torrents with TorrentGetAll():
torrents, err := transmissionbt.TorrentGetAll(context.TODO())
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println(torrents) // meh it's full of pointers
}
All fields for a restricted list of ids with TorrentGetAllFor():
torrents, err := transmissionbt.TorrentGetAllFor(context.TODO(), []int64{31})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println(torrents) // meh it's still full of pointers
}
Some fields for some torrents with the low level accessor TorrentGet():
torrents, err := transmissionbt.TorrentGet(context.TODO(), []string{"status"}, []int64{54, 55})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
for _, torrent := range torrents {
fmt.Println(torrent.Status) // the only instanciated field, as requested
}
}
Some fields for all torrents, still with the low level accessor TorrentGet():
torrents, err := transmissionbt.TorrentGet(context.TODO(), []string{"id", "name", "hashString"}, nil)
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
for _, torrent := range torrents {
fmt.Println(torrent.ID)
fmt.Println(torrent.Name)
fmt.Println(torrent.HashString)
}
}
Valid fields name can be found as JSON tag on the Torrent struct.
Adding a torrent from a file (using TorrentAddFile wrapper):
filepath := "/home/hekmon/Downloads/ubuntu-17.10.1-desktop-amd64.iso.torrent"
torrent, err := transmissionbt.TorrentAddFile(context.TODO(), filepath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
// Only 3 fields will be returned/set in the Torrent struct
fmt.Println(*torrent.ID)
fmt.Println(*torrent.Name)
fmt.Println(*torrent.HashString)
}
Adding a torrent from a file (using TorrentAddFileDownloadDir wrapper) to a specified DownloadDir (this allows for separation of downloads to target folders):
filepath := "/home/hekmon/Downloads/ubuntu-17.10.1-desktop-amd64.iso.torrent"
torrent, err := transmissionbt.TorrentAddFileDownloadDir(context.TODO(), filepath, "/path/to/other/download/dir")
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
// Only 3 fields will be returned/set in the Torrent struct
fmt.Println(*torrent.ID)
fmt.Println(*torrent.Name)
fmt.Println(*torrent.HashString)
}
Adding a torrent from an URL (ex: a magnet) with the real TorrentAdd method:
magnet := "magnet:?xt=urn:btih:f07e0b0584745b7bcb35e98097488d34e68623d0&dn=ubuntu-17.10.1-desktop-amd64.iso&tr=http%3A%2F%2Ftorrent.ubuntu.com%3A6969%2Fannounce&tr=http%3A%2F%2Fipv6.torrent.ubuntu.com%3A6969%2Fannounce"
torrent, err := btserv.TorrentAdd(context.TODO(), transmissionrpc.TorrentAddPayload{
Filename: &magnet,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
// Only 3 fields will be returned/set in the Torrent struct
fmt.Println(*torrent.ID)
fmt.Println(*torrent.Name)
fmt.Println(*torrent.HashString)
}
Which would output:
55
ubuntu-17.10.1-desktop-amd64.iso
f07e0b0584745b7bcb35e98097488d34e68623d0
Adding a torrent from a file, starting it paused:
filepath := "/home/hekmon/Downloads/ubuntu-17.10.1-desktop-amd64.iso.torrent"
b64, err := transmissionrpc.File2Base64(filepath)
if err != nil {
fmt.Fprintf(os.Stderr, "can't encode '%s' content as base64: %v", filepath, err)
} else {
// Prepare and send payload
paused := true
torrent, err := transmissionbt.TorrentAdd(context.TODO(), transmissionrpc.TorrentAddPayload{MetaInfo: &b64, Paused: &paused})
}
Mapped as TorrentRemove().
Mapped as TorrentSetLocation().
Mapped as TorrentRenamePath().
Mapped as SessionArgumentsSet().
Mapped as SessionArgumentsGet().
Mapped as SessionStats().
Mapped as BlocklistUpdate().
Mapped as PortTest().
Ex:
st, err := transmissionbt.PortTest(context.TODO())
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
if st {
fmt.Println("Open!")
}
Mapped as SessionClose().
Mapped as QueueMoveTop().
Mapped as QueueMoveUp().
Mapped as QueueMoveDown().
Mapped as QueueMoveBottom().
Mappped as FreeSpace().
Ex: Get the space available for /data.
freeSpace, totalSpace, err := transmissionbt.FreeSpace(context.TODO(), "/data")
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Printf("Free space: %s | %d | %v\n", freeSpace, freeSpace, freeSpace)
fmt.Printf("Total space: %s | %d | %v\n", totalSpace, totalSpace, totalSpace)
}
}
For more information about the freeSpace type, check the ComputerUnits library.
Mapped as BandwidthGroupSet().
Mapped as BandwidthGroupGet().
If you want to (or need to) inspect the requests made by the lib, you can use a custom round tripper within a custom HTTP client. I personnaly like to use the debuglog package from the starr project. Example below.
package main
import (
"context"
"fmt"
"net/url"
"time"
"github.com/hashicorp/go-cleanhttp"
"github.com/hekmon/transmissionrpc/v3"
"golift.io/starr/debuglog"
)
func main() {
// Parse API endpoint
endpoint, err := url.Parse("http://user:[email protected]:9091/transmission/rpc")
if err != nil {
panic(err)
}
// Create the HTTP client with debugging capabilities
httpClient := cleanhttp.DefaultPooledClient()
httpClient.Transport = debuglog.NewLoggingRoundTripper(debuglog.Config{
Redact: []string{endpoint.User.String()},
}, httpClient.Transport)
// Initialize the transmission API client with the debbuging HTTP client
tbt, err := transmissionrpc.New(endpoint, &transmissionrpc.Config{
CustomClient: httpClient,
})
if err != nil {
panic(err)
}
// do something with tbt now
}