mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
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:
parent
bc823f98a4
commit
90b5cc9351
4 changed files with 223 additions and 26 deletions
|
@ -63,13 +63,15 @@ archives:
|
||||||
builds:
|
builds:
|
||||||
- autobrr
|
- autobrr
|
||||||
- autobrrctl
|
- autobrrctl
|
||||||
files:
|
|
||||||
- none*
|
|
||||||
replacements:
|
|
||||||
amd64: x86_64
|
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
format: zip
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- .Version }}_
|
||||||
|
{{- .Os }}_
|
||||||
|
{{- if eq .Arch "amd64" }}x86_64
|
||||||
|
{{- else }}{{ .Arch }}{{ end }}
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
|
|
|
@ -5,6 +5,8 @@ package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
"github.com/autobrr/autobrr/pkg/errors"
|
"github.com/autobrr/autobrr/pkg/errors"
|
||||||
|
@ -12,6 +14,11 @@ import (
|
||||||
"github.com/hekmon/transmissionrpc/v2"
|
"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) {
|
func (s *service) transmission(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
|
||||||
s.log.Debug().Msgf("action Transmission: %s", action.Name)
|
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)
|
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{
|
tbt, err := transmissionrpc.New(client.Host, client.Username, client.Password, &transmissionrpc.AdvancedConfig{
|
||||||
HTTPS: client.TLS,
|
HTTPS: client.TLS,
|
||||||
Port: uint16(client.Port),
|
Port: uint16(client.Port),
|
||||||
|
@ -38,16 +43,26 @@ func (s *service) transmission(ctx context.Context, action *domain.Action, relea
|
||||||
return nil, errors.Wrap(err, "error logging into client: %s", client.Host)
|
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() {
|
if release.HasMagnetUri() {
|
||||||
payload := transmissionrpc.TorrentAddPayload{
|
payload.Filename = &release.MagnetURI
|
||||||
Filename: &release.MagnetURI,
|
|
||||||
}
|
|
||||||
if action.SavePath != "" {
|
|
||||||
payload.DownloadDir = &action.SavePath
|
|
||||||
}
|
|
||||||
if action.Paused {
|
|
||||||
payload.Paused = &action.Paused
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare and send payload
|
// Prepare and send payload
|
||||||
torrent, err := tbt.TorrentAdd(ctx, payload)
|
torrent, err := tbt.TorrentAdd(ctx, payload)
|
||||||
|
@ -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)
|
return nil, errors.Wrap(err, "cant encode file %s into base64", release.TorrentTmpFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := transmissionrpc.TorrentAddPayload{
|
payload.MetaInfo = &b64
|
||||||
MetaInfo: &b64,
|
|
||||||
}
|
|
||||||
if action.SavePath != "" {
|
|
||||||
payload.DownloadDir = &action.SavePath
|
|
||||||
}
|
|
||||||
if action.Paused {
|
|
||||||
payload.Paused = &action.Paused
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare and send payload
|
// Prepare and send payload
|
||||||
torrent, err := tbt.TorrentAdd(ctx, 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)
|
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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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 = {
|
export const rulesComponentMap: componentMapType = {
|
||||||
DELUGE_V1: <FormFieldsRulesBasic/>,
|
DELUGE_V1: <FormFieldsRulesBasic/>,
|
||||||
DELUGE_V2: <FormFieldsRulesBasic/>,
|
DELUGE_V2: <FormFieldsRulesBasic/>,
|
||||||
QBITTORRENT: <FormFieldsRulesQbit/>,
|
QBITTORRENT: <FormFieldsRulesQbit/>,
|
||||||
PORLA: <FormFieldsRulesBasic/>
|
PORLA: <FormFieldsRulesBasic/>,
|
||||||
|
TRANSMISSION: <FormFieldsRulesTransmission/>
|
||||||
};
|
};
|
||||||
|
|
||||||
interface formButtonsProps {
|
interface formButtonsProps {
|
||||||
|
|
|
@ -454,6 +454,34 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
case "PORLA":
|
case "PORLA":
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue