mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +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:
|
||||
- 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
|
||||
|
|
|
@ -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,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)
|
||||
}
|
||||
|
||||
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 := transmissionrpc.TorrentAddPayload{
|
||||
Filename: &release.MagnetURI,
|
||||
}
|
||||
if action.SavePath != "" {
|
||||
payload.DownloadDir = &action.SavePath
|
||||
}
|
||||
if action.Paused {
|
||||
payload.Paused = &action.Paused
|
||||
}
|
||||
payload.Filename = &release.MagnetURI
|
||||
|
||||
// Prepare and send 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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":
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue