diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d714246 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +TRANSMISSION_ADDR=http://localhost:9091 +TRANSMISSION_PASSWORD= +TRANSMISSION_USERNAME= +WEB_ADDR=:19091 +WEB_PATH=/metrics diff --git a/.gitignore b/.gitignore index 8390d84..b07b78b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.env /transmission-exporter *.o diff --git a/README.md b/README.md index b5e9ae2..7fef856 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,6 @@ ENV Variable | Description | TRANSMISSION_USERNAME | Transmission username, no default | | TRANSMISSION_PASSWORD | Transmission password, no default | -### Build - - make - -For development we encourage you to use `make install` instead, it's faster. - ### Docker docker pull metalmatze/transmission-exporter @@ -49,6 +43,14 @@ Example `docker-compose.yml` with Transmission also running in docker. environment: TRANSMISSION_ADDR: http://transmission:9091 +### Development + + make + +For development we encourage you to use `make install` instead, it's faster. + +Now simply copy the `.env.example` to `.env`, like `cp .env.example .env` and set your preferences. +Now you're good to go. ### Original authors of the Transmission package Tobias Blom (https://github.com/tubbebubbe/transmission) diff --git a/cmd/transmission-exporter/main.go b/cmd/transmission-exporter/main.go index 8543cbd..7f159f0 100644 --- a/cmd/transmission-exporter/main.go +++ b/cmd/transmission-exporter/main.go @@ -5,22 +5,28 @@ import ( "net/http" arg "github.com/alexflint/go-arg" + "github.com/joho/godotenv" transmission "github.com/metalmatze/transmission-exporter" "github.com/prometheus/client_golang/prometheus" ) // Config gets its content from env and passes it on to different packages type Config struct { - WebPath string `arg:"env:WEB_PATH"` - WebAddr string `arg:"env:WEB_ADDR"` TransmissionAddr string `arg:"env:TRANSMISSION_ADDR"` - TransmissionUsername string `arg:"env:TRANSMISSION_USERNAME"` TransmissionPassword string `arg:"env:TRANSMISSION_PASSWORD"` + TransmissionUsername string `arg:"env:TRANSMISSION_USERNAME"` + WebAddr string `arg:"env:WEB_ADDR"` + WebPath string `arg:"env:WEB_PATH"` } func main() { log.Println("starting transmission-exporter") + err := godotenv.Load() + if err != nil { + log.Println("no .env present") + } + c := Config{ WebPath: "/metrics", WebAddr: ":19091", @@ -40,8 +46,11 @@ func main() { client := transmission.New(c.TransmissionAddr, user) prometheus.MustRegister(NewTorrentCollector(client)) + prometheus.MustRegister(NewSessionCollector(client)) + prometheus.MustRegister(NewSessionStatsCollector(client)) http.Handle(c.WebPath, prometheus.Handler()) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(` Node Exporter @@ -54,3 +63,10 @@ func main() { log.Fatal(http.ListenAndServe(c.WebAddr, nil)) } + +func boolToString(true bool) string { + if true { + return "1" + } + return "0" +} diff --git a/cmd/transmission-exporter/session_collector.go b/cmd/transmission-exporter/session_collector.go new file mode 100644 index 0000000..8fa7478 --- /dev/null +++ b/cmd/transmission-exporter/session_collector.go @@ -0,0 +1,200 @@ +package main + +import ( + "log" + + "github.com/metalmatze/transmission-exporter" + "github.com/prometheus/client_golang/prometheus" +) + +// SessionCollector exposes session metrics +type SessionCollector struct { + client *transmission.Client + + AltSpeedDown *prometheus.Desc + AltSpeedUp *prometheus.Desc + CacheSize *prometheus.Desc + FreeSpace *prometheus.Desc + QueueDown *prometheus.Desc + QueueUp *prometheus.Desc + PeerLimitGlobal *prometheus.Desc + PeerLimitTorrent *prometheus.Desc + SeedRatioLimit *prometheus.Desc + SpeedLimitDown *prometheus.Desc + SpeedLimitUp *prometheus.Desc + Version *prometheus.Desc +} + +// NewSessionCollector takes a transmission.Client and returns a SessionCollector +func NewSessionCollector(client *transmission.Client) *SessionCollector { + return &SessionCollector{ + client: client, + + AltSpeedDown: prometheus.NewDesc( + namespace+"alt_speed_down", + "Alternative max global download speed", + []string{"enabled"}, + nil, + ), + AltSpeedUp: prometheus.NewDesc( + namespace+"alt_speed_up", + "Alternative max global upload speed", + []string{"enabled"}, + nil, + ), + CacheSize: prometheus.NewDesc( + namespace+"cache_size_bytes", + "Maximum size of the disk cache", + nil, + nil, + ), + FreeSpace: prometheus.NewDesc( + namespace+"free_space", + "Free space left on device to download to", + []string{"download_dir", "incomplete_dir"}, + nil, + ), + QueueDown: prometheus.NewDesc( + namespace+"queue_down", + "Max number of torrents to download at once", + []string{"enabled"}, + nil, + ), + QueueUp: prometheus.NewDesc( + namespace+"queue_up", + "Max number of torrents to upload at once", + []string{"enabled"}, + nil, + ), + PeerLimitGlobal: prometheus.NewDesc( + namespace+"global_peer_limit", + "Maximum global number of peers", + nil, + nil, + ), + PeerLimitTorrent: prometheus.NewDesc( + namespace+"torrent_peer_limit", + "Maximum number of peers for a single torrent", + nil, + nil, + ), + SeedRatioLimit: prometheus.NewDesc( + namespace+"seed_ratio_limit", + "The default seed ratio for torrents to use", + []string{"enabled"}, + nil, + ), + SpeedLimitDown: prometheus.NewDesc( + namespace+"speed_limit_down_bytes", + "Max global download speed", + []string{"enabled"}, + nil, + ), + SpeedLimitUp: prometheus.NewDesc( + namespace+"speed_limit_up_bytes", + "Max global upload speed", + []string{"enabled"}, + nil, + ), + Version: prometheus.NewDesc( + namespace+"version", + "Transmission version as label", + []string{"version"}, + nil, + ), + } +} + +// Describe implements the prometheus.Collector interface +func (sc *SessionCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- sc.AltSpeedDown + ch <- sc.AltSpeedUp + ch <- sc.CacheSize + ch <- sc.FreeSpace + ch <- sc.QueueDown + ch <- sc.QueueUp + ch <- sc.PeerLimitGlobal + ch <- sc.PeerLimitTorrent + ch <- sc.SeedRatioLimit + ch <- sc.SpeedLimitDown + ch <- sc.SpeedLimitUp + ch <- sc.Version +} + +// Collect implements the prometheus.Collector interface +func (sc *SessionCollector) Collect(ch chan<- prometheus.Metric) { + session, err := sc.client.GetSession() + if err != nil { + log.Printf("failed to get session: %v", err) + } + + ch <- prometheus.MustNewConstMetric( + sc.AltSpeedDown, + prometheus.GaugeValue, + float64(session.AltSpeedDown), + boolToString(session.AltSpeedEnabled), + ) + ch <- prometheus.MustNewConstMetric( + sc.AltSpeedUp, + prometheus.GaugeValue, + float64(session.AltSpeedUp), + boolToString(session.AltSpeedEnabled), + ) + ch <- prometheus.MustNewConstMetric( + sc.CacheSize, + prometheus.GaugeValue, + float64(session.CacheSizeMB*1024*1024), + ) + ch <- prometheus.MustNewConstMetric( + sc.FreeSpace, + prometheus.GaugeValue, + float64(session.DownloadDirFreeSpace), + session.DownloadDir, session.IncompleteDir, + ) + ch <- prometheus.MustNewConstMetric( + sc.QueueDown, + prometheus.GaugeValue, + float64(session.DownloadQueueSize), + boolToString(session.DownloadQueueEnabled), + ) + ch <- prometheus.MustNewConstMetric( + sc.QueueUp, + prometheus.GaugeValue, + float64(session.SeedQueueSize), + boolToString(session.SeedQueueEnabled), + ) + ch <- prometheus.MustNewConstMetric( + sc.PeerLimitGlobal, + prometheus.GaugeValue, + float64(session.PeerLimitGlobal), + ) + ch <- prometheus.MustNewConstMetric( + sc.PeerLimitTorrent, + prometheus.GaugeValue, + float64(session.PeerLimitPerTorrent), + ) + ch <- prometheus.MustNewConstMetric( + sc.SeedRatioLimit, + prometheus.GaugeValue, + float64(session.SeedRatioLimit), + boolToString(session.SeedRatioLimited), + ) + ch <- prometheus.MustNewConstMetric( + sc.SpeedLimitDown, + prometheus.GaugeValue, + float64(session.SpeedLimitDown), + boolToString(session.SpeedLimitDownEnabled), + ) + ch <- prometheus.MustNewConstMetric( + sc.SpeedLimitUp, + prometheus.GaugeValue, + float64(session.SpeedLimitUp), + boolToString(session.SpeedLimitUpEnabled), + ) + ch <- prometheus.MustNewConstMetric( + sc.Version, + prometheus.GaugeValue, + float64(1), + session.Version, + ) +} diff --git a/cmd/transmission-exporter/session_stats_collector.go b/cmd/transmission-exporter/session_stats_collector.go new file mode 100644 index 0000000..4bcba88 --- /dev/null +++ b/cmd/transmission-exporter/session_stats_collector.go @@ -0,0 +1,185 @@ +package main + +import ( + "log" + "time" + + "github.com/metalmatze/transmission-exporter" + "github.com/prometheus/client_golang/prometheus" +) + +// SessionStatsCollector exposes SessionStats as metrics +type SessionStatsCollector struct { + client *transmission.Client + + DownloadSpeed *prometheus.Desc + UploadSpeed *prometheus.Desc + TorrentsTotal *prometheus.Desc + TorrentsActive *prometheus.Desc + TorrentsPaused *prometheus.Desc + + Downloaded *prometheus.Desc + Uploaded *prometheus.Desc + FilesAdded *prometheus.Desc + ActiveTime *prometheus.Desc + SessionCount *prometheus.Desc +} + +// NewSessionStatsCollector takes a transmission.Client and returns a SessionStatsCollector +func NewSessionStatsCollector(client *transmission.Client) *SessionStatsCollector { + const collectorNamespace = "session_stats_" + + return &SessionStatsCollector{ + client: client, + + DownloadSpeed: prometheus.NewDesc( + namespace+collectorNamespace+"download_speed_bytes", + "Current download speed in bytes", + nil, + nil, + ), + UploadSpeed: prometheus.NewDesc( + namespace+collectorNamespace+"upload_speed_bytes", + "Current download speed in bytes", + nil, + nil, + ), + TorrentsTotal: prometheus.NewDesc( + namespace+collectorNamespace+"torrents_total", + "The total number of torrents", + nil, + nil, + ), + TorrentsActive: prometheus.NewDesc( + namespace+collectorNamespace+"torrents_active", + "The number of active torrents", + nil, + nil, + ), + TorrentsPaused: prometheus.NewDesc( + namespace+collectorNamespace+"torrents_paused", + "The number of paused torrents", + nil, + nil, + ), + + Downloaded: prometheus.NewDesc( + namespace+collectorNamespace+"downloaded_bytes", + "The number of downloaded bytes", + []string{"type"}, + nil, + ), + Uploaded: prometheus.NewDesc( + namespace+collectorNamespace+"uploaded_bytes", + "The number of uploaded bytes", + []string{"type"}, + nil, + ), + FilesAdded: prometheus.NewDesc( + namespace+collectorNamespace+"files_added", + "The number of files added", + []string{"type"}, + nil, + ), + ActiveTime: prometheus.NewDesc( + namespace+collectorNamespace+"active", + "The time transmission is active since", + []string{"type"}, + nil, + ), + SessionCount: prometheus.NewDesc( + namespace+collectorNamespace+"sessions", + "Count of the times transmission started", + []string{"type"}, + nil, + ), + } +} + +// Describe implements the prometheus.Collector interface +func (sc *SessionStatsCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- sc.DownloadSpeed + ch <- sc.UploadSpeed + ch <- sc.TorrentsTotal + ch <- sc.TorrentsActive + ch <- sc.TorrentsPaused +} + +// Collect implements the prometheus.Collector interface +func (sc *SessionStatsCollector) Collect(ch chan<- prometheus.Metric) { + stats, err := sc.client.GetSessionStats() + if err != nil { + log.Printf("failed to get session stats: %v", err) + } + + ch <- prometheus.MustNewConstMetric( + sc.DownloadSpeed, + prometheus.GaugeValue, + float64(stats.DownloadSpeed), + ) + ch <- prometheus.MustNewConstMetric( + sc.UploadSpeed, + prometheus.GaugeValue, + float64(stats.UploadSpeed), + ) + ch <- prometheus.MustNewConstMetric( + sc.TorrentsTotal, + prometheus.GaugeValue, + float64(stats.TorrentCount), + ) + ch <- prometheus.MustNewConstMetric( + sc.TorrentsActive, + prometheus.GaugeValue, + float64(stats.ActiveTorrentCount), + ) + ch <- prometheus.MustNewConstMetric( + sc.TorrentsPaused, + prometheus.GaugeValue, + float64(stats.PausedTorrentCount), + ) + + types := []string{"current", "cumulative"} + for _, t := range types { + var stateStats transmission.SessionStateStats + if t == types[0] { + stateStats = stats.CurrentStats + } else { + stateStats = stats.CumulativeStats + } + + ch <- prometheus.MustNewConstMetric( + sc.Downloaded, + prometheus.GaugeValue, + float64(stateStats.DownloadedBytes), + t, + ) + ch <- prometheus.MustNewConstMetric( + sc.Uploaded, + prometheus.GaugeValue, + float64(stateStats.UploadedBytes), + t, + ) + ch <- prometheus.MustNewConstMetric( + sc.FilesAdded, + prometheus.GaugeValue, + float64(stateStats.FilesAdded), + t, + ) + + dur := time.Duration(stateStats.SecondsActive) * time.Second + timestamp := time.Now().Add(-1 * dur).Unix() + + ch <- prometheus.MustNewConstMetric( + sc.ActiveTime, + prometheus.GaugeValue, + float64(timestamp), + t, + ) + ch <- prometheus.MustNewConstMetric( + sc.SessionCount, + prometheus.GaugeValue, + float64(stateStats.SessionCount), + t, + ) + } +} diff --git a/cmd/transmission-exporter/torrent_collector.go b/cmd/transmission-exporter/torrent_collector.go index cb9822b..33d8561 100644 --- a/cmd/transmission-exporter/torrent_collector.go +++ b/cmd/transmission-exporter/torrent_collector.go @@ -1,83 +1,134 @@ package main import ( + "log" "strconv" transmission "github.com/metalmatze/transmission-exporter" "github.com/prometheus/client_golang/prometheus" ) +const ( + namespace string = "transmission_" +) + // TorrentCollector has a transmission.Client to create torrent metrics type TorrentCollector struct { - client *transmission.Client + client *transmission.Client + Status *prometheus.Desc + Added *prometheus.Desc + Files *prometheus.Desc Finished *prometheus.Desc Done *prometheus.Desc - Added *prometheus.Desc Ratio *prometheus.Desc Download *prometheus.Desc Upload *prometheus.Desc + + // TrackerStats + Downloads *prometheus.Desc + Leechers *prometheus.Desc + Seeders *prometheus.Desc } // NewTorrentCollector creates a new torrent collector with the transmission.Client func NewTorrentCollector(client *transmission.Client) *TorrentCollector { + const collectorNamespace = "torrent_" + return &TorrentCollector{ client: client, Status: prometheus.NewDesc( - "transmission_torrent_status", + namespace+collectorNamespace+"status", "Status of a torrent", []string{"id", "name"}, nil, ), + Added: prometheus.NewDesc( + namespace+collectorNamespace+"added", + "The unixtime time a torrent was added", + []string{"id", "name"}, + nil, + ), + Files: prometheus.NewDesc( + namespace+collectorNamespace+"files_total", + "The unixtime time a torrent was added", + []string{"id", "name"}, + nil, + ), Finished: prometheus.NewDesc( - "transmission_torrent_finished", + namespace+collectorNamespace+"finished", "Indicates if a torrent is finished (1) or not (0)", []string{"id", "name"}, nil, ), Done: prometheus.NewDesc( - "transmission_torrent_done", + namespace+collectorNamespace+"done", "The percent of a torrent being done", []string{"id", "name"}, nil, ), - Added: prometheus.NewDesc( - "transmission_torrent_added", - "The unixtime time a torrent was added", - []string{"id", "name"}, - nil, - ), Ratio: prometheus.NewDesc( - "transmission_torrent_ratio", + namespace+collectorNamespace+"ratio", "The upload ratio of a torrent", []string{"id", "name"}, nil, ), Download: prometheus.NewDesc( - "transmission_torrent_download_bytes", + namespace+collectorNamespace+"download_bytes", "The current download rate of a torrent in bytes", []string{"id", "name"}, nil, ), Upload: prometheus.NewDesc( - "transmission_torrent_upload_bytes", + namespace+collectorNamespace+"upload_bytes", "The current upload rate of a torrent in bytes", []string{"id", "name"}, nil, ), + + // TrackerStats + Downloads: prometheus.NewDesc( + namespace+collectorNamespace+"downloads_total", + "How often this torrent was downloaded", + []string{"id", "name", "tracker"}, + nil, + ), + Leechers: prometheus.NewDesc( + namespace+collectorNamespace+"leechers", + "The number of peers downloading this torrent", + []string{"id", "name", "tracker"}, + nil, + ), + Seeders: prometheus.NewDesc( + namespace+collectorNamespace+"seeders", + "The number of peers uploading this torrent", + []string{"id", "name", "tracker"}, + nil, + ), } } // Describe implements the prometheus.Collector interface func (tc *TorrentCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- tc.Status + ch <- tc.Added + ch <- tc.Files + ch <- tc.Finished + ch <- tc.Done ch <- tc.Ratio + ch <- tc.Download + ch <- tc.Upload + ch <- tc.Downloads + ch <- tc.Leechers + ch <- tc.Seeders } // Collect implements the prometheus.Collector interface func (tc *TorrentCollector) Collect(ch chan<- prometheus.Metric) { torrents, err := tc.client.GetTorrents() if err != nil { + log.Printf("failed to get torrents: %v", err) return } @@ -97,21 +148,27 @@ func (tc *TorrentCollector) Collect(ch chan<- prometheus.Metric) { id, t.Name, ) ch <- prometheus.MustNewConstMetric( - tc.Finished, + tc.Added, prometheus.GaugeValue, - finished, + float64(t.Added), id, t.Name, ) ch <- prometheus.MustNewConstMetric( - tc.Done, + tc.Files, prometheus.GaugeValue, - t.PercentDone, + float64(len(t.Files)), id, t.Name, ) ch <- prometheus.MustNewConstMetric( - tc.Added, + tc.Finished, prometheus.GaugeValue, - float64(t.Added), + finished, + id, t.Name, + ) + ch <- prometheus.MustNewConstMetric( + tc.Done, + prometheus.GaugeValue, + t.PercentDone, id, t.Name, ) ch <- prometheus.MustNewConstMetric( @@ -132,5 +189,28 @@ func (tc *TorrentCollector) Collect(ch chan<- prometheus.Metric) { float64(t.RateUpload), id, t.Name, ) + + for _, tracker := range t.TrackerStats { + ch <- prometheus.MustNewConstMetric( + tc.Downloads, + prometheus.GaugeValue, + float64(tracker.DownloadCount), + id, t.Name, tracker.Host, + ) + + ch <- prometheus.MustNewConstMetric( + tc.Leechers, + prometheus.GaugeValue, + float64(tracker.LeecherCount), + id, t.Name, tracker.Host, + ) + + ch <- prometheus.MustNewConstMetric( + tc.Seeders, + prometheus.GaugeValue, + float64(tracker.SeederCount), + id, t.Name, tracker.Host, + ) + } } } diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index e0e90bb..f93914b 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -5,6 +5,7 @@ transmission: - "127.0.0.1:9091:9091" - "51413:51413" - "51413:51413/udp" + transmission-exporter: image: metalmatze/transmission-exporter restart: always @@ -14,3 +15,13 @@ transmission-exporter: - "127.0.0.1:19091:19091" environment: TRANSMISSION_ADDR: http://transmission:9091 + +prometheus: + image: quay.io/prometheus/prometheus + command: "-config.file=/etc/prometheus/prometheus.yml" + links: + - transmission-exporter + ports: + - "127.0.0.1:9090:9090" + volumes: + - "./prometheus:/etc/prometheus/" diff --git a/examples/prometheus/prometheus.yml b/examples/prometheus/prometheus.yml new file mode 100644 index 0000000..fec7444 --- /dev/null +++ b/examples/prometheus/prometheus.yml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: +- job_name: 'transmission-exporter' + static_configs: + - targets: + - 'transmission-exporter:19091' diff --git a/rpc_command.go b/rpc_command.go deleted file mode 100644 index 264a3d8..0000000 --- a/rpc_command.go +++ /dev/null @@ -1,27 +0,0 @@ -package transmission - -type ( - // RPCCommand is the root command to interact with Transmission via RPC - RPCCommand struct { - Method string `json:"method,omitempty"` - Arguments RPCArguments `json:"arguments,omitempty"` - Result string `json:"result,omitempty"` - } - // RPCArguments specifies the RPCCommand in more detail - RPCArguments struct { - Fields []string `json:"fields,omitempty"` - Torrents []Torrent `json:"torrents,omitempty"` - Ids []int `json:"ids,omitempty"` - DeleteData bool `json:"delete-local-data,omitempty"` - DownloadDir string `json:"download-dir,omitempty"` - MetaInfo string `json:"metainfo,omitempty"` - Filename string `json:"filename,omitempty"` - TorrentAdded RPCTorrentAdded `json:"torrent-added"` - } - // RPCTorrentAdded specifies the torrent to get added data from - RPCTorrentAdded struct { - HashString string `json:"hashString"` - ID int `json:"id"` - Name string `json:"name"` - } -) diff --git a/session.go b/session.go new file mode 100644 index 0000000..1a5af49 --- /dev/null +++ b/session.go @@ -0,0 +1,62 @@ +package transmission + +type ( + // SessionCommand is the root command to interact with Transmission via RPC + SessionCommand struct { + Method string `json:"method,omitempty"` + Session Session `json:"arguments,omitempty"` + Result string `json:"result,omitempty"` + } + + // Session information about the current transmission session + Session struct { + AltSpeedDown int `json:"alt-speed-down"` + AltSpeedEnabled bool `json:"alt-speed-enabled"` + //Alt_speed_time_begin int `json:"alt-speed-time-begin"` + //Alt_speed_time_day int `json:"alt-speed-time-day"` + //Alt_speed_time_enabled bool `json:"alt-speed-time-enabled"` + //Alt_speed_time_end int `json:"alt-speed-time-end"` + AltSpeedUp int `json:"alt-speed-up"` + //Blocklist_enabled bool `json:"blocklist-enabled"` + //Blocklist_size int `json:"blocklist-size"` + //Blocklist_url string `json:"blocklist-url"` + CacheSizeMB int `json:"cache-size-mb"` + //Config_dir string `json:"config-dir"` + //Dht_enabled bool `json:"dht-enabled"` + DownloadDir string `json:"download-dir"` + DownloadDirFreeSpace int `json:"download-dir-free-space"` + DownloadQueueEnabled bool `json:"download-queue-enabled"` + DownloadQueueSize int `json:"download-queue-size"` + //Encryption string `json:"encryption"` + //Idle_seeding_limit int `json:"idle-seeding-limit"` + //Idle_seeding_limit_enabled bool `json:"idle-seeding-limit-enabled"` + IncompleteDir string `json:"incomplete-dir"` + //Incomplete_dir_enabled bool `json:"incomplete-dir-enabled"` + //Lpd_enabled bool `json:"lpd-enabled"` + PeerLimitGlobal int `json:"peer-limit-global"` + PeerLimitPerTorrent int `json:"peer-limit-per-torrent"` + //Peer_port int `json:"peer-port"` + //Peer_port_random_on_start bool `json:"peer-port-random-on-start"` + //Pex_enabled bool `json:"pex-enabled"` + //Port_forwarding_enabled bool `json:"port-forwarding-enabled"` + //Queue_stalled_enabled bool `json:"queue-stalled-enabled"` + //Queue_stalled_minutes int `json:"queue-stalled-minutes"` + //Rename_partial_files bool `json:"rename-partial-files"` + //RPC_version int `json:"rpc-version"` + //RPC_version_minimum int `json:"rpc-version-minimum"` + //Script_torrent_done_enabled bool `json:"script-torrent-done-enabled"` + //Script_torrent_done_filename string `json:"script-torrent-done-filename"` + SeedQueueEnabled bool `json:"seed-queue-enabled"` + SeedQueueSize int `json:"seed-queue-size"` + SeedRatioLimit int `json:"seedRatioLimit"` + SeedRatioLimited bool `json:"seedRatioLimited"` + SpeedLimitDown int `json:"speed-limit-down"` + SpeedLimitDownEnabled bool `json:"speed-limit-down-enabled"` + SpeedLimitUp int `json:"speed-limit-up"` + SpeedLimitUpEnabled bool `json:"speed-limit-up-enabled"` + //Start_added_torrents bool `json:"start-added-torrents"` + //Trash_original_torrent_files bool `json:"trash-original-torrent-files"` + //Utp_enabled bool `json:"utp-enabled"` + Version string `json:"version"` + } +) diff --git a/session_stats.go b/session_stats.go new file mode 100644 index 0000000..4b5f7a9 --- /dev/null +++ b/session_stats.go @@ -0,0 +1,28 @@ +package transmission + +type ( + // SessionStatsCmd is the root command to interact with Transmission via RPC + SessionStatsCmd struct { + SessionStats `json:"arguments"` + Result string `json:"result"` + } + + // SessionStats contains information about the current & cumulative session + SessionStats struct { + DownloadSpeed int `json:"downloadSpeed"` + UploadSpeed int `json:"uploadSpeed"` + ActiveTorrentCount int `json:"activeTorrentCount"` + PausedTorrentCount int `json:"pausedTorrentCount"` + TorrentCount int `json:"torrentCount"` + CumulativeStats SessionStateStats `json:"cumulative-stats"` + CurrentStats SessionStateStats `json:"current-stats"` + } + // SessionStateStats contains current or cumulative session stats + SessionStateStats struct { + DownloadedBytes int `json:"downloadedBytes"` + UploadedBytes int `json:"uploadedBytes"` + FilesAdded int `json:"filesAdded"` + SecondsActive int `json:"secondsActive"` + SessionCount int `json:"sessionCount"` + } +) diff --git a/torrent.go b/torrent.go index 41e4f73..c3a05e0 100644 --- a/torrent.go +++ b/torrent.go @@ -1,24 +1,52 @@ package transmission type ( + // TorrentCommand is the root command to interact with Transmission via RPC + TorrentCommand struct { + Method string `json:"method,omitempty"` + Arguments TorrentArguments `json:"arguments,omitempty"` + Result string `json:"result,omitempty"` + } + // TorrentArguments specifies the TorrentCommand in more detail + TorrentArguments struct { + Fields []string `json:"fields,omitempty"` + Torrents []Torrent `json:"torrents,omitempty"` + Ids []int `json:"ids,omitempty"` + DeleteData bool `json:"delete-local-data,omitempty"` + DownloadDir string `json:"download-dir,omitempty"` + MetaInfo string `json:"metainfo,omitempty"` + Filename string `json:"filename,omitempty"` + TorrentAdded TorrentArgumentsAdded `json:"torrent-added"` + } + // TorrentArgumentsAdded specifies the torrent to get added data from + TorrentArgumentsAdded struct { + HashString string `json:"hashString"` + ID int `json:"id"` + Name string `json:"name"` + } + // Torrent represents a transmission torrent Torrent struct { - ID int `json:"id"` - Name string `json:"name"` - Status int `json:"status"` - Added int `json:"addedDate"` - LeftUntilDone int `json:"leftUntilDone"` - Eta int `json:"eta"` - UploadRatio float64 `json:"uploadRatio"` - RateDownload int `json:"rateDownload"` - RateUpload int `json:"rateUpload"` - DownloadDir string `json:"downloadDir"` - IsFinished bool `json:"isFinished"` - PercentDone float64 `json:"percentDone"` - SeedRatioMode int `json:"seedRatioMode"` - HashString string `json:"hashString"` - Error int `json:"error"` - ErrorString string `json:"errorString"` + ID int `json:"id"` + Name string `json:"name"` + Status int `json:"status"` + Added int `json:"addedDate"` + LeftUntilDone int `json:"leftUntilDone"` + Eta int `json:"eta"` + UploadRatio float64 `json:"uploadRatio"` + RateDownload int `json:"rateDownload"` + RateUpload int `json:"rateUpload"` + DownloadDir string `json:"downloadDir"` + IsFinished bool `json:"isFinished"` + PercentDone float64 `json:"percentDone"` + SeedRatioMode int `json:"seedRatioMode"` + HashString string `json:"hashString"` + Error int `json:"error"` + ErrorString string `json:"errorString"` + Files []File `json:"files"` + FilesStats []FileStat `json:"fileStats"` + TrackerStats []TrackerStat `json:"trackerStats"` + Peers []Peer `json:"peers"` } // ByID implements the sort Interface to sort by ID @@ -29,6 +57,70 @@ type ( ByDate []Torrent // ByRatio implements the sort Interface to sort by Ratio ByRatio []Torrent + + // File is a file contained inside a torrent + File struct { + BytesCompleted int `json:"bytesCompleted"` + Length int `json:"length"` + Name string `json:"name"` + } + + // FileStat describe a file's priority & if it's wanted + FileStat struct { + BytesCompleted int `json:"bytesCompleted"` + Priority int `json:"priority"` + Wanted bool `json:"wanted"` + } + + // TrackerStat has stats about the torrent's tracker + TrackerStat struct { + Announce string `json:"announce"` + AnnounceState int `json:"announceState"` + DownloadCount int `json:"downloadCount"` + HasAnnounced bool `json:"hasAnnounced"` + HasScraped bool `json:"hasScraped"` + Host string `json:"host"` + ID int `json:"id"` + IsBackup bool `json:"isBackup"` + LastAnnouncePeerCount int `json:"lastAnnouncePeerCount"` + LastAnnounceResult string `json:"lastAnnounceResult"` + LastAnnounceStartTime int `json:"lastAnnounceStartTime"` + LastAnnounceSucceeded bool `json:"lastAnnounceSucceeded"` + LastAnnounceTime int `json:"lastAnnounceTime"` + LastAnnounceTimedOut bool `json:"lastAnnounceTimedOut"` + LastScrapeResult string `json:"lastScrapeResult"` + LastScrapeStartTime int `json:"lastScrapeStartTime"` + LastScrapeSucceeded bool `json:"lastScrapeSucceeded"` + LastScrapeTime int `json:"lastScrapeTime"` + LastScrapeTimedOut int `json:"lastScrapeTimedOut"` + LeecherCount int `json:"leecherCount"` + NextAnnounceTime int `json:"nextAnnounceTime"` + NextScrapeTime int `json:"nextScrapeTime"` + Scrape string `json:"scrape"` + ScrapeState int `json:"scrapeState"` + SeederCount int `json:"seederCount"` + Tier int `json:"tier"` + } + + // Peer of a torrent + Peer struct { + Address string `json:"address"` + ClientIsChoked bool `json:"clientIsChoked"` + ClientIsInterested bool `json:"clientIsInterested"` + ClientName string `json:"clientName"` + FlagStr string `json:"flagStr"` + IsDownloadingFrom bool `json:"isDownloadingFrom"` + IsEncrypted bool `json:"isEncrypted"` + IsIncoming bool `json:"isIncoming"` + IsUTP bool `json:"isUTP"` + IsUploadingTo bool `json:"isUploadingTo"` + PeerIsChoked bool `json:"peerIsChoked"` + PeerIsInterested bool `json:"peerIsInterested"` + Port int `json:"port"` + Progress float64 `json:"progress"` + RateToClient int `json:"rateToClient"` + RateToPeer int `json:"rateToPeer"` + } ) func (t ByID) Len() int { return len(t) } diff --git a/transmission.go b/transmission.go index ba40ac5..fddebdc 100644 --- a/transmission.go +++ b/transmission.go @@ -104,33 +104,11 @@ func (c *Client) authRequest(method string, body []byte) (*http.Request, error) return req, nil } -// ExecuteCommand sends the RPCCommand to Transmission and get the response -func (c *Client) ExecuteCommand(cmd *RPCCommand) (*RPCCommand, error) { - var out RPCCommand - - body, err := json.Marshal(&cmd) - if err != nil { - return nil, err - } - - output, err := c.post(body) - if err != nil { - return nil, err - } - - err = json.Unmarshal(output, &out) - if err != nil { - return nil, err - } - - return &out, nil -} - // GetTorrents get a list of torrents func (c *Client) GetTorrents() ([]Torrent, error) { - cmd := &RPCCommand{ + cmd := TorrentCommand{ Method: "torrent-get", - Arguments: RPCArguments{ + Arguments: TorrentArguments{ Fields: []string{ "id", "name", @@ -148,14 +126,69 @@ func (c *Client) GetTorrents() ([]Torrent, error) { "seedRatioMode", "error", "errorString", + "files", + "fileStats", + "peers", + "trackers", + "trackerStats", }, }, } - out, err := c.ExecuteCommand(cmd) + req, err := json.Marshal(&cmd) + if err != nil { + return nil, err + } + + resp, err := c.post(req) if err != nil { return nil, err } + var out TorrentCommand + if err := json.Unmarshal(resp, &out); err != nil { + return nil, err + } + return out.Arguments.Torrents, nil } + +// GetSession gets the current session from transmission +func (c *Client) GetSession() (*Session, error) { + req, err := json.Marshal(SessionCommand{Method: "session-get"}) + if err != nil { + return nil, err + } + + resp, err := c.post(req) + if err != nil { + return nil, err + } + + var cmd SessionCommand + if err := json.Unmarshal(resp, &cmd); err != nil { + return nil, err + } + + return &cmd.Session, nil +} + +// GetSessionStats gets stats on the current & cumulative session +func (c *Client) GetSessionStats() (*SessionStats, error) { + req, err := json.Marshal(SessionCommand{Method: "session-stats"}) + if err != nil { + return nil, err + } + + resp, err := c.post(req) + if err != nil { + return nil, err + } + + var cmd SessionStatsCmd + if err := json.Unmarshal(resp, &cmd); err != nil { + return nil, err + } + + return &cmd.SessionStats, nil +} diff --git a/vendor/github.com/joho/godotenv/LICENCE b/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/joho/godotenv/README.md b/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..05c47e6 --- /dev/null +++ b/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,127 @@ +# GoDotEnv [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78 "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) + +A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file) + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc) or as a bin command. + +There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "github.com/joho/godotenv" + "log" + "os" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +_ = godotenv.Load("somerandomfile") +_ = godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +## Contributing + +Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases. + +*code changes without tests will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## CI + +Linux: [![wercker status](https://app.wercker.com/status/507594c2ec7e60f19403a568dfea0f78/m "wercker status")](https://app.wercker.com/project/bykey/507594c2ec7e60f19403a568dfea0f78) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](https://ci.appveyor.com/project/joho/godotenv) + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](http://whoisjohnbarton.com) based off the tests/fixtures in the original library. diff --git a/vendor/github.com/joho/godotenv/godotenv.go b/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..94b2676 --- /dev/null +++ b/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,229 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the github page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be avaiable through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bufio" + "errors" + "os" + "os/exec" + "strings" +) + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main) +// +// If you call Load without any args it will default to loading .env in the current path +// +// You can otherwise tell it which files to load (there can be more than one) like +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main) +// +// If you call Overload without any args it will default to loading .env in the current path +// +// You can otherwise tell it which files to load (there can be more than one) like +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run() +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string) error { + Load(filenames...) + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + for key, value := range envMap { + if os.Getenv(key) == "" || overload { + os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + envMap = make(map[string]string) + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + for _, fullLine := range lines { + if !isIgnoredLine(fullLine) { + key, value, err := parseLine(fullLine) + + if err == nil { + envMap[key] = value + } + } + } + return +} + +func parseLine(line string) (key string, value string, err error) { + if len(line) == 0 { + err = errors.New("zero length string") + return + } + + // ditch the comments (but keep quoted hashes) + if strings.Contains(line, "#") { + segmentsBetweenHashes := strings.Split(line, "#") + quotesAreOpen := false + var segmentsToKeep []string + for _, segment := range segmentsBetweenHashes { + if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { + if quotesAreOpen { + quotesAreOpen = false + segmentsToKeep = append(segmentsToKeep, segment) + } else { + quotesAreOpen = true + } + } + + if len(segmentsToKeep) == 0 || quotesAreOpen { + segmentsToKeep = append(segmentsToKeep, segment) + } + } + + line = strings.Join(segmentsToKeep, "#") + } + + // now split key from value + splitString := strings.SplitN(line, "=", 2) + + if len(splitString) != 2 { + // try yaml mode! + splitString = strings.SplitN(line, ":", 2) + } + + if len(splitString) != 2 { + err = errors.New("Can't separate key from value") + return + } + + // Parse the key + key = splitString[0] + if strings.HasPrefix(key, "export") { + key = strings.TrimPrefix(key, "export") + } + key = strings.Trim(key, " ") + + // Parse the value + value = splitString[1] + // trim + value = strings.Trim(value, " ") + + // check if we've got quoted values + if strings.Count(value, "\"") == 2 || strings.Count(value, "'") == 2 { + // pull the quotes off the edges + value = strings.Trim(value, "\"'") + + // expand quotes + value = strings.Replace(value, "\\\"", "\"", -1) + // expand newlines + value = strings.Replace(value, "\\n", "\n", -1) + } + + return +} + +func isIgnoredLine(line string) bool { + trimmedLine := strings.Trim(line, " \n\t") + return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") +} diff --git a/vendor/github.com/joho/godotenv/wercker.yml b/vendor/github.com/joho/godotenv/wercker.yml new file mode 100644 index 0000000..c716ac9 --- /dev/null +++ b/vendor/github.com/joho/godotenv/wercker.yml @@ -0,0 +1 @@ +box: pjvds/golang diff --git a/vendor/vendor.json b/vendor/vendor.json index a2ef711..c8d78b3 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -20,6 +20,12 @@ "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", "revisionTime": "2016-11-17T03:31:26Z" }, + { + "checksumSHA1": "ljZrmD7pmMXAkGNfp6IEzj31fY8=", + "path": "github.com/joho/godotenv", + "revision": "4ed13390c0acd2ff4e371e64d8b97c8954138243", + "revisionTime": "2015-09-07T01:02:28Z" + }, { "checksumSHA1": "bKMZjd2wPw13VwoE7mBeSv5djFA=", "path": "github.com/matttproud/golang_protobuf_extensions/pbutil",