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.
- If you want a 100% compatible lib with rpc v15, please use the v1 releases.
- If you want a 100% compatible lib with rpc v16, please use the v2 releases.
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:password@127.0.0.1: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()
- torrent-start
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")
}
- torrent-start-now
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")
}
- torrent-stop
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")
}
- torrent-verify
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")
}
- torrent-reannounce
Check TorrentReannounceIDs(), TorrentReannounceHashes() and TorrentReannounceRecentlyActive().
Ex:
err := transmissionbt.TorrentReannounceRecentlyActive(context.TODO())
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
fmt.Println("yay")
}
- torrent-set
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.
- torrent-get
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.
- torrent-add
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})
}
- torrent-remove
Mapped as TorrentRemove().
- torrent-set-location
Mapped as TorrentSetLocation().
- torrent-rename-path
Mapped as TorrentRenamePath().
- session-set
Mapped as SessionArgumentsSet().
- session-get
Mapped as SessionArgumentsGet().
- session-stats
Mapped as SessionStats().
- blocklist-update
Mapped as BlocklistUpdate().
- port-test
Mapped as PortTest().
Ex:
st, err := transmissionbt.PortTest(context.TODO())
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
if st {
fmt.Println("Open!")
}
- session-close
Mapped as SessionClose().
- queue-move-top
Mapped as QueueMoveTop().
- queue-move-up
Mapped as QueueMoveUp().
- queue-move-down
Mapped as QueueMoveDown().
- queue-move-bottom
Mapped as QueueMoveBottom().
- free-space
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.
- group-set
Mapped as BandwidthGroupSet().
- group-get
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:password@127.0.0.1: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
}