mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +00:00

* chore: update copyright year in license headers * Revert "chore: update copyright year in license headers" This reverts commit 3e58129c431b9a491089ce36b908f9bb6ba38ed3. * chore: update copyright year in license headers * fix: sort go imports * fix: add missing license headers
303 lines
8.9 KiB
Go
303 lines
8.9 KiB
Go
// Copyright (c) 2021 - 2025, 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/v3"
|
|
)
|
|
|
|
const (
|
|
ReannounceMaxAttempts = 50
|
|
ReannounceInterval = 7 // interval in seconds
|
|
)
|
|
|
|
var ErrReannounceTookTooLong = errors.New("ErrReannounceTookTooLong")
|
|
var TrTrue = true
|
|
|
|
func (s *service) transmission(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
|
|
s.log.Debug().Msgf("action Transmission: %s", action.Name)
|
|
|
|
client, err := s.clientSvc.GetClient(ctx, action.ClientID)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not get client with id %d", action.ClientID)
|
|
}
|
|
action.Client = client
|
|
|
|
if !client.Enabled {
|
|
return nil, errors.New("client %s %s not enabled", client.Type, client.Name)
|
|
}
|
|
|
|
tbt := client.Client.(*transmissionrpc.Client)
|
|
|
|
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 != "" || action.LimitUploadSpeed > 0 || action.LimitDownloadSpeed > 0 || action.LimitRatio > 0 || action.LimitSeedTime > 0 {
|
|
p := transmissionrpc.TorrentSetPayload{
|
|
IDs: []int64{*torrent.ID},
|
|
}
|
|
|
|
if action.Label != "" {
|
|
p.Labels = []string{action.Label}
|
|
}
|
|
|
|
if action.LimitUploadSpeed > 0 {
|
|
p.UploadLimit = &action.LimitUploadSpeed
|
|
p.UploadLimited = &TrTrue
|
|
}
|
|
if action.LimitDownloadSpeed > 0 {
|
|
p.DownloadLimit = &action.LimitDownloadSpeed
|
|
p.DownloadLimited = &TrTrue
|
|
}
|
|
if action.LimitRatio > 0 {
|
|
p.SeedRatioLimit = &action.LimitRatio
|
|
ratioMode := transmissionrpc.SeedRatioModeCustom
|
|
p.SeedRatioMode = &ratioMode
|
|
}
|
|
if action.LimitSeedTime > 0 {
|
|
t := time.Duration(action.LimitSeedTime) * time.Minute
|
|
//p.SeedIdleLimit = &action.LimitSeedTime
|
|
p.SeedIdleLimit = &t
|
|
|
|
// seed idle mode 1
|
|
seedIdleMode := int64(1)
|
|
p.SeedIdleMode = &seedIdleMode
|
|
}
|
|
|
|
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 err := s.downloadSvc.DownloadRelease(ctx, &release); err != nil {
|
|
return nil, errors.Wrap(err, "could not download torrent file for release: %s", release.TorrentName)
|
|
}
|
|
|
|
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 != "" || action.LimitUploadSpeed > 0 || action.LimitDownloadSpeed > 0 || action.LimitRatio > 0 || action.LimitSeedTime > 0 {
|
|
p := transmissionrpc.TorrentSetPayload{
|
|
IDs: []int64{*torrent.ID},
|
|
}
|
|
|
|
if action.Label != "" {
|
|
p.Labels = []string{action.Label}
|
|
}
|
|
|
|
if action.LimitUploadSpeed > 0 {
|
|
p.UploadLimit = &action.LimitUploadSpeed
|
|
p.UploadLimited = &TrTrue
|
|
}
|
|
if action.LimitDownloadSpeed > 0 {
|
|
p.DownloadLimit = &action.LimitDownloadSpeed
|
|
p.DownloadLimited = &TrTrue
|
|
}
|
|
if action.LimitRatio > 0 {
|
|
p.SeedRatioLimit = &action.LimitRatio
|
|
ratioMode := transmissionrpc.SeedRatioModeCustom
|
|
p.SeedRatioMode = &ratioMode
|
|
}
|
|
if action.LimitSeedTime > 0 {
|
|
t := time.Duration(action.LimitSeedTime) * time.Minute
|
|
p.SeedIdleLimit = &t
|
|
|
|
// seed idle mode 1
|
|
seedIdleMode := int64(1)
|
|
p.SeedIdleMode = &seedIdleMode
|
|
}
|
|
|
|
s.log.Trace().Msgf("transmission torrent set payload: %+v for torrent hash %s client: %s", p, *torrent.HashString, client.Name)
|
|
|
|
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
|
|
}
|