Skip to content

hekmon/transmissionrpc

Repository files navigation

TransmissionRPC

Go Reference Go report card

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).

Getting started

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)

Features

Torrent Requests

Torrent Action Requests

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 Mutators

  • 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 Accessors

  • 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.

Adding a Torrent

  • 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})
}

Removing a Torrent

  • torrent-remove

Mapped as TorrentRemove().

Moving a Torrent

  • torrent-set-location

Mapped as TorrentSetLocation().

Renaming a Torrent path

  • torrent-rename-path

Mapped as TorrentRenamePath().

Session Requests

Session Arguments

  • session-set

Mapped as SessionArgumentsSet().

  • session-get

Mapped as SessionArgumentsGet().

Session Statistics

  • session-stats

Mapped as SessionStats().

Blocklist

  • blocklist-update

Mapped as BlocklistUpdate().

Port Checking

  • 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 Shutdown

  • session-close

Mapped as SessionClose().

Queue Movement Requests

  • 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

  • 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.

Bandwidth Groups

  • group-set

Mapped as BandwidthGroupSet().

  • group-get

Mapped as BandwidthGroupGet().

Debugging

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
}