1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00
wtf/vendor/github.com/hekmon/transmissionrpc/torrent_accessors.go
2019-07-15 09:06:49 -07:00

610 lines
22 KiB
Go

package transmissionrpc
/*
Torrent Accessors
https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L142
*/
import (
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/hekmon/cunits"
)
var validTorrentFields []string
func init() {
torrentType := reflect.TypeOf(Torrent{})
for i := 0; i < torrentType.NumField(); i++ {
validTorrentFields = append(validTorrentFields, torrentType.Field(i).Tag.Get("json"))
}
}
// TorrentGetAll returns all the known fields for all the torrents.
func (c *Client) TorrentGetAll() (torrents []*Torrent, err error) {
// Send already validated fields to the low level fx
return c.torrentGet(validTorrentFields, nil)
}
// TorrentGetAllFor returns all known fields for the given torrent's ids.
func (c *Client) TorrentGetAllFor(ids []int64) (torrents []*Torrent, err error) {
return c.torrentGet(validTorrentFields, ids)
}
// TorrentGetAllForHashes returns all known fields for the given torrent's ids by string (usually hash).
func (c *Client) TorrentGetAllForHashes(hashes []string) (torrents []*Torrent, err error) {
return c.torrentGetHash(validTorrentFields, hashes)
}
// TorrentGet returns the given of fields (mandatory) for each ids (optionnal).
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L144
func (c *Client) TorrentGet(fields []string, ids []int64) (torrents []*Torrent, err error) {
if err = c.validateFields(fields); err != nil {
return
}
return c.torrentGet(fields, ids)
}
// TorrentGetHashes returns the given of fields (mandatory) for each ids (optionnal).
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L144
func (c *Client) TorrentGetHashes(fields []string, hashes []string) (torrents []*Torrent, err error) {
if err = c.validateFields(fields); err != nil {
return
}
return c.torrentGetHash(fields, hashes)
}
func (c *Client) validateFields(fields []string) (err error) {
// Validate fields
var fieldInvalid bool
var knownField string
for _, inputField := range fields {
fieldInvalid = true
for _, knownField = range validTorrentFields {
if inputField == knownField {
fieldInvalid = false
break
}
}
if fieldInvalid {
err = fmt.Errorf("field '%s' is invalid", inputField)
return
}
}
return
}
func (c *Client) torrentGet(fields []string, ids []int64) (torrents []*Torrent, err error) {
var result torrentGetResults
if err = c.rpcCall("torrent-get", &torrentGetParams{
Fields: fields,
IDs: ids,
}, &result); err != nil {
err = fmt.Errorf("'torrent-get' rpc method failed: %v", err)
return
}
torrents = result.Torrents
return
}
func (c *Client) torrentGetHash(fields []string, hashes []string) (torrents []*Torrent, err error) {
var result torrentGetResults
if err = c.rpcCall("torrent-get", &torrentGetHashParams{
Fields: fields,
Hashes: hashes,
}, &result); err != nil {
err = fmt.Errorf("'torrent-get' rpc method failed: %v", err)
return
}
torrents = result.Torrents
return
}
type torrentGetParams struct {
Fields []string `json:"fields"`
IDs []int64 `json:"ids,omitempty"`
}
type torrentGetHashParams struct {
Fields []string `json:"fields"`
Hashes []string `json:"ids,omitempty"`
}
type torrentGetResults struct {
Torrents []*Torrent `json:"torrents"`
}
// Torrent represents all the possible fields of data for a torrent.
// All fields are pointers to detect if the value is nil (field not requested) or default real default value.
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L163
type Torrent struct {
ActivityDate *time.Time `json:"activityDate"`
AddedDate *time.Time `json:"addedDate"`
BandwidthPriority *int64 `json:"bandwidthPriority"`
Comment *string `json:"comment"`
CorruptEver *int64 `json:"corruptEver"`
Creator *string `json:"creator"`
DateCreated *time.Time `json:"dateCreated"`
DesiredAvailable *int64 `json:"desiredAvailable"`
DoneDate *time.Time `json:"doneDate"`
DownloadDir *string `json:"downloadDir"`
DownloadedEver *int64 `json:"downloadedEver"`
DownloadLimit *int64 `json:"downloadLimit"`
DownloadLimited *bool `json:"downloadLimited"`
Error *int64 `json:"error"`
ErrorString *string `json:"errorString"`
Eta *int64 `json:"eta"`
EtaIdle *int64 `json:"etaIdle"`
Files []*TorrentFile `json:"files"`
FileStats []*TorrentFileStat `json:"fileStats"`
HashString *string `json:"hashString"`
HaveUnchecked *int64 `json:"haveUnchecked"`
HaveValid *int64 `json:"haveValid"`
HonorsSessionLimits *bool `json:"honorsSessionLimits"`
ID *int64 `json:"id"`
IsFinished *bool `json:"isFinished"`
IsPrivate *bool `json:"isPrivate"`
IsStalled *bool `json:"isStalled"`
LeftUntilDone *int64 `json:"leftUntilDone"`
MagnetLink *string `json:"magnetLink"`
ManualAnnounceTime *int64 `json:"manualAnnounceTime"`
MaxConnectedPeers *int64 `json:"maxConnectedPeers"`
MetadataPercentComplete *float64 `json:"metadataPercentComplete"`
Name *string `json:"name"`
PeerLimit *int64 `json:"peer-limit"`
Peers []*Peer `json:"peers"`
PeersConnected *int64 `json:"peersConnected"`
PeersFrom *TorrentPeersFrom `json:"peersFrom"`
PeersGettingFromUs *int64 `json:"peersGettingFromUs"`
PeersSendingToUs *int64 `json:"peersSendingToUs"`
PercentDone *float64 `json:"percentDone"`
Pieces *string `json:"pieces"` // https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L279
PieceCount *int64 `json:"pieceCount"`
PieceSize *cunits.Bits `json:"pieceSize"`
Priorities []int64 `json:"priorities"` // https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L285
QueuePosition *int64 `json:"queuePosition"`
RateDownload *int64 `json:"rateDownload"` // B/s
RateUpload *int64 `json:"rateUpload"` // B/s
RecheckProgress *float64 `json:"recheckProgress"`
SecondsDownloading *int64 `json:"secondsDownloading"`
SecondsSeeding *time.Duration `json:"secondsSeeding"`
SeedIdleLimit *int64 `json:"seedIdleLimit"`
SeedIdleMode *int64 `json:"seedIdleMode"`
SeedRatioLimit *float64 `json:"seedRatioLimit"`
SeedRatioMode *SeedRatioMode `json:"seedRatioMode"`
SizeWhenDone *cunits.Bits `json:"sizeWhenDone"`
StartDate *time.Time `json:"startDate"`
Status *TorrentStatus `json:"status"`
Trackers []*Tracker `json:"trackers"`
TrackerStats []*TrackerStats `json:"trackerStats"`
TotalSize *cunits.Bits `json:"totalSize"`
TorrentFile *string `json:"torrentFile"`
UploadedEver *int64 `json:"uploadedEver"`
UploadLimit *int64 `json:"uploadLimit"`
UploadLimited *bool `json:"uploadLimited"`
UploadRatio *float64 `json:"uploadRatio"`
Wanted []bool `json:"wanted"` //https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L325
WebSeeds []string `json:"webseeds"` // https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L329
WebSeedsSendingToUs *int64 `json:"webseedsSendingToUs"`
}
// ConvertDownloadSpeed will return the download speed as cunits.Bitss/second
func (t *Torrent) ConvertDownloadSpeed() (speed cunits.Bits) {
if t.RateDownload != nil {
speed = cunits.ImportInByte(float64(*t.RateDownload))
}
return
}
// ConvertUploadSpeed will return the upload speed as cunits.Bitss/second
func (t *Torrent) ConvertUploadSpeed() (speed cunits.Bits) {
if t.RateUpload != nil {
speed = cunits.ImportInByte(float64(*t.RateUpload))
}
return
}
// UnmarshalJSON allows to convert timestamps to golang time.Time values.
func (t *Torrent) UnmarshalJSON(data []byte) (err error) {
// Shadow real type for regular unmarshalling
type RawTorrent Torrent
tmp := &struct {
ActivityDate *int64 `json:"activityDate"`
AddedDate *int64 `json:"addedDate"`
DateCreated *int64 `json:"dateCreated"`
DoneDate *int64 `json:"doneDate"`
PieceSize *int64 `json:"pieceSize"`
SecondsSeeding *int64 `json:"secondsSeeding"`
SizeWhenDone *int64 `json:"sizeWhenDone"`
StartDate *int64 `json:"startDate"`
TotalSize *int64 `json:"totalSize"`
Wanted []int64 `json:"wanted"` // boolean in number form
*RawTorrent
}{
RawTorrent: (*RawTorrent)(t),
}
// Unmarshall (with timestamps as number)
if err = json.Unmarshal(data, &tmp); err != nil {
return
}
// Create the real time & duration from timsteamps and seconds
if tmp.ActivityDate != nil {
ad := time.Unix(*tmp.ActivityDate, 0)
t.ActivityDate = &ad
}
if tmp.AddedDate != nil {
ad := time.Unix(*tmp.AddedDate, 0)
t.AddedDate = &ad
}
if tmp.DateCreated != nil {
dc := time.Unix(*tmp.DateCreated, 0)
t.DateCreated = &dc
}
if tmp.DoneDate != nil {
dd := time.Unix(*tmp.DoneDate, 0)
t.DoneDate = &dd
}
if tmp.PieceSize != nil {
ps := cunits.ImportInByte(float64(*tmp.PieceSize))
t.PieceSize = &ps
}
if tmp.SecondsSeeding != nil {
dur := time.Duration(*tmp.SecondsSeeding) * time.Second
t.SecondsSeeding = &dur
}
if tmp.SizeWhenDone != nil {
swd := cunits.ImportInByte(float64(*tmp.SizeWhenDone))
t.SizeWhenDone = &swd
}
if tmp.StartDate != nil {
st := time.Unix(*tmp.StartDate, 0)
t.StartDate = &st
}
if tmp.TotalSize != nil {
ts := cunits.ImportInByte(float64(*tmp.TotalSize))
t.TotalSize = &ts
}
// Boolean slice in decimal form
if tmp.Wanted != nil {
t.Wanted = make([]bool, len(tmp.Wanted))
for index, value := range tmp.Wanted {
if value == 1 {
t.Wanted[index] = true
} else if value != 0 {
return fmt.Errorf("Can't convert Wanted index %d value '%d' as boolean", index, value)
}
}
}
return
}
// MarshalJSON allows to convert back golang values to original payload values.
func (t *Torrent) MarshalJSON() (data []byte, err error) {
// Shadow real type for regular unmarshalling
type RawTorrent Torrent
tmp := &struct {
ActivityDate *int64 `json:"activityDate"`
AddedDate *int64 `json:"addedDate"`
DateCreated *int64 `json:"dateCreated"`
DoneDate *int64 `json:"doneDate"`
SecondsSeeding *int64 `json:"secondsSeeding"`
StartDate *int64 `json:"startDate"`
Wanted []int64 `json:"wanted"` // boolean in number form
*RawTorrent
}{
RawTorrent: (*RawTorrent)(t),
}
// Timestamps & Duration
if t.ActivityDate != nil {
ad := t.ActivityDate.Unix()
tmp.ActivityDate = &ad
}
if t.AddedDate != nil {
ad := t.AddedDate.Unix()
tmp.AddedDate = &ad
}
if t.DateCreated != nil {
dc := t.DateCreated.Unix()
tmp.DateCreated = &dc
}
if t.DoneDate != nil {
dd := t.DoneDate.Unix()
tmp.DoneDate = &dd
}
if t.SecondsSeeding != nil {
ss := int64(*t.SecondsSeeding / time.Second)
tmp.SecondsSeeding = &ss
}
if t.StartDate != nil {
st := t.StartDate.Unix()
tmp.StartDate = &st
}
// Boolean as number
if t.Wanted != nil {
tmp.Wanted = make([]int64, len(t.Wanted))
for index, value := range t.Wanted {
if value {
tmp.Wanted[index] = 1
}
}
}
// Marshall original values within the tmp payload
return json.Marshal(&tmp)
}
// TorrentFile represent one file from a Torrent.
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L236
type TorrentFile struct {
BytesCompleted int64 `json:"bytesCompleted"`
Length int64 `json:"length"`
Name string `json:"name"`
}
// TorrentFileStat represents the metadata of a torrent's file.
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L242
type TorrentFileStat struct {
BytesCompleted int64 `json:"bytesCompleted"`
Wanted bool `json:"wanted"`
Priority int64 `json:"priority"`
}
// Peer represent a peer metadata of a torrent's peer list.
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L250
type Peer struct {
Address string `json:"address"`
ClientName string `json:"clientName"`
ClientIsChoked bool `json:"clientIsChoked"`
ClientIsint64erested bool `json:"clientIsint64erested"`
FlagStr string `json:"flagStr"`
IsDownloadingFrom bool `json:"isDownloadingFrom"`
IsEncrypted bool `json:"isEncrypted"`
IsIncoming bool `json:"isIncoming"`
IsUploadingTo bool `json:"isUploadingTo"`
IsUTP bool `json:"isUTP"`
PeerIsChoked bool `json:"peerIsChoked"`
PeerIsint64erested bool `json:"peerIsint64erested"`
Port int64 `json:"port"`
Progress float64 `json:"progress"`
RateToClient int64 `json:"rateToClient"` // B/s
RateToPeer int64 `json:"rateToPeer"` // B/s
}
// ConvertDownloadSpeed will return the download speed from peer as cunits.Bits/second
func (p *Peer) ConvertDownloadSpeed() (speed cunits.Bits) {
return cunits.ImportInByte(float64(p.RateToClient))
}
// ConvertUploadSpeed will return the upload speed to peer as cunits.Bits/second
func (p *Peer) ConvertUploadSpeed() (speed cunits.Bits) {
return cunits.ImportInByte(float64(p.RateToPeer))
}
// TorrentPeersFrom represents the peers statistics of a torrent.
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L269
type TorrentPeersFrom struct {
FromCache int64 `json:"fromCache"`
FromDHT int64 `json:"fromDht"`
FromIncoming int64 `json:"fromIncoming"`
FromLPD int64 `json:"fromLpd"`
FromLTEP int64 `json:"fromLtep"`
FromPEX int64 `json:"fromPex"`
FromTracker int64 `json:"fromTracker"`
}
// SeedRatioMode represents a torrent current seeding mode
type SeedRatioMode int64
const (
// SeedRatioModeGlobal represents the use of the global ratio for a torrent
SeedRatioModeGlobal SeedRatioMode = 0
// SeedRatioModeCustom represents the use of a custom ratio for a torrent
SeedRatioModeCustom SeedRatioMode = 1
// SeedRatioModeNoRatio represents the absence of ratio for a torrent
SeedRatioModeNoRatio SeedRatioMode = 2
)
func (srm SeedRatioMode) String() string {
switch srm {
case SeedRatioModeGlobal:
return "global"
case SeedRatioModeCustom:
return "custom"
case SeedRatioModeNoRatio:
return "no ratio"
default:
return "<unknown>"
}
}
// GoString implements the GoStringer interface from the stdlib fmt package
func (srm SeedRatioMode) GoString() string {
switch srm {
case SeedRatioModeGlobal:
return fmt.Sprintf("global (%d)", srm)
case SeedRatioModeCustom:
return fmt.Sprintf("custom (%d)", srm)
case SeedRatioModeNoRatio:
return fmt.Sprintf("no ratio (%d)", srm)
default:
return fmt.Sprintf("<unknown> (%d)", srm)
}
}
// TorrentStatus binds torrent status to a status code
type TorrentStatus int64
const (
// TorrentStatusStopped represents a stopped torrent
TorrentStatusStopped TorrentStatus = 0
// TorrentStatusCheckWait represents a torrent queued for files checking
TorrentStatusCheckWait TorrentStatus = 1
// TorrentStatusCheck represents a torrent which files are currently checked
TorrentStatusCheck TorrentStatus = 2
// TorrentStatusDownloadWait represents a torrent queue to download
TorrentStatusDownloadWait TorrentStatus = 3
// TorrentStatusDownload represents a torrent currently downloading
TorrentStatusDownload TorrentStatus = 4
// TorrentStatusSeedWait represents a torrent queued to seed
TorrentStatusSeedWait TorrentStatus = 5
// TorrentStatusSeed represents a torrent currently seeding
TorrentStatusSeed TorrentStatus = 6
// TorrentStatusIsolated represents a torrent which can't find peers
TorrentStatusIsolated TorrentStatus = 7
)
func (status TorrentStatus) String() string {
switch status {
case TorrentStatusStopped:
return "stopped"
case TorrentStatusCheckWait:
return "waiting to check files"
case TorrentStatusCheck:
return "checking files"
case TorrentStatusDownloadWait:
return "waiting to download"
case TorrentStatusDownload:
return "downloading"
case TorrentStatusSeedWait:
return "waiting to seed"
case TorrentStatusSeed:
return "seeding"
case TorrentStatusIsolated:
return "can't find peers"
default:
return "<unknown>"
}
}
// GoString implements the GoStringer interface from the stdlib fmt package
func (status TorrentStatus) GoString() string {
switch status {
case TorrentStatusStopped:
return fmt.Sprintf("stopped (%d)", status)
case TorrentStatusCheckWait:
return fmt.Sprintf("waiting to check files (%d)", status)
case TorrentStatusCheck:
return fmt.Sprintf("checking files (%d)", status)
case TorrentStatusDownloadWait:
return fmt.Sprintf("waiting to download (%d)", status)
case TorrentStatusDownload:
return fmt.Sprintf("downloading (%d)", status)
case TorrentStatusSeedWait:
return fmt.Sprintf("waiting to seed (%d)", status)
case TorrentStatusSeed:
return fmt.Sprintf("seeding (%d)", status)
case TorrentStatusIsolated:
return fmt.Sprintf("can't find peers (%d)", status)
default:
return fmt.Sprintf("<unknown> (%d)", status)
}
}
// Tracker represent the base data of a torrent's tracker.
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L289
type Tracker struct {
Announce string `json:"announce"`
ID int64 `json:"id"`
Scrape string `json:"scrape"`
Tier int64 `json:"tier"`
}
// TrackerStats represent the extended data of a torrent's tracker.
// https://github.com/transmission/transmission/blob/2.9x/extras/rpc-spec.txt#L296
type TrackerStats struct {
Announce string `json:"announce"`
AnnounceState int64 `json:"announceState"`
DownloadCount int64 `json:"downloadCount"`
HasAnnounced bool `json:"hasAnnounced"`
HasScraped bool `json:"hasScraped"`
Host string `json:"host"`
ID int64 `json:"id"`
IsBackup bool `json:"isBackup"`
LastAnnouncePeerCount int64 `json:"lastAnnouncePeerCount"`
LastAnnounceResult string `json:"lastAnnounceResult"`
LastAnnounceStartTime time.Time `json:"lastAnnounceStartTime"`
LastAnnounceSucceeded bool `json:"lastAnnounceSucceeded"`
LastAnnounceTime time.Time `json:"lastAnnounceTime"`
LastAnnounceTimedOut bool `json:"lastAnnounceTimedOut"`
LastScrapeResult string `json:"lastScrapeResult"`
LastScrapeStartTime time.Time `json:"lastScrapeStartTime"`
LastScrapeSucceeded bool `json:"lastScrapeSucceeded"`
LastScrapeTime time.Time `json:"lastScrapeTime"`
LastScrapeTimedOut bool `json:"lastScrapeTimedOut"` // should be boolean but number. Will be converter in UnmarshalJSON
LeecherCount int64 `json:"leecherCount"`
NextAnnounceTime time.Time `json:"nextAnnounceTime"`
NextScrapeTime time.Time `json:"nextScrapeTime"`
Scrape string `json:"scrape"`
ScrapeState int64 `json:"scrapeState"`
SeederCount int64 `json:"seederCount"`
Tier int64 `json:"tier"`
}
// UnmarshalJSON allows to convert timestamps to golang time.Time values.
func (ts *TrackerStats) UnmarshalJSON(data []byte) (err error) {
// Shadow real type for regular unmarshalling
type RawTrackerStats TrackerStats
tmp := struct {
LastAnnounceStartTime int64 `json:"lastAnnounceStartTime"`
LastAnnounceTime int64 `json:"lastAnnounceTime"`
LastScrapeStartTime int64 `json:"lastScrapeStartTime"`
LastScrapeTime int64 `json:"lastScrapeTime"`
LastScrapeTimedOut int64 `json:"lastScrapeTimedOut"`
NextAnnounceTime int64 `json:"nextAnnounceTime"`
NextScrapeTime int64 `json:"nextScrapeTime"`
*RawTrackerStats
}{
RawTrackerStats: (*RawTrackerStats)(ts),
}
// Unmarshall (with timestamps as number)
if err = json.Unmarshal(data, &tmp); err != nil {
return
}
// Convert to real boolean
if tmp.LastScrapeTimedOut == 1 {
ts.LastScrapeTimedOut = true
} else if tmp.LastScrapeTimedOut != 0 {
return fmt.Errorf("can't convert 'lastScrapeTimedOut' value '%v' into boolean", tmp.LastScrapeTimedOut)
}
// Create the real time value from the timestamps
ts.LastAnnounceStartTime = time.Unix(tmp.LastAnnounceStartTime, 0)
ts.LastAnnounceTime = time.Unix(tmp.LastAnnounceTime, 0)
ts.LastScrapeStartTime = time.Unix(tmp.LastScrapeStartTime, 0)
ts.LastScrapeTime = time.Unix(tmp.LastScrapeTime, 0)
ts.NextAnnounceTime = time.Unix(tmp.NextAnnounceTime, 0)
ts.NextScrapeTime = time.Unix(tmp.NextScrapeTime, 0)
return
}
// MarshalJSON allows to convert back golang values to original payload values.
func (ts *TrackerStats) MarshalJSON() (data []byte, err error) {
// Shadow real type for regular unmarshalling
type RawTrackerStats TrackerStats
tmp := struct {
LastAnnounceStartTime int64 `json:"lastAnnounceStartTime"`
LastAnnounceTime int64 `json:"lastAnnounceTime"`
LastScrapeStartTime int64 `json:"lastScrapeStartTime"`
LastScrapeTime int64 `json:"lastScrapeTime"`
LastScrapeTimedOut int64 `json:"lastScrapeTimedOut"`
NextAnnounceTime int64 `json:"nextAnnounceTime"`
NextScrapeTime int64 `json:"nextScrapeTime"`
*RawTrackerStats
}{
LastAnnounceStartTime: ts.LastAnnounceStartTime.Unix(),
LastAnnounceTime: ts.LastAnnounceTime.Unix(),
LastScrapeStartTime: ts.LastScrapeStartTime.Unix(),
LastScrapeTime: ts.LastScrapeTime.Unix(),
NextAnnounceTime: ts.NextAnnounceTime.Unix(),
NextScrapeTime: ts.NextScrapeTime.Unix(),
RawTrackerStats: (*RawTrackerStats)(ts),
}
// Convert real bool to its number form
if ts.LastScrapeTimedOut {
tmp.LastScrapeTimedOut = 1
}
// MarshalJSON allows to convert back golang values to original payload values
return json.Marshal(&tmp)
}