feat(transmissionbt): implement reannounce and max active rules (#708)

* feat(transmissionbt): feature parity with qBit

* Update transmission.go

* feat(actions): transmission re-announce

* build(goreleaser): update archive name replacement

* feat(actions): transmission max active downloads check

* build(goreleaser): update archive name replacement

* build(goreleaser): remove archive files none

---------

Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
Kyle Sanderson 2023-07-01 13:51:57 -07:00 committed by GitHub
parent bc823f98a4
commit 90b5cc9351
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 223 additions and 26 deletions

View file

@ -63,13 +63,15 @@ archives:
builds:
- autobrr
- autobrrctl
files:
- none*
replacements:
amd64: x86_64
format_overrides:
- goos: windows
format: zip
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
{{- .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else }}{{ .Arch }}{{ end }}
release:
prerelease: auto

View file

@ -5,6 +5,8 @@ package action
import (
"context"
"strings"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
@ -12,6 +14,11 @@ import (
"github.com/hekmon/transmissionrpc/v2"
)
const (
ReannounceMaxAttempts = 50
ReannounceInterval = 7000
)
func (s *service) transmission(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
s.log.Debug().Msgf("action Transmission: %s", action.Name)
@ -28,8 +35,6 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea
return nil, errors.New("could not find client by id: %d", action.ClientID)
}
var rejections []string
tbt, err := transmissionrpc.New(client.Host, client.Username, client.Password, &transmissionrpc.AdvancedConfig{
HTTPS: client.TLS,
Port: uint16(client.Port),
@ -38,10 +43,17 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea
return nil, errors.Wrap(err, "error logging into client: %s", client.Host)
}
if release.HasMagnetUri() {
payload := transmissionrpc.TorrentAddPayload{
Filename: &release.MagnetURI,
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
}
@ -49,6 +61,9 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea
payload.Paused = &action.Paused
}
if release.HasMagnetUri() {
payload.Filename = &release.MagnetURI
// Prepare and send payload
torrent, err := tbt.TorrentAdd(ctx, payload)
if err != nil {
@ -72,15 +87,7 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea
return nil, errors.Wrap(err, "cant encode file %s into base64", release.TorrentTmpFile)
}
payload := transmissionrpc.TorrentAddPayload{
MetaInfo: &b64,
}
if action.SavePath != "" {
payload.DownloadDir = &action.SavePath
}
if action.Paused {
payload.Paused = &action.Paused
}
payload.MetaInfo = &b64
// Prepare and send payload
torrent, err := tbt.TorrentAdd(ctx, payload)
@ -88,8 +95,136 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea
return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, client.Host)
}
s.log.Info().Msgf("torrent with hash %v successfully added to client: '%s'", torrent.HashString, client.Name)
if !action.Paused && !action.ReAnnounceSkip {
if err := s.transmissionReannounce(ctx, action, tbt, *torrent.ID); err != 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 %v 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 %v 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.New("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
}

View file

@ -422,11 +422,43 @@ function FormFieldsRulesQbit() {
);
}
function FormFieldsRulesTransmission() {
const {
values: { settings }
} = useFormikContext<InitialValues>();
return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5 px-2">
<div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
Rules
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Manage max downloads etc.
</p>
</div>
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
{settings.rules?.enabled === true && (
<>
<NumberFieldWide
name="settings.rules.max_active_downloads"
label="Max active downloads"
tooltip={<><p>Limit the amount of active downloads (0 is unlimited), to give the maximum amount of bandwidth and disk for the downloads.</p><a href='https://autobrr.com/configuration/download-clients/dedicated#transmission-rules' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#transmission-rules</a><br /><br /><p>See recommendations for various server types here:</p><a href='https://autobrr.com/filters/examples#build-buffer' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/filters/examples#build-buffer</a></>}
/>
</>
)}
</div>
);
}
export const rulesComponentMap: componentMapType = {
DELUGE_V1: <FormFieldsRulesBasic/>,
DELUGE_V2: <FormFieldsRulesBasic/>,
QBITTORRENT: <FormFieldsRulesQbit/>,
PORLA: <FormFieldsRulesBasic/>
PORLA: <FormFieldsRulesBasic/>,
TRANSMISSION: <FormFieldsRulesTransmission/>
};
interface formButtonsProps {

View file

@ -454,6 +454,34 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
/>
</div>
</div>
<CollapsableSection title="Re-announce" subtitle="Re-announce options">
<div className="col-span-12">
<div className="mt-6 grid grid-cols-12 gap-6">
<NumberField
name={`actions.${idx}.reannounce_interval`}
label="Reannounce interval. Run every X seconds"
placeholder="7 is default and recommended"
/>
<NumberField
name={`actions.${idx}.reannounce_max_attempts`}
label="Run reannounce Y times"
/>
</div>
</div>
<div className="col-span-6">
<SwitchGroup
name={`actions.${idx}.reannounce_skip`}
label="Disable reannounce"
description="Reannounce is enabled by default. Disable if needed."
/>
<SwitchGroup
name={`actions.${idx}.reannounce_delete`}
label="Delete stalled"
description="Delete stalled torrents after X attempts"
/>
</div>
</CollapsableSection>
</div>
);
case "PORLA":