autobrr/internal/action/transmission.go
ze0s 522f22db46
feat(clients): Transmission set label (#1204)
* feat(clients): Transmission set label

* fix(clients): Transmission fmt

* fix(clients): Transmission set label before reannounce
2023-10-23 19:02:04 +02:00

263 lines
7.5 KiB
Go

// Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
package action
import (
"context"
"fmt"
"strings"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/hekmon/transmissionrpc/v2"
)
const (
ReannounceMaxAttempts = 50
ReannounceInterval = 7 // interval in seconds
)
var ErrReannounceTookTooLong = errors.New("ErrReannounceTookTooLong")
func (s *service) transmission(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
s.log.Debug().Msgf("action Transmission: %s", action.Name)
var err error
// get client for action
client, err := s.clientSvc.FindByID(ctx, action.ClientID)
if err != nil {
s.log.Error().Stack().Err(err).Msgf("error finding client: %d", action.ClientID)
return nil, err
}
if client == nil {
return nil, errors.New("could not find client by id: %d", action.ClientID)
}
tbt, err := transmissionrpc.New(client.Host, client.Username, client.Password, &transmissionrpc.AdvancedConfig{
HTTPS: client.TLS,
Port: uint16(client.Port),
UserAgent: "autobrr",
})
if err != nil {
return nil, errors.Wrap(err, "error logging into client: %s", client.Host)
}
rejections, err := s.transmissionCheckRulesCanDownload(ctx, action, client, tbt)
if err != nil {
return nil, errors.Wrap(err, "error checking client rules: %s", action.Name)
}
if len(rejections) > 0 {
return rejections, nil
}
payload := transmissionrpc.TorrentAddPayload{}
if action.SavePath != "" {
payload.DownloadDir = &action.SavePath
}
if action.Paused {
payload.Paused = &action.Paused
}
if release.HasMagnetUri() {
payload.Filename = &release.MagnetURI
// Prepare and send payload
torrent, err := tbt.TorrentAdd(ctx, payload)
if err != nil {
return nil, errors.Wrap(err, "could not add torrent from magnet %s to client: %s", release.MagnetURI, client.Host)
}
if action.Label != "" {
p := transmissionrpc.TorrentSetPayload{
IDs: []int64{*torrent.ID},
Labels: []string{action.Label},
}
if err := tbt.TorrentSet(ctx, p); err != nil {
return nil, errors.Wrap(err, "could not set label for hash %s to client: %s", *torrent.HashString, client.Host)
}
s.log.Debug().Msgf("set label for torrent hash %s successful to client: '%s'", *torrent.HashString, client.Name)
}
s.log.Info().Msgf("torrent from magnet with hash %v successfully added to client: '%s'", torrent.HashString, client.Name)
return nil, nil
}
if release.TorrentTmpFile == "" {
if err := release.DownloadTorrentFileCtx(ctx); err != nil {
s.log.Error().Err(err).Msgf("could not download torrent file for release: %s", release.TorrentName)
return nil, err
}
}
b64, err := transmissionrpc.File2Base64(release.TorrentTmpFile)
if err != nil {
return nil, errors.Wrap(err, "cant encode file %s into base64", release.TorrentTmpFile)
}
payload.MetaInfo = &b64
// Prepare and send payload
torrent, err := tbt.TorrentAdd(ctx, payload)
if err != nil {
return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.TorrentTmpFile, client.Host)
}
if action.Label != "" {
p := transmissionrpc.TorrentSetPayload{
IDs: []int64{*torrent.ID},
Labels: []string{action.Label},
}
if err := tbt.TorrentSet(ctx, p); err != nil {
return nil, errors.Wrap(err, "could not set label for hash %s to client: %s", *torrent.HashString, client.Host)
}
s.log.Debug().Msgf("set label for torrent hash %s successful to client: '%s'", *torrent.HashString, client.Name)
}
if !action.Paused && !action.ReAnnounceSkip {
if err := s.transmissionReannounce(ctx, action, tbt, *torrent.ID); err != nil {
if errors.Is(err, ErrReannounceTookTooLong) {
return []string{fmt.Sprintf("reannounce took too long for torrent: %s, deleted", *torrent.HashString)}, nil
}
return nil, errors.Wrap(err, "could not reannounce torrent: %s", *torrent.HashString)
}
return nil, nil
}
s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", *torrent.HashString, client.Name)
return rejections, nil
}
func (s *service) transmissionReannounce(ctx context.Context, action *domain.Action, tbt *transmissionrpc.Client, torrentId int64) error {
interval := ReannounceInterval
if action.ReAnnounceInterval > 0 {
interval = int(action.ReAnnounceInterval)
}
maxAttempts := ReannounceMaxAttempts
if action.ReAnnounceMaxAttempts > 0 {
maxAttempts = int(action.ReAnnounceMaxAttempts)
}
attempts := 0
for attempts <= maxAttempts {
s.log.Debug().Msgf("re-announce %d attempt: %d/%d", torrentId, attempts, maxAttempts)
// add delay for next run
time.Sleep(time.Duration(interval) * time.Second)
t, err := tbt.TorrentGet(ctx, []string{"trackerStats"}, []int64{torrentId})
if err != nil {
return errors.Wrap(err, "reannounced, failed to find torrentid")
}
if len(t) < 1 {
return errors.Wrap(err, "reannounced, failed to get torrent from id")
}
for _, tracker := range t[0].TrackerStats {
tracker := tracker
s.log.Trace().Msgf("transmission tracker: %+v", tracker)
if tracker.IsBackup {
continue
}
if isUnregistered(tracker.LastAnnounceResult) {
continue
}
if tracker.SeederCount > 0 {
return nil
} else if tracker.LeecherCount > 0 {
return nil
}
}
s.log.Debug().Msgf("transmission re-announce not working yet, lets re-announce %d again attempt: %d/%d", torrentId, attempts, maxAttempts)
if err := tbt.TorrentReannounceIDs(ctx, []int64{torrentId}); err != nil {
return errors.Wrap(err, "failed to reannounce")
}
attempts++
}
if attempts == maxAttempts && action.ReAnnounceDelete {
s.log.Info().Msgf("re-announce for %v took too long, deleting torrent", torrentId)
if err := tbt.TorrentRemove(ctx, transmissionrpc.TorrentRemovePayload{IDs: []int64{torrentId}}); err != nil {
return errors.Wrap(err, "could not delete torrent: %v from client after max re-announce attempts reached", torrentId)
}
return errors.Wrap(ErrReannounceTookTooLong, "transmission re-announce took too long, deleted torrent %v", torrentId)
}
return nil
}
func (s *service) transmissionCheckRulesCanDownload(ctx context.Context, action *domain.Action, client *domain.DownloadClient, tbt *transmissionrpc.Client) ([]string, error) {
s.log.Trace().Msgf("action transmission: %s check rules", action.Name)
// check for active downloads and other rules
if client.Settings.Rules.Enabled && !action.IgnoreRules {
torrents, err := tbt.TorrentGet(ctx, []string{"status"}, []int64{})
if err != nil {
return nil, errors.Wrap(err, "could not fetch active downloads")
}
var activeDownloads []transmissionrpc.Torrent
// there is no way to get torrents by status, so we need to filter ourselves
for _, torrent := range torrents {
if *torrent.Status == transmissionrpc.TorrentStatusDownload {
activeDownloads = append(activeDownloads, torrent)
}
}
// make sure it's not set to 0 by default
if client.Settings.Rules.MaxActiveDownloads > 0 {
// if max active downloads reached, check speed and if lower than threshold add anyway
if len(activeDownloads) >= client.Settings.Rules.MaxActiveDownloads {
rejection := "max active downloads reached, skipping"
s.log.Debug().Msg(rejection)
return []string{rejection}, nil
}
}
}
return nil, nil
}
func isUnregistered(msg string) bool {
words := []string{"unregistered", "not registered", "not found", "not exist"}
msg = strings.ToLower(msg)
for _, v := range words {
if strings.Contains(msg, v) {
return true
}
}
return false
}