05fe601479
* implemented downloaddir command * implemented uplimit and downlimit commands * moved command-related code to transmission lib
1585 lines
43 KiB
Go
1585 lines
43 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/pyed/tailer"
|
|
"github.com/pyed/transmission"
|
|
tgbotapi "gopkg.in/telegram-bot-api.v4"
|
|
)
|
|
|
|
const (
|
|
VERSION = "v1.4.1"
|
|
|
|
HELP = `
|
|
*list* or *li* or *ls*
|
|
Lists all the torrents, takes an optional argument which is a query to list only torrents that has a tracker matches the query, or some of it.
|
|
|
|
*head* or *he*
|
|
Lists the first n number of torrents, n defaults to 5 if no argument is provided.
|
|
|
|
*tail* or *ta*
|
|
Lists the last n number of torrents, n defaults to 5 if no argument is provided.
|
|
|
|
*downs* or *dg*
|
|
Lists torrents with the status of _Downloading_ or in the queue to download.
|
|
|
|
*seeding* or *sd*
|
|
Lists torrents with the status of _Seeding_ or in the queue to seed.
|
|
|
|
*paused* or *pa*
|
|
Lists _Paused_ torrents.
|
|
|
|
*checking* or *ch*
|
|
Lists torrents with the status of _Verifying_ or in the queue to verify.
|
|
|
|
*active* or *ac*
|
|
Lists torrents that are actively uploading or downloading.
|
|
|
|
*errors* or *er*
|
|
Lists torrents with with errors along with the error message.
|
|
|
|
*sort* or *so*
|
|
Manipulate the sorting of the aforementioned commands. Call it without arguments for more.
|
|
|
|
*trackers* or *tr*
|
|
Lists all the trackers along with the number of torrents.
|
|
|
|
*downloaddir* or *dd*
|
|
Set download directory to the specified path. Transmission will automatically create a
|
|
directory in case you provided an inexistent one.
|
|
|
|
*add* or *ad*
|
|
Takes one or many URLs or magnets to add them. You can send a ".torrent" file via Telegram to add it.
|
|
|
|
*search* or *se*
|
|
Takes a query and lists torrents with matching names.
|
|
|
|
*latest* or *la*
|
|
Lists the newest n torrents, n defaults to 5 if no argument is provided.
|
|
|
|
*info* or *in*
|
|
Takes one or more torrent's IDs to list more info about them.
|
|
|
|
*stop* or *sp*
|
|
Takes one or more torrent's IDs to stop them, or _all_ to stop all torrents.
|
|
|
|
*start* or *st*
|
|
Takes one or more torrent's IDs to start them, or _all_ to start all torrents.
|
|
|
|
*check* or *ck*
|
|
Takes one or more torrent's IDs to verify them, or _all_ to verify all torrents.
|
|
|
|
*del* or *rm*
|
|
Takes one or more torrent's IDs to delete them.
|
|
|
|
*deldata*
|
|
Takes one or more torrent's IDs to delete them and their data.
|
|
|
|
*stats* or *sa*
|
|
Shows Transmission's stats.
|
|
|
|
*downlimit* or *dl*
|
|
Set global limit for download speed in kilobytes.
|
|
|
|
*uplimit* or *ul*
|
|
Set global limit for upload speed in kilobytes.
|
|
|
|
*speed* or *ss*
|
|
Shows the upload and download speeds.
|
|
|
|
*count* or *co*
|
|
Shows the torrents counts per status.
|
|
|
|
*help*
|
|
Shows this help message.
|
|
|
|
*version* or *ver*
|
|
Shows version numbers.
|
|
|
|
- Prefix commands with '/' if you want to talk to your bot in a group.
|
|
- report any issues [here](https://github.com/pyed/transmission-telegram)
|
|
`
|
|
)
|
|
|
|
var (
|
|
|
|
// flags
|
|
BotToken string
|
|
Masters masterSlice
|
|
RPCURL string
|
|
Username string
|
|
Password string
|
|
LogFile string
|
|
TransLogFile string // Transmission log file
|
|
NoLive bool
|
|
|
|
// transmission
|
|
Client *transmission.TransmissionClient
|
|
|
|
// telegram
|
|
Bot *tgbotapi.BotAPI
|
|
Updates <-chan tgbotapi.Update
|
|
|
|
// chatID will be used to keep track of which chat to send completion notifictions.
|
|
chatID int64
|
|
|
|
// logging
|
|
logger = log.New(os.Stdout, "", log.LstdFlags)
|
|
|
|
// interval in seconds for live updates, affects: "active", "info", "speed", "head", "tail"
|
|
interval time.Duration = 5
|
|
// duration controls how many intervals will happen
|
|
duration = 10
|
|
|
|
// since telegram's markdown can't be escaped, we have to replace some chars
|
|
// affects only markdown users: info, active, head, tail
|
|
mdReplacer = strings.NewReplacer("*", "•",
|
|
"[", "(",
|
|
"]", ")",
|
|
"_", "-",
|
|
"`", "'")
|
|
)
|
|
|
|
// we need a type for masters for the flag package to parse them as a slice
|
|
type masterSlice []string
|
|
|
|
// String is mandatory functions for the flag package
|
|
func (masters *masterSlice) String() string {
|
|
return fmt.Sprintf("%s", *masters)
|
|
}
|
|
|
|
// Set is mandatory functions for the flag package
|
|
func (masters *masterSlice) Set(master string) error {
|
|
*masters = append(*masters, strings.ToLower(master))
|
|
return nil
|
|
}
|
|
|
|
// Contains takes a string and return true of masterSlice has it
|
|
func (masters masterSlice) Contains(master string) bool {
|
|
master = strings.ToLower(master)
|
|
for i := range masters {
|
|
if masters[i] == master {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// init flags
|
|
func init() {
|
|
// define arguments and parse them.
|
|
flag.StringVar(&BotToken, "token", "", "Telegram bot token, Can be passed via environment variable 'TT_BOTT'")
|
|
flag.Var(&Masters, "master", "Your telegram handler, So the bot will only respond to you. Can specify more than one")
|
|
flag.StringVar(&RPCURL, "url", "http://localhost:9091/transmission/rpc", "Transmission RPC URL")
|
|
flag.StringVar(&Username, "username", "", "Transmission username")
|
|
flag.StringVar(&Password, "password", "", "Transmission password")
|
|
flag.StringVar(&LogFile, "logfile", "", "Send logs to a file")
|
|
flag.StringVar(&TransLogFile, "transmission-logfile", "", "Open transmission logfile to monitor torrents completion")
|
|
flag.BoolVar(&NoLive, "no-live", false, "Don't edit and update info after sending")
|
|
|
|
// set the usage message
|
|
flag.Usage = func() {
|
|
fmt.Fprint(os.Stderr, "Usage: transmission-telegram <-token=TOKEN> <-master=@tuser> [-master=@yuser2] [-url=http://] [-username=user] [-password=pass]\n\n")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
flag.Parse()
|
|
|
|
// if we don't have BotToken passed, check the environment variable "TT_BOTT"
|
|
if BotToken == "" {
|
|
if token := os.Getenv("TT_BOTT"); len(token) > 1 {
|
|
BotToken = token
|
|
}
|
|
}
|
|
|
|
// make sure that we have the two madatory arguments: telegram token & master's handler.
|
|
if BotToken == "" ||
|
|
len(Masters) < 1 {
|
|
fmt.Fprintf(os.Stderr, "Error: Mandatory argument missing! (-token or -master)\n\n")
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
// make sure that the handler doesn't contain @
|
|
for i := range Masters {
|
|
Masters[i] = strings.Replace(Masters[i], "@", "", -1)
|
|
}
|
|
|
|
// if we got a log file, log to it
|
|
if LogFile != "" {
|
|
logf, err := os.OpenFile(LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
logger.SetOutput(logf)
|
|
}
|
|
|
|
// if we got a transmission log file, monitor it for torrents completion to notify upon them.
|
|
if TransLogFile != "" {
|
|
go func() {
|
|
ft := tailer.RunFileTailer(TransLogFile, false, nil)
|
|
|
|
// [2017-02-22 21:00:00.898] File-Name State changed from "Incomplete" to "Complete" (torrent.c:2218)
|
|
const (
|
|
substring = `"Incomplete" to "Complete"`
|
|
start = len(`[2017-02-22 21:00:00.898] `)
|
|
end = len(` State changed from "Incomplete" to "Complete" (torrent.c:2218)`)
|
|
)
|
|
|
|
for {
|
|
select {
|
|
case line := <-ft.Lines():
|
|
if strings.Contains(line, substring) {
|
|
// if we don't have a chatID continue
|
|
if chatID == 0 {
|
|
continue
|
|
}
|
|
|
|
msg := fmt.Sprintf("Completed: %s", line[start:len(line)-end])
|
|
send(msg, chatID, false)
|
|
}
|
|
case err := <-ft.Errors():
|
|
logger.Printf("[ERROR] tailing transmission log: %s", err)
|
|
return
|
|
}
|
|
|
|
}
|
|
}()
|
|
}
|
|
|
|
// if the `-username` flag isn't set, look into the environment variable 'TR_AUTH'
|
|
if Username == "" {
|
|
if values := strings.Split(os.Getenv("TR_AUTH"), ":"); len(values) > 1 {
|
|
Username, Password = values[0], values[1]
|
|
}
|
|
}
|
|
|
|
// log the flags
|
|
logger.Printf("[INFO] Token=%s\n\t\tMasters=%s\n\t\tURL=%s\n\t\tUSER=%s\n\t\tPASS=%s",
|
|
BotToken, Masters, RPCURL, Username, Password)
|
|
}
|
|
|
|
// init transmission
|
|
func init() {
|
|
var err error
|
|
Client, err = transmission.New(RPCURL, Username, Password)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Transmission: Make sure you have the right URL, Username and Password\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
}
|
|
|
|
// init telegram
|
|
func init() {
|
|
// authorize using the token
|
|
var err error
|
|
Bot, err = tgbotapi.NewBotAPI(BotToken)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Telegram: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
logger.Printf("[INFO] Authorized: %s", Bot.Self.UserName)
|
|
|
|
u := tgbotapi.NewUpdate(0)
|
|
u.Timeout = 60
|
|
|
|
Updates, err = Bot.GetUpdatesChan(u)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[ERROR] Telegram: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
for update := range Updates {
|
|
// ignore edited messages
|
|
if update.Message == nil {
|
|
continue
|
|
}
|
|
|
|
// ignore non masters
|
|
if !Masters.Contains(update.Message.From.UserName) {
|
|
logger.Printf("[INFO] Ignored a message from: %s", update.Message.From.String())
|
|
continue
|
|
}
|
|
|
|
// update chatID for complete notification
|
|
if TransLogFile != "" && chatID != update.Message.Chat.ID {
|
|
chatID = update.Message.Chat.ID
|
|
}
|
|
|
|
// tokenize the update
|
|
tokens := strings.Split(update.Message.Text, " ")
|
|
|
|
// preprocess message based on URL schema
|
|
// in case those were added from the mobile via "Share..." option
|
|
// when it is not possible to easily prepend it with "add" command
|
|
if strings.HasPrefix(tokens[0], "magnet") || strings.HasPrefix(tokens[0], "http") {
|
|
tokens = append([]string{"add"}, tokens...)
|
|
}
|
|
|
|
command := strings.ToLower(tokens[0])
|
|
|
|
switch command {
|
|
case "list", "/list", "li", "/li", "/ls", "ls":
|
|
go list(update, tokens[1:])
|
|
|
|
case "head", "/head", "he", "/he":
|
|
go head(update, tokens[1:])
|
|
|
|
case "tail", "/tail", "ta", "/ta":
|
|
go tail(update, tokens[1:])
|
|
|
|
case "downs", "/downs", "dg", "/dg":
|
|
go downs(update)
|
|
|
|
case "seeding", "/seeding", "sd", "/sd":
|
|
go seeding(update)
|
|
|
|
case "paused", "/paused", "pa", "/pa":
|
|
go paused(update)
|
|
|
|
case "checking", "/checking", "ch", "/ch":
|
|
go checking(update)
|
|
|
|
case "active", "/active", "ac", "/ac":
|
|
go active(update)
|
|
|
|
case "errors", "/errors", "er", "/er":
|
|
go errors(update)
|
|
|
|
case "sort", "/sort", "so", "/so":
|
|
go sort(update, tokens[1:])
|
|
|
|
case "trackers", "/trackers", "tr", "/tr":
|
|
go trackers(update)
|
|
|
|
case "downloaddir", "dd":
|
|
go downloaddir(update, tokens[1:])
|
|
|
|
case "add", "/add", "ad", "/ad":
|
|
go add(update, tokens[1:])
|
|
|
|
case "search", "/search", "se", "/se":
|
|
go search(update, tokens[1:])
|
|
|
|
case "latest", "/latest", "la", "/la":
|
|
go latest(update, tokens[1:])
|
|
|
|
case "info", "/info", "in", "/in":
|
|
go info(update, tokens[1:])
|
|
|
|
case "stop", "/stop", "sp", "/sp":
|
|
go stop(update, tokens[1:])
|
|
|
|
case "start", "/start", "st", "/st":
|
|
go start(update, tokens[1:])
|
|
|
|
case "check", "/check", "ck", "/ck":
|
|
go check(update, tokens[1:])
|
|
|
|
case "stats", "/stats", "sa", "/sa":
|
|
go stats(update)
|
|
|
|
case "downlimit", "dl":
|
|
go downlimit(update, tokens[1:])
|
|
|
|
case "uplimit", "ul":
|
|
go uplimit(update, tokens[1:])
|
|
|
|
case "speed", "/speed", "ss", "/ss":
|
|
go speed(update)
|
|
|
|
case "count", "/count", "co", "/co":
|
|
go count(update)
|
|
|
|
case "del", "/del", "rm", "/rm":
|
|
go del(update, tokens[1:])
|
|
|
|
case "deldata", "/deldata":
|
|
go deldata(update, tokens[1:])
|
|
|
|
case "help", "/help":
|
|
go send(HELP, update.Message.Chat.ID, true)
|
|
|
|
case "version", "/version", "ver", "/ver":
|
|
go getVersion(update)
|
|
|
|
case "":
|
|
// might be a file received
|
|
go receiveTorrent(update)
|
|
|
|
default:
|
|
// no such command, try help
|
|
go send("No such command, try /help", update.Message.Chat.ID, false)
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// list will form and send a list of all the torrents
|
|
// takes an optional argument which is a query to match against trackers
|
|
// to list only torrents that has a tracker that matchs.
|
|
func list(ud tgbotapi.Update, tokens []string) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*list:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
// if it gets a query, it will list torrents that has trackers that match the query
|
|
if len(tokens) != 0 {
|
|
// (?i) for case insensitivity
|
|
regx, err := regexp.Compile("(?i)" + tokens[0])
|
|
if err != nil {
|
|
send("*list:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
for i := range torrents {
|
|
if regx.MatchString(torrents[i].GetTrackers()) {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
|
|
}
|
|
}
|
|
} else { // if we did not get a query, list all torrents
|
|
for i := range torrents {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
|
|
}
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
// if we got a tracker query show different message
|
|
if len(tokens) != 0 {
|
|
send(fmt.Sprintf("*list:* No tracker matches: *%s*", tokens[0]), ud.Message.Chat.ID, true)
|
|
return
|
|
}
|
|
send("*list:* no torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// head will list the first 5 or n torrents
|
|
func head(ud tgbotapi.Update, tokens []string) {
|
|
var (
|
|
n = 5 // default to 5
|
|
err error
|
|
)
|
|
|
|
if len(tokens) > 0 {
|
|
n, err = strconv.Atoi(tokens[0])
|
|
if err != nil {
|
|
send("*head:* argument must be a number", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
}
|
|
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*head:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// make sure that we stay in the boundaries
|
|
if n <= 0 || n > len(torrents) {
|
|
n = len(torrents)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents[:n] {
|
|
torrentName := mdReplacer.Replace(torrents[i].Name) // escape markdown
|
|
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
|
|
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
|
|
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, humanize.Bytes(torrents[i].RateDownload),
|
|
humanize.Bytes(torrents[i].RateUpload), torrents[i].Ratio()))
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
send("*head:* no torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
msgID := send(buf.String(), ud.Message.Chat.ID, true)
|
|
|
|
if NoLive {
|
|
return
|
|
}
|
|
|
|
// keep the info live
|
|
for i := 0; i < duration; i++ {
|
|
time.Sleep(time.Second * interval)
|
|
buf.Reset()
|
|
|
|
torrents, err = Client.GetTorrents()
|
|
if err != nil {
|
|
continue // try again if some error heppened
|
|
}
|
|
|
|
if len(torrents) < 1 {
|
|
continue
|
|
}
|
|
|
|
// make sure that we stay in the boundaries
|
|
if n <= 0 || n > len(torrents) {
|
|
n = len(torrents)
|
|
}
|
|
|
|
for _, torrent := range torrents[:n] {
|
|
torrentName := mdReplacer.Replace(torrent.Name) // escape markdown
|
|
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
|
|
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()),
|
|
humanize.Bytes(torrent.SizeWhenDone), torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload),
|
|
humanize.Bytes(torrent.RateUpload), torrent.Ratio()))
|
|
}
|
|
|
|
// no need to check if it is empty, as if the buffer is empty telegram won't change the message
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
|
|
editConf.ParseMode = tgbotapi.ModeMarkdown
|
|
Bot.Send(editConf)
|
|
}
|
|
|
|
}
|
|
|
|
// tail lists the last 5 or n torrents
|
|
func tail(ud tgbotapi.Update, tokens []string) {
|
|
var (
|
|
n = 5 // default to 5
|
|
err error
|
|
)
|
|
|
|
if len(tokens) > 0 {
|
|
n, err = strconv.Atoi(tokens[0])
|
|
if err != nil {
|
|
send("*tail:* argument must be a number", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
}
|
|
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*tail:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// make sure that we stay in the boundaries
|
|
if n <= 0 || n > len(torrents) {
|
|
n = len(torrents)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for _, torrent := range torrents[len(torrents)-n:] {
|
|
torrentName := mdReplacer.Replace(torrent.Name) // escape markdown
|
|
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
|
|
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()),
|
|
humanize.Bytes(torrent.SizeWhenDone), torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload),
|
|
humanize.Bytes(torrent.RateUpload), torrent.Ratio()))
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
send("*tail:* no torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
msgID := send(buf.String(), ud.Message.Chat.ID, true)
|
|
|
|
if NoLive {
|
|
return
|
|
}
|
|
|
|
// keep the info live
|
|
for i := 0; i < duration; i++ {
|
|
time.Sleep(time.Second * interval)
|
|
buf.Reset()
|
|
|
|
torrents, err = Client.GetTorrents()
|
|
if err != nil {
|
|
continue // try again if some error heppened
|
|
}
|
|
|
|
if len(torrents) < 1 {
|
|
continue
|
|
}
|
|
|
|
// make sure that we stay in the boundaries
|
|
if n <= 0 || n > len(torrents) {
|
|
n = len(torrents)
|
|
}
|
|
|
|
for _, torrent := range torrents[len(torrents)-n:] {
|
|
torrentName := mdReplacer.Replace(torrent.Name) // escape markdown
|
|
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
|
|
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()),
|
|
humanize.Bytes(torrent.SizeWhenDone), torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload),
|
|
humanize.Bytes(torrent.RateUpload), torrent.Ratio()))
|
|
}
|
|
|
|
// no need to check if it is empty, as if the buffer is empty telegram won't change the message
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
|
|
editConf.ParseMode = tgbotapi.ModeMarkdown
|
|
Bot.Send(editConf)
|
|
}
|
|
|
|
}
|
|
|
|
// downs will send the names of torrents with status 'Downloading' or in queue to
|
|
func downs(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*downs:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents {
|
|
// Downloading or in queue to download
|
|
if torrents[i].Status == transmission.StatusDownloading ||
|
|
torrents[i].Status == transmission.StatusDownloadPending {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
|
|
}
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
send("No downloads", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// seeding will send the names of the torrents with the status 'Seeding' or in the queue to
|
|
func seeding(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*seeding:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents {
|
|
if torrents[i].Status == transmission.StatusSeeding ||
|
|
torrents[i].Status == transmission.StatusSeedPending {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
|
|
}
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
send("No torrents seeding", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
|
|
}
|
|
|
|
// paused will send the names of the torrents with status 'Paused'
|
|
func paused(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*paused:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents {
|
|
if torrents[i].Status == transmission.StatusStopped {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n%s (%.1f%%) DL: %s UL: %s R: %s\n\n",
|
|
torrents[i].ID, torrents[i].Name, torrents[i].TorrentStatus(),
|
|
torrents[i].PercentDone*100, humanize.Bytes(torrents[i].DownloadedEver),
|
|
humanize.Bytes(torrents[i].UploadedEver), torrents[i].Ratio()))
|
|
}
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
send("No paused torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// checking will send the names of torrents with the status 'verifying' or in the queue to
|
|
func checking(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*checking:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents {
|
|
if torrents[i].Status == transmission.StatusChecking ||
|
|
torrents[i].Status == transmission.StatusCheckPending {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n%s (%.1f%%)\n\n",
|
|
torrents[i].ID, torrents[i].Name, torrents[i].TorrentStatus(),
|
|
torrents[i].PercentDone*100))
|
|
|
|
}
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
send("No torrents verifying", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// active will send torrents that are actively downloading or uploading
|
|
func active(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*active:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents {
|
|
if torrents[i].RateDownload > 0 ||
|
|
torrents[i].RateUpload > 0 {
|
|
// escape markdown
|
|
torrentName := mdReplacer.Replace(torrents[i].Name)
|
|
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
|
|
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
|
|
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, humanize.Bytes(torrents[i].RateDownload),
|
|
humanize.Bytes(torrents[i].RateUpload), torrents[i].Ratio()))
|
|
}
|
|
}
|
|
if buf.Len() == 0 {
|
|
send("No active torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
msgID := send(buf.String(), ud.Message.Chat.ID, true)
|
|
|
|
if NoLive {
|
|
return
|
|
}
|
|
|
|
// keep the active list live for 'duration * interval'
|
|
for i := 0; i < duration; i++ {
|
|
time.Sleep(time.Second * interval)
|
|
// reset the buffer to reuse it
|
|
buf.Reset()
|
|
|
|
// update torrents
|
|
torrents, err = Client.GetTorrents()
|
|
if err != nil {
|
|
continue // if there was error getting torrents, skip to the next iteration
|
|
}
|
|
|
|
// do the same loop again
|
|
for i := range torrents {
|
|
if torrents[i].RateDownload > 0 ||
|
|
torrents[i].RateUpload > 0 {
|
|
torrentName := mdReplacer.Replace(torrents[i].Name) // replace markdown chars
|
|
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
|
|
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
|
|
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, humanize.Bytes(torrents[i].RateDownload),
|
|
humanize.Bytes(torrents[i].RateUpload), torrents[i].Ratio()))
|
|
}
|
|
}
|
|
|
|
// no need to check if it is empty, as if the buffer is empty telegram won't change the message
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
|
|
editConf.ParseMode = tgbotapi.ModeMarkdown
|
|
Bot.Send(editConf)
|
|
}
|
|
// sleep one more time before putting the dashes
|
|
time.Sleep(time.Second * interval)
|
|
|
|
// replace the speed with dashes to indicate that we are done being live
|
|
buf.Reset()
|
|
for i := range torrents {
|
|
if torrents[i].RateDownload > 0 ||
|
|
torrents[i].RateUpload > 0 {
|
|
// escape markdown
|
|
torrentName := mdReplacer.Replace(torrents[i].Name)
|
|
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *-* ↑ *-* R: *%s*\n\n",
|
|
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
|
|
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, torrents[i].Ratio()))
|
|
}
|
|
}
|
|
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
|
|
editConf.ParseMode = tgbotapi.ModeMarkdown
|
|
Bot.Send(editConf)
|
|
|
|
}
|
|
|
|
// errors will send torrents with errors
|
|
func errors(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*errors:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents {
|
|
if torrents[i].Error != 0 {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n%s\n",
|
|
torrents[i].ID, torrents[i].Name, torrents[i].ErrorString))
|
|
}
|
|
}
|
|
if buf.Len() == 0 {
|
|
send("No errors", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// sort changes torrents sorting
|
|
func sort(ud tgbotapi.Update, tokens []string) {
|
|
if len(tokens) == 0 {
|
|
send(`*sort* takes one of:
|
|
(*id, name, age, size, progress, downspeed, upspeed, download, upload, ratio*)
|
|
optionally start with (*rev*) for reversed order
|
|
e.g. "*sort rev size*" to get biggest torrents first.`, ud.Message.Chat.ID, true)
|
|
return
|
|
}
|
|
|
|
var reversed bool
|
|
if strings.ToLower(tokens[0]) == "rev" {
|
|
reversed = true
|
|
tokens = tokens[1:]
|
|
}
|
|
|
|
switch strings.ToLower(tokens[0]) {
|
|
case "id":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevID)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortID)
|
|
case "name":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevName)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortName)
|
|
case "age":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevAge)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortAge)
|
|
case "size":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevSize)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortSize)
|
|
case "progress":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevProgress)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortProgress)
|
|
case "downspeed":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevDownSpeed)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortDownSpeed)
|
|
case "upspeed":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevUpSpeed)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortUpSpeed)
|
|
case "download":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevDownloaded)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortDownloaded)
|
|
case "upload":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevUploaded)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortUploaded)
|
|
case "ratio":
|
|
if reversed {
|
|
Client.SetSort(transmission.SortRevRatio)
|
|
break
|
|
}
|
|
Client.SetSort(transmission.SortRatio)
|
|
default:
|
|
send("unkown sorting method", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
if reversed {
|
|
send("*sort:* reversed "+tokens[0], ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send("*sort:* "+tokens[0], ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
var trackerRegex = regexp.MustCompile(`[https?|udp]://([^:/]*)`)
|
|
|
|
// trackers will send a list of trackers and how many torrents each one has
|
|
func trackers(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*trackers:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
trackers := make(map[string]int)
|
|
|
|
for i := range torrents {
|
|
for _, tracker := range torrents[i].Trackers {
|
|
sm := trackerRegex.FindSubmatch([]byte(tracker.Announce))
|
|
if len(sm) > 1 {
|
|
currentTracker := string(sm[1])
|
|
n, ok := trackers[currentTracker]
|
|
if !ok {
|
|
trackers[currentTracker] = 1
|
|
continue
|
|
}
|
|
trackers[currentTracker] = n + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for k, v := range trackers {
|
|
buf.WriteString(fmt.Sprintf("%d - %s\n", v, k))
|
|
}
|
|
|
|
if buf.Len() == 0 {
|
|
send("No trackers!", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// downloaddir takes a path and sets it as the download directory
|
|
func downloaddir(ud tgbotapi.Update, tokens []string) {
|
|
if len(tokens) < 1 {
|
|
send("Please, specify a path for downloaddir", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
downloadDir := tokens[0]
|
|
|
|
cmd := transmission.NewSessionSetCommand()
|
|
cmd.SetDownloadDir(downloadDir)
|
|
|
|
out, err := Client.ExecuteCommand(cmd)
|
|
if err != nil {
|
|
send("*downloaddir:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
if out.Result != "success" {
|
|
send("*downloaddir:* "+out.Result, ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send(
|
|
"*downloaddir:* downloaddir has been successfully changed to"+downloadDir,
|
|
ud.Message.Chat.ID, false,
|
|
)
|
|
}
|
|
|
|
// add takes an URL to a .torrent file to add it to transmission
|
|
func add(ud tgbotapi.Update, tokens []string) {
|
|
if len(tokens) == 0 {
|
|
send("*add:* needs at least one URL", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// loop over the URL/s and add them
|
|
for _, url := range tokens {
|
|
cmd := transmission.NewAddCmdByURL(url)
|
|
|
|
torrent, err := Client.ExecuteAddCommand(cmd)
|
|
if err != nil {
|
|
send("*add:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
|
|
// check if torrent.Name is empty, then an error happened
|
|
if torrent.Name == "" {
|
|
send("*add:* error adding "+url, ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
send(fmt.Sprintf("*Added:* <%d> %s", torrent.ID, torrent.Name), ud.Message.Chat.ID, false)
|
|
}
|
|
}
|
|
|
|
// receiveTorrent gets an update that potentially has a .torrent file to add
|
|
func receiveTorrent(ud tgbotapi.Update) {
|
|
if ud.Message.Document == nil {
|
|
return // has no document
|
|
}
|
|
|
|
// get the file ID and make the config
|
|
fconfig := tgbotapi.FileConfig{
|
|
FileID: ud.Message.Document.FileID,
|
|
}
|
|
file, err := Bot.GetFile(fconfig)
|
|
if err != nil {
|
|
send("*receiver:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// add by file URL
|
|
add(ud, []string{file.Link(BotToken)})
|
|
}
|
|
|
|
// search takes a query and returns torrents with match
|
|
func search(ud tgbotapi.Update, tokens []string) {
|
|
// make sure that we got a query
|
|
if len(tokens) == 0 {
|
|
send("*search:* needs an argument", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
query := strings.Join(tokens, " ")
|
|
// "(?i)" for case insensitivity
|
|
regx, err := regexp.Compile("(?i)" + query)
|
|
if err != nil {
|
|
send("*search:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*search:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents {
|
|
if regx.MatchString(torrents[i].Name) {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
|
|
}
|
|
}
|
|
if buf.Len() == 0 {
|
|
send("No matches!", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// latest takes n and returns the latest n torrents
|
|
func latest(ud tgbotapi.Update, tokens []string) {
|
|
var (
|
|
n = 5 // default to 5
|
|
err error
|
|
)
|
|
|
|
if len(tokens) > 0 {
|
|
n, err = strconv.Atoi(tokens[0])
|
|
if err != nil {
|
|
send("*latest:* argument must be a number", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
}
|
|
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*latest:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// make sure that we stay in the boundaries
|
|
if n <= 0 || n > len(torrents) {
|
|
n = len(torrents)
|
|
}
|
|
|
|
// sort by age, and set reverse to true to get the latest first
|
|
torrents.SortAge(true)
|
|
|
|
buf := new(bytes.Buffer)
|
|
for i := range torrents[:n] {
|
|
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
|
|
}
|
|
if buf.Len() == 0 {
|
|
send("*latest:* No torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(buf.String(), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
// info takes an id of a torrent and returns some info about it
|
|
func info(ud tgbotapi.Update, tokens []string) {
|
|
if len(tokens) == 0 {
|
|
send("*info:* needs a torrent ID number", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
for _, id := range tokens {
|
|
torrentID, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*info:* %s is not a number", id), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
|
|
// get the torrent
|
|
torrent, err := Client.GetTorrent(torrentID)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*info:* Can't find a torrent with an ID of %d", torrentID), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
|
|
// get the trackers using 'trackerRegex'
|
|
var trackers string
|
|
for _, tracker := range torrent.Trackers {
|
|
sm := trackerRegex.FindSubmatch([]byte(tracker.Announce))
|
|
if len(sm) > 1 {
|
|
trackers += string(sm[1]) + " "
|
|
}
|
|
}
|
|
|
|
// format the info
|
|
torrentName := mdReplacer.Replace(torrent.Name) // escape markdown
|
|
info := fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\nDL: *%s* UP: *%s*\nAdded: *%s*, ETA: *%s*\nTrackers: `%s`",
|
|
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()), humanize.Bytes(torrent.SizeWhenDone),
|
|
torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload), humanize.Bytes(torrent.RateUpload), torrent.Ratio(),
|
|
humanize.Bytes(torrent.DownloadedEver), humanize.Bytes(torrent.UploadedEver), time.Unix(torrent.AddedDate, 0).Format(time.Stamp),
|
|
torrent.ETA(), trackers)
|
|
|
|
// send it
|
|
msgID := send(info, ud.Message.Chat.ID, true)
|
|
|
|
if NoLive {
|
|
return
|
|
}
|
|
|
|
// this go-routine will make the info live for 'duration * interval'
|
|
go func(torrentID, msgID int) {
|
|
for i := 0; i < duration; i++ {
|
|
time.Sleep(time.Second * interval)
|
|
torrent, err = Client.GetTorrent(torrentID)
|
|
if err != nil {
|
|
continue // skip this iteration if there's an error retrieving the torrent's info
|
|
}
|
|
|
|
torrentName := mdReplacer.Replace(torrent.Name)
|
|
info := fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\nDL: *%s* UP: *%s*\nAdded: *%s*, ETA: *%s*\nTrackers: `%s`",
|
|
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()), humanize.Bytes(torrent.SizeWhenDone),
|
|
torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload), humanize.Bytes(torrent.RateUpload), torrent.Ratio(),
|
|
humanize.Bytes(torrent.DownloadedEver), humanize.Bytes(torrent.UploadedEver), time.Unix(torrent.AddedDate, 0).Format(time.Stamp),
|
|
torrent.ETA(), trackers)
|
|
|
|
// update the message
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, info)
|
|
editConf.ParseMode = tgbotapi.ModeMarkdown
|
|
Bot.Send(editConf)
|
|
|
|
}
|
|
// sleep one more time before the dashes
|
|
time.Sleep(time.Second * interval)
|
|
|
|
// at the end write dashes to indicate that we are done being live.
|
|
torrentName := mdReplacer.Replace(torrent.Name)
|
|
info := fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *- B* ↑ *- B* R: *%s*\nDL: *%s* UP: *%s*\nAdded: *%s*, ETA: *-*\nTrackers: `%s`",
|
|
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()), humanize.Bytes(torrent.SizeWhenDone),
|
|
torrent.PercentDone*100, torrent.Ratio(), humanize.Bytes(torrent.DownloadedEver), humanize.Bytes(torrent.UploadedEver),
|
|
time.Unix(torrent.AddedDate, 0).Format(time.Stamp), trackers)
|
|
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, info)
|
|
editConf.ParseMode = tgbotapi.ModeMarkdown
|
|
Bot.Send(editConf)
|
|
}(torrentID, msgID)
|
|
}
|
|
}
|
|
|
|
// stop takes id[s] of torrent[s] or 'all' to stop them
|
|
func stop(ud tgbotapi.Update, tokens []string) {
|
|
// make sure that we got at least one argument
|
|
if len(tokens) == 0 {
|
|
send("*stop:* needs an argument", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// if the first argument is 'all' then stop all torrents
|
|
if tokens[0] == "all" {
|
|
if err := Client.StopAll(); err != nil {
|
|
send("*stop:* error occurred while stopping some torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send("Stopped all torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
for _, id := range tokens {
|
|
num, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*stop:* %s is not a number", id), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
status, err := Client.StopTorrent(num)
|
|
if err != nil {
|
|
send("*stop:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
|
|
torrent, err := Client.GetTorrent(num)
|
|
if err != nil {
|
|
send(fmt.Sprintf("[fail] *stop:* No torrent with an ID of %d", num), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(fmt.Sprintf("[%s] *stop:* %s", status, torrent.Name), ud.Message.Chat.ID, false)
|
|
}
|
|
}
|
|
|
|
// start takes id[s] of torrent[s] or 'all' to start them
|
|
func start(ud tgbotapi.Update, tokens []string) {
|
|
// make sure that we got at least one argument
|
|
if len(tokens) == 0 {
|
|
send("*start:* needs an argument", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// if the first argument is 'all' then start all torrents
|
|
if tokens[0] == "all" {
|
|
if err := Client.StartAll(); err != nil {
|
|
send("*start:* error occurred while starting some torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send("Started all torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
|
|
}
|
|
|
|
for _, id := range tokens {
|
|
num, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*start:* %s is not a number", id), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
status, err := Client.StartTorrent(num)
|
|
if err != nil {
|
|
send("*start:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
|
|
torrent, err := Client.GetTorrent(num)
|
|
if err != nil {
|
|
send(fmt.Sprintf("[fail] *start:* No torrent with an ID of %d", num), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(fmt.Sprintf("[%s] *start:* %s", status, torrent.Name), ud.Message.Chat.ID, false)
|
|
}
|
|
}
|
|
|
|
// check takes id[s] of torrent[s] or 'all' to verify them
|
|
func check(ud tgbotapi.Update, tokens []string) {
|
|
// make sure that we got at least one argument
|
|
if len(tokens) == 0 {
|
|
send("*check:* needs an argument", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// if the first argument is 'all' then start all torrents
|
|
if tokens[0] == "all" {
|
|
if err := Client.VerifyAll(); err != nil {
|
|
send("*check:* error occurred while verifying some torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send("Verifying all torrents", ud.Message.Chat.ID, false)
|
|
return
|
|
|
|
}
|
|
|
|
for _, id := range tokens {
|
|
num, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*check:* %s is not a number", id), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
status, err := Client.VerifyTorrent(num)
|
|
if err != nil {
|
|
send("*check:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
continue
|
|
}
|
|
|
|
torrent, err := Client.GetTorrent(num)
|
|
if err != nil {
|
|
send(fmt.Sprintf("[fail] *check:* No torrent with an ID of %d", num), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
send(fmt.Sprintf("[%s] *check:* %s", status, torrent.Name), ud.Message.Chat.ID, false)
|
|
}
|
|
|
|
}
|
|
|
|
// stats echo back transmission stats
|
|
func stats(ud tgbotapi.Update) {
|
|
stats, err := Client.GetStats()
|
|
if err != nil {
|
|
send("*stats:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
msg := fmt.Sprintf(
|
|
`
|
|
Total: *%d*
|
|
Active: *%d*
|
|
Paused: *%d*
|
|
|
|
_Current Stats_
|
|
Downloaded: *%s*
|
|
Uploaded: *%s*
|
|
Running time: *%s*
|
|
|
|
_Accumulative Stats_
|
|
Sessions: *%d*
|
|
Downloaded: *%s*
|
|
Uploaded: *%s*
|
|
Total Running time: *%s*
|
|
`,
|
|
|
|
stats.TorrentCount,
|
|
stats.ActiveTorrentCount,
|
|
stats.PausedTorrentCount,
|
|
humanize.Bytes(stats.CurrentStats.DownloadedBytes),
|
|
humanize.Bytes(stats.CurrentStats.UploadedBytes),
|
|
stats.CurrentActiveTime(),
|
|
stats.CumulativeStats.SessionCount,
|
|
humanize.Bytes(stats.CumulativeStats.DownloadedBytes),
|
|
humanize.Bytes(stats.CumulativeStats.UploadedBytes),
|
|
stats.CumulativeActiveTime(),
|
|
)
|
|
|
|
send(msg, ud.Message.Chat.ID, true)
|
|
}
|
|
|
|
// downlimit sets the global downlimit to a provided value in kilobytes
|
|
func downlimit(ud tgbotapi.Update, tokens []string) {
|
|
speedLimit(ud, tokens, transmission.DownloadLimitType)
|
|
}
|
|
|
|
// uplimit sets the global uplimit to a provided value in kilobytes
|
|
func uplimit(ud tgbotapi.Update, tokens []string) {
|
|
speedLimit(ud, tokens, transmission.UploadLimitType)
|
|
}
|
|
|
|
// speedLimit sets either a donwload or upload limit
|
|
func speedLimit(ud tgbotapi.Update, tokens []string, limitType transmission.SpeedLimitType) {
|
|
if len(tokens) < 1 {
|
|
send("Please, specify the limit", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
limit, err := strconv.ParseUint(tokens[0], 10, 32)
|
|
if err != nil {
|
|
send("Please, specify the limit as number of kilobytes", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
speedLimitCmd := transmission.NewSpeedLimitCommand(limitType, uint(limit))
|
|
if speedLimitCmd == nil {
|
|
send(fmt.Sprintf("*%s:* internal error", limitType), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
out, err := Client.ExecuteCommand(speedLimitCmd)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*%s:* %v", limitType, err.Error()), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
if out.Result != "success" {
|
|
send(fmt.Sprintf("*%s:* %v", limitType, out.Result), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send(
|
|
fmt.Sprintf("*%s:* limit has been successfully changed to %d KB/s", limitType, limit),
|
|
ud.Message.Chat.ID, false,
|
|
)
|
|
}
|
|
|
|
// speed will echo back the current download and upload speeds
|
|
func speed(ud tgbotapi.Update) {
|
|
stats, err := Client.GetStats()
|
|
if err != nil {
|
|
send("*speed:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
msg := fmt.Sprintf("↓ %s ↑ %s", humanize.Bytes(stats.DownloadSpeed), humanize.Bytes(stats.UploadSpeed))
|
|
|
|
msgID := send(msg, ud.Message.Chat.ID, false)
|
|
|
|
if NoLive {
|
|
return
|
|
}
|
|
|
|
for i := 0; i < duration; i++ {
|
|
time.Sleep(time.Second * interval)
|
|
stats, err = Client.GetStats()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
msg = fmt.Sprintf("↓ %s ↑ %s", humanize.Bytes(stats.DownloadSpeed), humanize.Bytes(stats.UploadSpeed))
|
|
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, msg)
|
|
Bot.Send(editConf)
|
|
time.Sleep(time.Second * interval)
|
|
}
|
|
// sleep one more time before switching to dashes
|
|
time.Sleep(time.Second * interval)
|
|
|
|
// show dashes to indicate that we are done updating.
|
|
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, "↓ - B ↑ - B")
|
|
Bot.Send(editConf)
|
|
}
|
|
|
|
// count returns current torrents count per status
|
|
func count(ud tgbotapi.Update) {
|
|
torrents, err := Client.GetTorrents()
|
|
if err != nil {
|
|
send("*count:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
var downloading, seeding, stopped, checking, downloadingQ, seedingQ, checkingQ int
|
|
|
|
for i := range torrents {
|
|
switch torrents[i].Status {
|
|
case transmission.StatusDownloading:
|
|
downloading++
|
|
case transmission.StatusSeeding:
|
|
seeding++
|
|
case transmission.StatusStopped:
|
|
stopped++
|
|
case transmission.StatusChecking:
|
|
checking++
|
|
case transmission.StatusDownloadPending:
|
|
downloadingQ++
|
|
case transmission.StatusSeedPending:
|
|
seedingQ++
|
|
case transmission.StatusCheckPending:
|
|
checkingQ++
|
|
}
|
|
}
|
|
|
|
msg := fmt.Sprintf("Downloading: %d\nSeeding: %d\nPaused: %d\nVerifying: %d\n\n- Waiting to -\nDownload: %d\nSeed: %d\nVerify: %d\n\nTotal: %d",
|
|
downloading, seeding, stopped, checking, downloadingQ, seedingQ, checkingQ, len(torrents))
|
|
|
|
send(msg, ud.Message.Chat.ID, false)
|
|
|
|
}
|
|
|
|
// del takes an id or more, and delete the corresponding torrent/s
|
|
func del(ud tgbotapi.Update, tokens []string) {
|
|
// make sure that we got an argument
|
|
if len(tokens) == 0 {
|
|
send("*del:* needs an ID", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
// loop over tokens to read each potential id
|
|
for _, id := range tokens {
|
|
num, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*del:* %s is not an ID", id), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
name, err := Client.DeleteTorrent(num, false)
|
|
if err != nil {
|
|
send("*del:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send("*Deleted:* "+name, ud.Message.Chat.ID, false)
|
|
}
|
|
}
|
|
|
|
// deldata takes an id or more, and delete the corresponding torrent/s with their data
|
|
func deldata(ud tgbotapi.Update, tokens []string) {
|
|
// make sure that we got an argument
|
|
if len(tokens) == 0 {
|
|
send("*deldata:* needs an ID", ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
// loop over tokens to read each potential id
|
|
for _, id := range tokens {
|
|
num, err := strconv.Atoi(id)
|
|
if err != nil {
|
|
send(fmt.Sprintf("*deldata:* %s is not an ID", id), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
name, err := Client.DeleteTorrent(num, true)
|
|
if err != nil {
|
|
send("*deldata:* "+err.Error(), ud.Message.Chat.ID, false)
|
|
return
|
|
}
|
|
|
|
send("Deleted with data: "+name, ud.Message.Chat.ID, false)
|
|
}
|
|
}
|
|
|
|
// getVersion sends transmission version + transmission-telegram version
|
|
func getVersion(ud tgbotapi.Update) {
|
|
send(fmt.Sprintf("Transmission *%s*\nTransmission-telegram *%s*", Client.Version(), VERSION), ud.Message.Chat.ID, true)
|
|
}
|
|
|
|
// send takes a chat id and a message to send, returns the message id of the send message
|
|
func send(text string, chatID int64, markdown bool) int {
|
|
// set typing action
|
|
action := tgbotapi.NewChatAction(chatID, tgbotapi.ChatTyping)
|
|
Bot.Send(action)
|
|
|
|
// check the rune count, telegram is limited to 4096 chars per message;
|
|
// so if our message is > 4096, split it in chunks the send them.
|
|
msgRuneCount := utf8.RuneCountInString(text)
|
|
LenCheck:
|
|
stop := 4095
|
|
if msgRuneCount > 4096 {
|
|
for text[stop] != 10 { // '\n'
|
|
stop--
|
|
}
|
|
msg := tgbotapi.NewMessage(chatID, text[:stop])
|
|
msg.DisableWebPagePreview = true
|
|
if markdown {
|
|
msg.ParseMode = tgbotapi.ModeMarkdown
|
|
}
|
|
|
|
// send current chunk
|
|
if _, err := Bot.Send(msg); err != nil {
|
|
logger.Printf("[ERROR] Send: %s", err)
|
|
}
|
|
// move to the next chunk
|
|
text = text[stop:]
|
|
msgRuneCount = utf8.RuneCountInString(text)
|
|
goto LenCheck
|
|
}
|
|
|
|
// if msgRuneCount < 4096, send it normally
|
|
msg := tgbotapi.NewMessage(chatID, text)
|
|
msg.DisableWebPagePreview = true
|
|
if markdown {
|
|
msg.ParseMode = tgbotapi.ModeMarkdown
|
|
}
|
|
|
|
resp, err := Bot.Send(msg)
|
|
if err != nil {
|
|
logger.Printf("[ERROR] Send: %s", err)
|
|
}
|
|
|
|
return resp.MessageID
|
|
}
|