From ca196f0bf1a83d4ec5edc1006c982ca0fcaa7490 Mon Sep 17 00:00:00 2001 From: ze0s <43699394+zze0s@users.noreply.github.com> Date: Tue, 28 Feb 2023 22:16:10 +0100 Subject: [PATCH] feat(releases): support magnet links (#730) * feat(releases): support magnet links * feat(feeds): support magnet links * feat(actions): log messages * fix: component warning * fix: check hasprefix instead of hassuffix for magnet * feat(release): resolve magnet uri from link * fix(actions): deluge use magnet uri * fix(macros): add `MagnetURI` var * fix(actions): run magnet resolving before macros * feat(feeds): set download type on creation --- internal/action/deluge.go | 240 ++++++++++++++-------- internal/action/exec.go | 16 +- internal/action/lidarr.go | 1 + internal/action/porla.go | 85 +++++--- internal/action/qbittorrent.go | 69 ++++--- internal/action/radarr.go | 1 + internal/action/readarr.go | 1 + internal/action/rtorrent.go | 91 +++++--- internal/action/run.go | 15 +- internal/action/sonarr.go | 1 + internal/action/transmission.go | 83 +++++--- internal/action/whisparr.go | 1 + internal/database/feed.go | 57 ++++- internal/domain/feed.go | 13 +- internal/domain/macros.go | 2 + internal/domain/release.go | 80 ++++++++ internal/feed/rss.go | 5 + internal/feed/service.go | 2 +- internal/feed/torznab.go | 5 + internal/filter/service.go | 22 +- pkg/lidarr/lidarr.go | 3 +- pkg/porla/domain.go | 3 +- pkg/radarr/radarr.go | 3 +- pkg/readarr/readarr.go | 3 +- pkg/sonarr/sonarr.go | 3 +- pkg/whisparr/whisparr.go | 3 +- web/src/components/inputs/input_wide.tsx | 4 +- web/src/components/inputs/select_wide.tsx | 141 ++++++++++++- web/src/domain/constants.ts | 33 ++- web/src/forms/settings/FeedForms.tsx | 10 +- web/src/forms/settings/IndexerForms.tsx | 25 ++- web/src/types/Feed.d.ts | 9 + 32 files changed, 770 insertions(+), 260 deletions(-) diff --git a/internal/action/deluge.go b/internal/action/deluge.go index 7ea236e..2ab7c40 100644 --- a/internal/action/deluge.go +++ b/internal/action/deluge.go @@ -13,19 +13,19 @@ import ( ) func (s *service) deluge(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) { - s.log.Debug().Msgf("action Deluge: %v", action.Name) + s.log.Debug().Msgf("action Deluge: %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: %v", action.ClientID) + 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: %v", action.ClientID) + return nil, errors.New("could not find client by id: %d", action.ClientID) } var rejections []string @@ -54,7 +54,7 @@ func (s *service) delugeCheckRulesCanDownload(deluge delugeClient.DelugeClient, // 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 anyways + // if max active downloads reached, check speed and if lower than threshold add anyway if len(activeDownloads) >= client.Settings.Rules.MaxActiveDownloads { s.log.Debug().Msg("max active downloads reached, skipping") @@ -101,7 +101,7 @@ func (s *service) delugeV1(ctx context.Context, client *domain.DownloadClient, a // perform connection to Deluge server err := deluge.Connect() if err != nil { - return nil, errors.Wrap(err, "could not connect to client %v at %v", client.Name, client.Host) + return nil, errors.Wrap(err, "could not connect to client %s at %s", client.Name, client.Host) } defer deluge.Close() @@ -109,59 +109,92 @@ func (s *service) delugeV1(ctx context.Context, client *domain.DownloadClient, a // perform connection to Deluge server rejections, err := s.delugeCheckRulesCanDownload(deluge, client, action) if err != nil { - s.log.Error().Err(err).Msgf("error checking client rules: %v", action.Name) + s.log.Error().Err(err).Msgf("error checking client rules: %s", action.Name) return nil, err } if rejections != nil { return rejections, nil } - if release.TorrentTmpFile == "" { - if err := release.DownloadTorrentFileCtx(ctx); err != nil { - s.log.Error().Err(err).Msgf("could not download torrent file for release: %v", release.TorrentName) - return nil, err - } - } - - t, err := os.ReadFile(release.TorrentTmpFile) - if err != nil { - return nil, errors.Wrap(err, "could not read torrent file: %v", release.TorrentTmpFile) - } - - // encode file to base64 before sending to deluge - encodedFile := base64.StdEncoding.EncodeToString(t) - if encodedFile == "" { - return nil, errors.Wrap(err, "could not encode torrent file: %v", release.TorrentTmpFile) - } - - options, err := s.prepareDelugeOptions(action) - if err != nil { - return nil, errors.Wrap(err, "could not prepare options") - } - - s.log.Trace().Msgf("action Deluge options: %+v", options) - - torrentHash, err := deluge.AddTorrentFile(release.TorrentTmpFile, encodedFile, &options) - if err != nil { - return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, client.Name) - } - - if action.Label != "" { - labelPluginActive, err := deluge.LabelPlugin() + if release.HasMagnetUri() { + options, err := s.prepareDelugeOptions(action) if err != nil { - return nil, errors.Wrap(err, "could not load label plugin for client: %v", client.Name) + return nil, errors.Wrap(err, "could not prepare options") } - if labelPluginActive != nil { - // TODO first check if label exists, if not, add it, otherwise set - err = labelPluginActive.SetTorrentLabel(torrentHash, action.Label) + s.log.Trace().Msgf("action Deluge options: %+v", options) + + torrentHash, err := deluge.AddTorrentMagnet(release.MagnetURI, &options) + if err != nil { + return nil, errors.Wrap(err, "could not add torrent magnet %s to client: %s", release.TorrentURL, client.Name) + } + + if action.Label != "" { + labelPluginActive, err := deluge.LabelPlugin() if err != nil { - return nil, errors.Wrap(err, "could not set label: %v on client: %v", action.Label, client.Name) + return nil, errors.Wrap(err, "could not load label plugin for client: %s", client.Name) + } + + if labelPluginActive != nil { + // TODO first check if label exists, if not, add it, otherwise set + err = labelPluginActive.SetTorrentLabel(torrentHash, action.Label) + if err != nil { + return nil, errors.Wrap(err, "could not set label: %s on client: %s", action.Label, client.Name) + } } } - } - s.log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", torrentHash, client.Name) + s.log.Info().Msgf("torrent from magnet with hash %s successfully added to client: '%s'", torrentHash, client.Name) + + return nil, nil + } else { + 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 + } + } + + t, err := os.ReadFile(release.TorrentTmpFile) + if err != nil { + return nil, errors.Wrap(err, "could not read torrent file: %s", release.TorrentTmpFile) + } + + // encode file to base64 before sending to deluge + encodedFile := base64.StdEncoding.EncodeToString(t) + if encodedFile == "" { + return nil, errors.Wrap(err, "could not encode torrent file: %s", release.TorrentTmpFile) + } + + options, err := s.prepareDelugeOptions(action) + if err != nil { + return nil, errors.Wrap(err, "could not prepare options") + } + + s.log.Trace().Msgf("action Deluge options: %+v", options) + + torrentHash, err := deluge.AddTorrentFile(release.TorrentTmpFile, encodedFile, &options) + if err != nil { + return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, client.Name) + } + + if action.Label != "" { + labelPluginActive, err := deluge.LabelPlugin() + if err != nil { + return nil, errors.Wrap(err, "could not load label plugin for client: %s", client.Name) + } + + if labelPluginActive != nil { + // TODO first check if label exists, if not, add it, otherwise set + err = labelPluginActive.SetTorrentLabel(torrentHash, action.Label) + if err != nil { + return nil, errors.Wrap(err, "could not set label: %v on client: %s", action.Label, client.Name) + } + } + } + + s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", torrentHash, client.Name) + } return nil, nil } @@ -181,7 +214,7 @@ func (s *service) delugeV2(ctx context.Context, client *domain.DownloadClient, a // perform connection to Deluge server err := deluge.Connect() if err != nil { - return nil, errors.Wrap(err, "could not connect to client %v at %v", client.Name, client.Host) + return nil, errors.Wrap(err, "could not connect to client %s at %s", client.Name, client.Host) } defer deluge.Close() @@ -189,60 +222,93 @@ func (s *service) delugeV2(ctx context.Context, client *domain.DownloadClient, a // perform connection to Deluge server rejections, err := s.delugeCheckRulesCanDownload(deluge, client, action) if err != nil { - s.log.Error().Err(err).Msgf("error checking client rules: %v", action.Name) + s.log.Error().Err(err).Msgf("error checking client rules: %s", action.Name) return nil, err } if rejections != nil { return rejections, nil } - if release.TorrentTmpFile == "" { - if err := release.DownloadTorrentFileCtx(ctx); err != nil { - s.log.Error().Err(err).Msgf("could not download torrent file for release: %v", release.TorrentName) - return nil, err - } - } - - t, err := os.ReadFile(release.TorrentTmpFile) - if err != nil { - return nil, errors.Wrap(err, "could not read torrent file: %v", release.TorrentTmpFile) - } - - // encode file to base64 before sending to deluge - encodedFile := base64.StdEncoding.EncodeToString(t) - if encodedFile == "" { - return nil, errors.Wrap(err, "could not encode torrent file: %v", release.TorrentTmpFile) - } - - // set options - options, err := s.prepareDelugeOptions(action) - if err != nil { - return nil, errors.Wrap(err, "could not prepare options") - } - - s.log.Trace().Msgf("action Deluge options: %+v", options) - - torrentHash, err := deluge.AddTorrentFile(release.TorrentTmpFile, encodedFile, &options) - if err != nil { - return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, client.Name) - } - - if action.Label != "" { - labelPluginActive, err := deluge.LabelPlugin() + if release.HasMagnetUri() { + options, err := s.prepareDelugeOptions(action) if err != nil { - return nil, errors.Wrap(err, "could not load label plugin for client: %v", client.Name) + return nil, errors.Wrap(err, "could not prepare options") } - if labelPluginActive != nil { - // TODO first check if label exists, if not, add it, otherwise set - err = labelPluginActive.SetTorrentLabel(torrentHash, action.Label) + s.log.Trace().Msgf("action Deluge options: %+v", options) + + torrentHash, err := deluge.AddTorrentMagnet(release.MagnetURI, &options) + if err != nil { + return nil, errors.Wrap(err, "could not add torrent magnet %s to client: %s", release.TorrentURL, client.Name) + } + + if action.Label != "" { + labelPluginActive, err := deluge.LabelPlugin() if err != nil { - return nil, errors.Wrap(err, "could not set label: %v on client: %v", action.Label, client.Name) + return nil, errors.Wrap(err, "could not load label plugin for client: %s", client.Name) + } + + if labelPluginActive != nil { + // TODO first check if label exists, if not, add it, otherwise set + err = labelPluginActive.SetTorrentLabel(torrentHash, action.Label) + if err != nil { + return nil, errors.Wrap(err, "could not set label: %s on client: %s", action.Label, client.Name) + } } } - } - s.log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", torrentHash, client.Name) + s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", torrentHash, client.Name) + + return nil, nil + } else { + 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 + } + } + + t, err := os.ReadFile(release.TorrentTmpFile) + if err != nil { + return nil, errors.Wrap(err, "could not read torrent file: %s", release.TorrentTmpFile) + } + + // encode file to base64 before sending to deluge + encodedFile := base64.StdEncoding.EncodeToString(t) + if encodedFile == "" { + return nil, errors.Wrap(err, "could not encode torrent file: %s", release.TorrentTmpFile) + } + + // set options + options, err := s.prepareDelugeOptions(action) + if err != nil { + return nil, errors.Wrap(err, "could not prepare options") + } + + s.log.Trace().Msgf("action Deluge options: %+v", options) + + torrentHash, err := deluge.AddTorrentFile(release.TorrentTmpFile, encodedFile, &options) + if err != nil { + return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.TorrentTmpFile, client.Name) + } + + if action.Label != "" { + labelPluginActive, err := deluge.LabelPlugin() + if err != nil { + return nil, errors.Wrap(err, "could not load label plugin for client: %s", client.Name) + } + + if labelPluginActive != nil { + // TODO first check if label exists, if not, add it, otherwise set + err = labelPluginActive.SetTorrentLabel(torrentHash, action.Label) + if err != nil { + return nil, errors.Wrap(err, "could not set label: %s on client: %s", action.Label, client.Name) + } + } + } + + s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", torrentHash, client.Name) + } return nil, nil } diff --git a/internal/action/exec.go b/internal/action/exec.go index 7bc942e..59baeb8 100644 --- a/internal/action/exec.go +++ b/internal/action/exec.go @@ -14,11 +14,11 @@ import ( ) func (s *service) execCmd(ctx context.Context, action *domain.Action, release domain.Release) error { - s.log.Debug().Msgf("action exec: %v release: %v", action.Name, release.TorrentName) + s.log.Debug().Msgf("action exec: %s release: %s", action.Name, release.TorrentName) if release.TorrentTmpFile == "" && strings.Contains(action.ExecArgs, "TorrentPathName") { if err := release.DownloadTorrentFileCtx(ctx); err != nil { - return errors.Wrap(err, "error downloading torrent file for release: %v", release.TorrentName) + return errors.Wrap(err, "error downloading torrent file for release: %s", release.TorrentName) } } @@ -26,7 +26,7 @@ func (s *service) execCmd(ctx context.Context, action *domain.Action, release do if len(release.TorrentDataRawBytes) == 0 && release.TorrentTmpFile != "" { t, err := os.ReadFile(release.TorrentTmpFile) if err != nil { - return errors.Wrap(err, "could not read torrent file: %v", release.TorrentTmpFile) + return errors.Wrap(err, "could not read torrent file: %s", release.TorrentTmpFile) } release.TorrentDataRawBytes = t @@ -35,14 +35,14 @@ func (s *service) execCmd(ctx context.Context, action *domain.Action, release do // check if program exists cmd, err := exec.LookPath(action.ExecCmd) if err != nil { - return errors.Wrap(err, "exec failed, could not find program: %v", action.ExecCmd) + return errors.Wrap(err, "exec failed, could not find program: %s", action.ExecCmd) } p := shellwords.NewParser() p.ParseBacktick = true args, err := p.Parse(action.ExecArgs) if err != nil { - return errors.Wrap(err, "could not parse exec args: %v", action.ExecArgs) + return errors.Wrap(err, "could not parse exec args: %s", action.ExecArgs) } // we need to split on space into a string slice, so we can spread the args into exec @@ -56,14 +56,14 @@ func (s *service) execCmd(ctx context.Context, action *domain.Action, release do output, err := command.CombinedOutput() if err != nil { // everything other than exit 0 is considered an error - return errors.Wrap(err, "error executing command: %v args: %v", cmd, args) + return errors.Wrap(err, "error executing command: %s args: %s", cmd, args) } - s.log.Trace().Msgf("executed command: '%v'", string(output)) + s.log.Trace().Msgf("executed command: '%s'", string(output)) duration := time.Since(start) - s.log.Info().Msgf("executed command: '%v', args: '%v' %v,%v, total time %v", cmd, args, release.TorrentName, release.Indexer, duration) + s.log.Info().Msgf("executed command: '%s', args: '%s' %s,%s, total time %v", cmd, args, release.TorrentName, release.Indexer, duration) return nil } diff --git a/internal/action/lidarr.go b/internal/action/lidarr.go index 7681cd2..4466089 100644 --- a/internal/action/lidarr.go +++ b/internal/action/lidarr.go @@ -45,6 +45,7 @@ func (s *service) lidarr(ctx context.Context, action *domain.Action, release dom r := lidarr.Release{ Title: release.TorrentName, DownloadUrl: release.TorrentURL, + MagnetUrl: release.MagnetURI, Size: int64(release.Size), Indexer: release.Indexer, DownloadProtocol: "torrent", diff --git a/internal/action/porla.go b/internal/action/porla.go index 38c0c43..84c0d95 100644 --- a/internal/action/porla.go +++ b/internal/action/porla.go @@ -48,44 +48,69 @@ func (s *service) porla(ctx context.Context, action *domain.Action, release doma return rejections, nil } - if release.TorrentTmpFile == "" { - if err := release.DownloadTorrentFile(); err != nil { - return nil, errors.Wrap(err, "error downloading torrent file for release: %s", release.TorrentName) + if release.HasMagnetUri() { + opts := &porla.TorrentsAddReq{ + DownloadLimit: -1, + UploadLimit: -1, + SavePath: action.SavePath, + MagnetUri: release.MagnetURI, } - } - file, err := os.Open(release.TorrentTmpFile) - if err != nil { - return nil, errors.Wrap(err, "error opening file %s", release.TorrentTmpFile) - } - defer file.Close() + if action.LimitDownloadSpeed > 0 { + opts.DownloadLimit = action.LimitDownloadSpeed * 1000 + } - reader := bufio.NewReader(file) - content, err := io.ReadAll(reader) - if err != nil { - return nil, errors.Wrap(err, "failed to read file: %s", release.TorrentTmpFile) - } + if action.LimitUploadSpeed > 0 { + opts.UploadLimit = action.LimitUploadSpeed * 1000 + } - opts := &porla.TorrentsAddReq{ - DownloadLimit: -1, - SavePath: action.SavePath, - Ti: base64.StdEncoding.EncodeToString(content), - UploadLimit: -1, - } + if err = prl.TorrentsAdd(ctx, opts); err != nil { + return nil, errors.Wrap(err, "could not add torrent from magnet %s to client: %s", release.MagnetURI, client.Name) + } - if action.LimitDownloadSpeed > 0 { - opts.DownloadLimit = action.LimitDownloadSpeed * 1000 - } + s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", release.TorrentHash, client.Name) - if action.LimitUploadSpeed > 0 { - opts.UploadLimit = action.LimitUploadSpeed * 1000 - } + return nil, nil + } else { + if release.TorrentTmpFile == "" { + if err := release.DownloadTorrentFileCtx(ctx); err != nil { + return nil, errors.Wrap(err, "error downloading torrent file for release: %s", release.TorrentName) + } + } - if err = prl.TorrentsAdd(ctx, opts); err != nil { - return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, client.Name) - } + file, err := os.Open(release.TorrentTmpFile) + if err != nil { + return nil, errors.Wrap(err, "error opening file %s", release.TorrentTmpFile) + } + defer file.Close() - s.log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", release.TorrentHash, client.Name) + reader := bufio.NewReader(file) + content, err := io.ReadAll(reader) + if err != nil { + return nil, errors.Wrap(err, "failed to read file: %s", release.TorrentTmpFile) + } + + opts := &porla.TorrentsAddReq{ + DownloadLimit: -1, + SavePath: action.SavePath, + Ti: base64.StdEncoding.EncodeToString(content), + UploadLimit: -1, + } + + if action.LimitDownloadSpeed > 0 { + opts.DownloadLimit = action.LimitDownloadSpeed * 1000 + } + + if action.LimitUploadSpeed > 0 { + opts.UploadLimit = action.LimitUploadSpeed * 1000 + } + + if err = prl.TorrentsAdd(ctx, opts); err != nil { + return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.TorrentTmpFile, client.Name) + } + + s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", release.TorrentHash, client.Name) + } return nil, nil } diff --git a/internal/action/qbittorrent.go b/internal/action/qbittorrent.go index 0d1aa0a..e4d08d4 100644 --- a/internal/action/qbittorrent.go +++ b/internal/action/qbittorrent.go @@ -11,48 +11,65 @@ import ( ) func (s *service) qbittorrent(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) { - s.log.Debug().Msgf("action qBittorrent: %v", action.Name) + s.log.Debug().Msgf("action qBittorrent: %s", action.Name) c := s.clientSvc.GetCachedClient(ctx, action.ClientID) rejections, err := s.qbittorrentCheckRulesCanDownload(ctx, action, c.Dc, c.Qbt) if err != nil { - return nil, errors.Wrap(err, "error checking client rules: %v", action.Name) + return nil, errors.Wrap(err, "error checking client rules: %s", action.Name) } if len(rejections) > 0 { return rejections, nil } - if release.TorrentTmpFile == "" { - if err := release.DownloadTorrentFileCtx(ctx); err != nil { - return nil, errors.Wrap(err, "error downloading torrent file for release: %v", release.TorrentName) + if release.HasMagnetUri() { + options, err := s.prepareQbitOptions(action) + if err != nil { + return nil, errors.Wrap(err, "could not prepare options") } - } - options, err := s.prepareQbitOptions(action) - if err != nil { - return nil, errors.Wrap(err, "could not prepare options") - } + s.log.Trace().Msgf("action qBittorrent options: %+v", options) - s.log.Trace().Msgf("action qBittorrent options: %+v", options) - - if err = c.Qbt.AddTorrentFromFileCtx(ctx, release.TorrentTmpFile, options); err != nil { - return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, c.Dc.Name) - } - - if !action.Paused && !action.ReAnnounceSkip && release.TorrentHash != "" { - opts := qbittorrent.ReannounceOptions{ - Interval: int(action.ReAnnounceInterval), - MaxAttempts: int(action.ReAnnounceMaxAttempts), - DeleteOnFailure: action.ReAnnounceDelete, + if err = c.Qbt.AddTorrentFromUrlCtx(ctx, release.MagnetURI, options); err != nil { + return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.MagnetURI, c.Dc.Name) } - if err := c.Qbt.ReannounceTorrentWithRetry(ctx, opts, release.TorrentHash); err != nil { - return nil, errors.Wrap(err, "could not reannounce torrent: %v", release.TorrentHash) - } - } - s.log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", release.TorrentHash, c.Dc.Name) + s.log.Info().Msgf("torrent from magnet successfully added to client: '%s'", c.Dc.Name) + + return nil, nil + } else { + if release.TorrentTmpFile == "" { + if err := release.DownloadTorrentFileCtx(ctx); err != nil { + return nil, errors.Wrap(err, "error downloading torrent file for release: %s", release.TorrentName) + } + } + + options, err := s.prepareQbitOptions(action) + if err != nil { + return nil, errors.Wrap(err, "could not prepare options") + } + + s.log.Trace().Msgf("action qBittorrent options: %+v", options) + + if err = c.Qbt.AddTorrentFromFileCtx(ctx, release.TorrentTmpFile, options); err != nil { + return nil, errors.Wrap(err, "could not add torrent %s to client: %s", release.TorrentTmpFile, c.Dc.Name) + } + + if !action.Paused && !action.ReAnnounceSkip && release.TorrentHash != "" { + opts := qbittorrent.ReannounceOptions{ + Interval: int(action.ReAnnounceInterval), + MaxAttempts: int(action.ReAnnounceMaxAttempts), + DeleteOnFailure: action.ReAnnounceDelete, + } + if err := c.Qbt.ReannounceTorrentWithRetry(ctx, opts, release.TorrentHash); err != nil { + return nil, errors.Wrap(err, "could not reannounce torrent: %s", release.TorrentHash) + } + } + + s.log.Info().Msgf("torrent with hash %s successfully added to client: '%s'", release.TorrentHash, c.Dc.Name) + } return nil, nil } diff --git a/internal/action/radarr.go b/internal/action/radarr.go index 3e92762..c723b75 100644 --- a/internal/action/radarr.go +++ b/internal/action/radarr.go @@ -44,6 +44,7 @@ func (s *service) radarr(ctx context.Context, action *domain.Action, release dom r := radarr.Release{ Title: release.TorrentName, DownloadUrl: release.TorrentURL, + MagnetUrl: release.MagnetURI, Size: int64(release.Size), Indexer: release.Indexer, DownloadProtocol: "torrent", diff --git a/internal/action/readarr.go b/internal/action/readarr.go index a175b82..24d7eaa 100644 --- a/internal/action/readarr.go +++ b/internal/action/readarr.go @@ -44,6 +44,7 @@ func (s *service) readarr(ctx context.Context, action *domain.Action, release do r := readarr.Release{ Title: release.TorrentName, DownloadUrl: release.TorrentURL, + MagnetUrl: release.MagnetURI, Size: int64(release.Size), Indexer: release.Indexer, DownloadProtocol: "torrent", diff --git a/internal/action/rtorrent.go b/internal/action/rtorrent.go index f8f0798..de8b30b 100644 --- a/internal/action/rtorrent.go +++ b/internal/action/rtorrent.go @@ -2,66 +2,93 @@ package action import ( "context" + "os" + "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/pkg/errors" - "os" "github.com/mrobinsn/go-rtorrent/rtorrent" ) func (s *service) rtorrent(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) { - s.log.Debug().Msgf("action rTorrent: %v", action.Name) + s.log.Debug().Msgf("action rTorrent: %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: %v", action.ClientID) + 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: %v", action.ClientID) + return nil, errors.New("could not find client by id: %d", action.ClientID) } var rejections []string - if release.TorrentTmpFile == "" { - if err := release.DownloadTorrentFileCtx(ctx); err != nil { - s.log.Error().Err(err).Msgf("could not download torrent file for release: %v", release.TorrentName) - return nil, err - } - } - // create client rt := rtorrent.New(client.Host, true) - tmpFile, err := os.ReadFile(release.TorrentTmpFile) - if err != nil { - return nil, errors.Wrap(err, "could not read torrent file: %v", release.TorrentTmpFile) - } + if release.HasMagnetUri() { + var args []*rtorrent.FieldValue - var args []*rtorrent.FieldValue + if action.Label != "" { + args = append(args, &rtorrent.FieldValue{ + Field: rtorrent.DLabel, + Value: action.Label, + }) + } + if action.SavePath != "" { + args = append(args, &rtorrent.FieldValue{ + Field: rtorrent.DDirectory, + Value: action.SavePath, + }) + } - if action.Label != "" { - args = append(args, &rtorrent.FieldValue{ - Field: rtorrent.DLabel, - Value: action.Label, - }) - } - if action.SavePath != "" { - args = append(args, &rtorrent.FieldValue{ - Field: rtorrent.DDirectory, - Value: action.SavePath, - }) - } + if err := rt.Add(release.MagnetURI, args...); err != nil { + return nil, errors.Wrap(err, "could not add torrent from magnet: %s", release.MagnetURI) + } - if err := rt.AddTorrent(tmpFile, args...); err != nil { - return nil, errors.Wrap(err, "could not add torrent file: %v", release.TorrentTmpFile) - } + s.log.Info().Msgf("torrent from magnet successfully added to client: '%s'", client.Name) - s.log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", "", client.Name) + return nil, nil + + } else { + 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 + } + } + + tmpFile, err := os.ReadFile(release.TorrentTmpFile) + if err != nil { + return nil, errors.Wrap(err, "could not read torrent file: %s", release.TorrentTmpFile) + } + + var args []*rtorrent.FieldValue + + if action.Label != "" { + args = append(args, &rtorrent.FieldValue{ + Field: rtorrent.DLabel, + Value: action.Label, + }) + } + if action.SavePath != "" { + args = append(args, &rtorrent.FieldValue{ + Field: rtorrent.DDirectory, + Value: action.SavePath, + }) + } + + if err := rt.AddTorrent(tmpFile, args...); err != nil { + return nil, errors.Wrap(err, "could not add torrent file: %s", release.TorrentTmpFile) + } + + s.log.Info().Msgf("torrent successfully added to client: '%s'", client.Name) + } return rejections, nil } diff --git a/internal/action/run.go b/internal/action/run.go index 455f9e0..a139f4b 100644 --- a/internal/action/run.go +++ b/internal/action/run.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/tls" + "fmt" "io" "net/http" "os" @@ -24,12 +25,18 @@ func (s *service) RunAction(ctx context.Context, action *domain.Action, release defer func() { if r := recover(); r != nil { - s.log.Error().Msgf("recovering from panic in run action %v error: %v", action.Name, r) - err = errors.New("panic in action: %v", action.Name) + s.log.Error().Msgf("recovering from panic in run action %s error: %v", action.Name, r) + err = errors.New("panic in action: %s", action.Name) return } }() + // if set, try to resolve MagnetURI before parsing macros + // to allow webhook and exec to get the magnet_uri + if err := release.ResolveMagnetUri(ctx); err != nil { + return nil, err + } + // parse all macros in one go if err := action.ParseMacros(release); err != nil { return nil, err @@ -147,6 +154,10 @@ func (s *service) test(name string) { } func (s *service) watchFolder(ctx context.Context, action *domain.Action, release domain.Release) error { + if release.HasMagnetUri() { + return fmt.Errorf("action watch folder does not support magnet links: %s", release.TorrentName) + } + if release.TorrentTmpFile == "" { if err := release.DownloadTorrentFileCtx(ctx); err != nil { return errors.Wrap(err, "watch folder: could not download torrent file for release: %v", release.TorrentName) diff --git a/internal/action/sonarr.go b/internal/action/sonarr.go index 8b9b431..52a2cb8 100644 --- a/internal/action/sonarr.go +++ b/internal/action/sonarr.go @@ -44,6 +44,7 @@ func (s *service) sonarr(ctx context.Context, action *domain.Action, release dom r := sonarr.Release{ Title: release.TorrentName, DownloadUrl: release.TorrentURL, + MagnetUrl: release.MagnetURI, Size: int64(release.Size), Indexer: release.Indexer, DownloadProtocol: "torrent", diff --git a/internal/action/transmission.go b/internal/action/transmission.go index ba83dd8..93f8370 100644 --- a/internal/action/transmission.go +++ b/internal/action/transmission.go @@ -10,60 +10,83 @@ import ( ) func (s *service) transmission(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) { - s.log.Debug().Msgf("action Transmission: %v", action.Name) + 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: %v", action.ClientID) + 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: %v", action.ClientID) + return nil, errors.New("could not find client by id: %d", action.ClientID) } var rejections []string - if release.TorrentTmpFile == "" { - if err := release.DownloadTorrentFileCtx(ctx); err != nil { - s.log.Error().Err(err).Msgf("could not download torrent file for release: %v", release.TorrentName) - return nil, err - } - } - tbt, err := transmissionrpc.New(client.Host, client.Username, client.Password, &transmissionrpc.AdvancedConfig{ HTTPS: client.TLS, Port: uint16(client.Port), }) if err != nil { - return nil, errors.Wrap(err, "error logging into client: %v", client.Host) + return nil, errors.Wrap(err, "error logging into client: %s", client.Host) } - b64, err := transmissionrpc.File2Base64(release.TorrentTmpFile) - if err != nil { - return nil, errors.Wrap(err, "cant encode file %v into base64", release.TorrentTmpFile) - } + if release.HasMagnetUri() { + payload := transmissionrpc.TorrentAddPayload{ + Filename: &release.MagnetURI, + } + if action.SavePath != "" { + payload.DownloadDir = &action.SavePath + } + if action.Paused { + payload.Paused = &action.Paused + } - payload := transmissionrpc.TorrentAddPayload{ - MetaInfo: &b64, - } - if action.SavePath != "" { - payload.DownloadDir = &action.SavePath - } - if action.Paused { - payload.Paused = &action.Paused - } + // 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) + } - // Prepare and send payload - torrent, err := tbt.TorrentAdd(ctx, payload) - if err != nil { - return nil, errors.Wrap(err, "could not add torrent %v to client: %v", release.TorrentTmpFile, client.Host) - } + s.log.Info().Msgf("torrent from magnet with hash %v successfully added to client: '%s'", torrent.HashString, client.Name) - s.log.Info().Msgf("torrent with hash %v successfully added to client: '%v'", torrent.HashString, client.Name) + return nil, nil + + } else { + 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 := transmissionrpc.TorrentAddPayload{ + MetaInfo: &b64, + } + if action.SavePath != "" { + payload.DownloadDir = &action.SavePath + } + if action.Paused { + payload.Paused = &action.Paused + } + + // Prepare and send payload + torrent, err := tbt.TorrentAdd(ctx, payload) + if err != nil { + 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) + } return rejections, nil } diff --git a/internal/action/whisparr.go b/internal/action/whisparr.go index 5ab05bb..99d0319 100644 --- a/internal/action/whisparr.go +++ b/internal/action/whisparr.go @@ -44,6 +44,7 @@ func (s *service) whisparr(ctx context.Context, action *domain.Action, release d r := whisparr.Release{ Title: release.TorrentName, DownloadUrl: release.TorrentURL, + MagnetUrl: release.MagnetURI, Size: int64(release.Size), Indexer: release.Indexer, DownloadProtocol: "torrent", diff --git a/internal/database/feed.go b/internal/database/feed.go index 377cd2f..ee3551e 100644 --- a/internal/database/feed.go +++ b/internal/database/feed.go @@ -3,6 +3,7 @@ package database import ( "context" "database/sql" + "encoding/json" "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/logger" @@ -38,6 +39,7 @@ func (r *FeedRepo) FindByID(ctx context.Context, id int) (*domain.Feed, error) { "f.max_age", "f.api_key", "f.cookie", + "f.settings", "f.created_at", "f.updated_at", ). @@ -57,16 +59,22 @@ func (r *FeedRepo) FindByID(ctx context.Context, id int) (*domain.Feed, error) { var f domain.Feed - var apiKey, cookie sql.NullString + var apiKey, cookie, settings sql.NullString - if err := row.Scan(&f.ID, &f.Indexer, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &f.CreatedAt, &f.UpdatedAt); err != nil { + if err := row.Scan(&f.ID, &f.Indexer, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil { return nil, errors.Wrap(err, "error scanning row") - } f.ApiKey = apiKey.String f.Cookie = cookie.String + var settingsJson domain.FeedSettingsJSON + if err = json.Unmarshal([]byte(settings.String), &settingsJson); err != nil { + return nil, errors.Wrap(err, "error unmarshal settings") + } + + f.Settings = &settingsJson + return &f, nil } @@ -84,6 +92,7 @@ func (r *FeedRepo) FindByIndexerIdentifier(ctx context.Context, indexer string) "f.max_age", "f.api_key", "f.cookie", + "f.settings", "f.created_at", "f.updated_at", ). @@ -103,15 +112,22 @@ func (r *FeedRepo) FindByIndexerIdentifier(ctx context.Context, indexer string) var f domain.Feed - var apiKey, cookie sql.NullString + var apiKey, cookie, settings sql.NullString - if err := row.Scan(&f.ID, &f.Indexer, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &f.CreatedAt, &f.UpdatedAt); err != nil { + if err := row.Scan(&f.ID, &f.Indexer, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil { return nil, errors.Wrap(err, "error scanning row") } f.ApiKey = apiKey.String f.Cookie = cookie.String + var settingsJson domain.FeedSettingsJSON + if err = json.Unmarshal([]byte(settings.String), &settingsJson); err != nil { + return nil, errors.Wrap(err, "error unmarshal settings") + } + + f.Settings = &settingsJson + return &f, nil } @@ -131,6 +147,7 @@ func (r *FeedRepo) Find(ctx context.Context) ([]domain.Feed, error) { "f.cookie", "f.last_run", "f.last_run_data", + "f.settings", "f.created_at", "f.updated_at", ). @@ -154,10 +171,10 @@ func (r *FeedRepo) Find(ctx context.Context) ([]domain.Feed, error) { for rows.Next() { var f domain.Feed - var apiKey, cookie, lastRunData sql.NullString + var apiKey, cookie, lastRunData, settings sql.NullString var lastRun sql.NullTime - if err := rows.Scan(&f.ID, &f.Indexer, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &lastRun, &lastRunData, &f.CreatedAt, &f.UpdatedAt); err != nil { + if err := rows.Scan(&f.ID, &f.Indexer, &f.Name, &f.Type, &f.Enabled, &f.URL, &f.Interval, &f.Timeout, &f.MaxAge, &apiKey, &cookie, &lastRun, &lastRunData, &settings, &f.CreatedAt, &f.UpdatedAt); err != nil { return nil, errors.Wrap(err, "error scanning row") } @@ -166,6 +183,19 @@ func (r *FeedRepo) Find(ctx context.Context) ([]domain.Feed, error) { f.ApiKey = apiKey.String f.Cookie = cookie.String + f.Settings = &domain.FeedSettingsJSON{ + DownloadType: domain.FeedDownloadTypeTorrent, + } + + if settings.Valid { + var settingsJson domain.FeedSettingsJSON + if err = json.Unmarshal([]byte(settings.String), &settingsJson); err != nil { + return nil, errors.Wrap(err, "error unmarshal settings") + } + + f.Settings = &settingsJson + } + feeds = append(feeds, f) } @@ -200,6 +230,11 @@ func (r *FeedRepo) GetLastRunDataByID(ctx context.Context, id int) (string, erro } func (r *FeedRepo) Store(ctx context.Context, feed *domain.Feed) error { + settings, err := json.Marshal(feed.Settings) + if err != nil { + return errors.Wrap(err, "error marshaling feed settings json data") + } + queryBuilder := r.db.squirrel. Insert("feed"). Columns( @@ -211,6 +246,7 @@ func (r *FeedRepo) Store(ctx context.Context, feed *domain.Feed) error { "timeout", "api_key", "indexer_id", + "settings", ). Values( feed.Name, @@ -221,6 +257,7 @@ func (r *FeedRepo) Store(ctx context.Context, feed *domain.Feed) error { feed.Timeout, feed.ApiKey, feed.IndexerID, + settings, ). Suffix("RETURNING id").RunWith(r.db.handler) @@ -236,6 +273,11 @@ func (r *FeedRepo) Store(ctx context.Context, feed *domain.Feed) error { } func (r *FeedRepo) Update(ctx context.Context, feed *domain.Feed) error { + settings, err := json.Marshal(feed.Settings) + if err != nil { + return errors.Wrap(err, "error marshaling feed settings json data") + } + queryBuilder := r.db.squirrel. Update("feed"). Set("name", feed.Name). @@ -247,6 +289,7 @@ func (r *FeedRepo) Update(ctx context.Context, feed *domain.Feed) error { Set("max_age", feed.MaxAge). Set("api_key", feed.ApiKey). Set("cookie", feed.Cookie). + Set("settings", settings). Set("updated_at", sq.Expr("CURRENT_TIMESTAMP")). Where(sq.Eq{"id": feed.ID}) diff --git a/internal/domain/feed.go b/internal/domain/feed.go index 54cd1de..0e3b4de 100644 --- a/internal/domain/feed.go +++ b/internal/domain/feed.go @@ -41,7 +41,7 @@ type Feed struct { Capabilities []string `json:"capabilities"` ApiKey string `json:"api_key"` Cookie string `json:"cookie"` - Settings map[string]string `json:"settings"` + Settings *FeedSettingsJSON `json:"settings"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` IndexerID int `json:"indexer_id,omitempty"` @@ -50,6 +50,10 @@ type Feed struct { LastRunData string `json:"last_run_data"` } +type FeedSettingsJSON struct { + DownloadType FeedDownloadType `json:"download_type"` +} + type FeedIndexer struct { ID int `json:"id"` Name string `json:"name"` @@ -63,6 +67,13 @@ const ( FeedTypeRSS FeedType = "RSS" ) +type FeedDownloadType string + +const ( + FeedDownloadTypeMagnet FeedDownloadType = "MAGNET" + FeedDownloadTypeTorrent FeedDownloadType = "TORRENT" +) + type FeedCacheItem struct { Bucket string `json:"bucket"` Key string `json:"key"` diff --git a/internal/domain/macros.go b/internal/domain/macros.go index 6d68f73..e66c54d 100644 --- a/internal/domain/macros.go +++ b/internal/domain/macros.go @@ -17,6 +17,7 @@ type Macro struct { TorrentHash string TorrentUrl string TorrentDataRawBytes []byte + MagnetURI string Indexer string Title string Resolution string @@ -44,6 +45,7 @@ func NewMacro(release Release) Macro { TorrentPathName: release.TorrentTmpFile, TorrentDataRawBytes: release.TorrentDataRawBytes, TorrentHash: release.TorrentHash, + MagnetURI: release.MagnetURI, Indexer: release.Indexer, Title: release.Title, Resolution: release.Resolution, diff --git a/internal/domain/release.go b/internal/domain/release.go index 4c0029a..d4e0352 100644 --- a/internal/domain/release.go +++ b/internal/domain/release.go @@ -46,6 +46,7 @@ type Release struct { Timestamp time.Time `json:"timestamp"` InfoURL string `json:"info_url"` TorrentURL string `json:"download_url"` + MagnetURI string `json:"-"` GroupID string `json:"group_id"` TorrentID string `json:"torrent_id"` TorrentTmpFile string `json:"-"` @@ -290,6 +291,10 @@ func (r *Release) DownloadTorrentFile() error { } func (r *Release) downloadTorrentFile(ctx context.Context) error { + if r.HasMagnetUri() { + return fmt.Errorf("error trying to download magnet link: %s", r.MagnetURI) + } + if r.TorrentURL == "" { return errors.New("download_file: url can't be empty") } else if r.TorrentTmpFile != "" { @@ -389,6 +394,81 @@ func (r *Release) downloadTorrentFile(ctx context.Context) error { return errFunc } +// HasMagnetUri check uf MagnetURI is set or empty +func (r *Release) HasMagnetUri() bool { + return r.MagnetURI != "" +} + +type magnetRoundTripper struct{} + +func (rt *magnetRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + if r.URL.Scheme == "magnet" { + responseBody := r.URL.String() + respReader := io.NopCloser(strings.NewReader(responseBody)) + + resp := &http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: respReader, + ContentLength: int64(len(responseBody)), + Header: map[string][]string{ + "Content-Type": {"text/plain"}, + "Location": {responseBody}, + }, + Proto: "HTTP/2.0", + ProtoMajor: 2, + } + + return resp, nil + } + + return http.DefaultTransport.RoundTrip(r) +} + +func (r *Release) ResolveMagnetUri(ctx context.Context) error { + if r.MagnetURI == "" { + return nil + } else if strings.HasPrefix(r.MagnetURI, "magnet:?") { + return nil + } + + client := http.Client{ + Transport: &magnetRoundTripper{}, + Timeout: time.Second * 60, + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.MagnetURI, nil) + if err != nil { + return errors.Wrap(err, "could not build request to resolve magnet uri") + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "autobrr") + + res, err := client.Do(req) + if err != nil { + return errors.Wrap(err, "could not make request to resolve magnet uri") + } + + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return errors.New("unexpected status code: %d", res.StatusCode) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return errors.Wrap(err, "could not read response body") + } + + magnet := string(body) + if magnet != "" { + r.MagnetURI = magnet + } + + return nil +} + func (r *Release) addRejection(reason string) { r.Rejections = append(r.Rejections, reason) } diff --git a/internal/feed/rss.go b/internal/feed/rss.go index 3d2772f..d7dd1ca 100644 --- a/internal/feed/rss.go +++ b/internal/feed/rss.go @@ -108,6 +108,11 @@ func (j *RSSJob) processItem(item *gofeed.Item) *domain.Release { rls.ParseString(item.Title) + if j.Feed.Settings != nil && j.Feed.Settings.DownloadType == domain.FeedDownloadTypeMagnet { + rls.MagnetURI = item.Link + rls.TorrentURL = "" + } + if len(item.Enclosures) > 0 { e := item.Enclosures[0] if e.Type == "application/x-bittorrent" && e.URL != "" { diff --git a/internal/feed/service.go b/internal/feed/service.go index c9fefee..a17849d 100644 --- a/internal/feed/service.go +++ b/internal/feed/service.go @@ -275,7 +275,7 @@ func (s *service) start() error { for _, feed := range feeds { feed := feed if err := s.startJob(&feed); err != nil { - s.log.Error().Err(err).Msg("failed to initialize torznab job") + s.log.Error().Err(err).Msgf("failed to initialize feed job: %s", feed.Name) continue } } diff --git a/internal/feed/torznab.go b/internal/feed/torznab.go index 227da47..99f3328 100644 --- a/internal/feed/torznab.go +++ b/internal/feed/torznab.go @@ -89,6 +89,11 @@ func (j *TorznabJob) process(ctx context.Context) error { rls.ParseString(item.Title) + if j.Feed.Settings != nil && j.Feed.Settings.DownloadType == domain.FeedDownloadTypeMagnet { + rls.MagnetURI = item.Link + rls.TorrentURL = "" + } + // Get freeleech percentage between 0 - 100. The value is ignored if // an error occurrs freeleechPercentage, err := parseFreeleechTorznab(item) diff --git a/internal/filter/service.go b/internal/filter/service.go index 071f84c..cd57eff 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -33,7 +33,7 @@ type Service interface { Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error Delete(ctx context.Context, filterID int) error - AdditionalSizeCheck(f domain.Filter, release *domain.Release) (bool, error) + AdditionalSizeCheck(ctx context.Context, f domain.Filter, release *domain.Release) (bool, error) CanDownloadShow(ctx context.Context, release *domain.Release) (bool, error) } @@ -336,7 +336,7 @@ func (s *service) CheckFilter(ctx context.Context, f domain.Filter, release *dom if release.AdditionalSizeCheckRequired { s.log.Debug().Msgf("filter.Service.CheckFilter: (%v) additional size check required", f.Name) - ok, err := s.AdditionalSizeCheck(f, release) + ok, err := s.AdditionalSizeCheck(ctx, f, release) if err != nil { s.log.Error().Stack().Err(err).Msgf("filter.Service.CheckFilter: (%v) additional size check error", f.Name) return false, err @@ -350,7 +350,7 @@ func (s *service) CheckFilter(ctx context.Context, f domain.Filter, release *dom // run external script if f.ExternalScriptEnabled && f.ExternalScriptCmd != "" { - exitCode, err := s.execCmd(release, f.ExternalScriptCmd, f.ExternalScriptArgs) + exitCode, err := s.execCmd(ctx, release, f.ExternalScriptCmd, f.ExternalScriptArgs) if err != nil { s.log.Error().Err(err).Msgf("filter.Service.CheckFilter: error executing external command for filter: %+v", f.Name) return false, err @@ -366,7 +366,7 @@ func (s *service) CheckFilter(ctx context.Context, f domain.Filter, release *dom // run external webhook if f.ExternalWebhookEnabled && f.ExternalWebhookHost != "" && f.ExternalWebhookData != "" { // run external scripts - statusCode, err := s.webhook(release, f.ExternalWebhookHost, f.ExternalWebhookData) + statusCode, err := s.webhook(ctx, release, f.ExternalWebhookHost, f.ExternalWebhookData) if err != nil { s.log.Error().Err(err).Msgf("filter.Service.CheckFilter: error executing external webhook for filter: %v", f.Name) return false, err @@ -404,7 +404,7 @@ func (s *service) CheckFilter(ctx context.Context, f domain.Filter, release *dom // Some indexers do not announce the size and if size (min,max) is set in a filter then it will need // additional size check. Some indexers have api implemented to fetch this data and for the others // it will download the torrent file to parse and make the size check. This is all to minimize the amount of downloads. -func (s *service) AdditionalSizeCheck(f domain.Filter, release *domain.Release) (bool, error) { +func (s *service) AdditionalSizeCheck(ctx context.Context, f domain.Filter, release *domain.Release) (bool, error) { // do additional size check against indexer api or torrent for size s.log.Debug().Msgf("filter.Service.AdditionalSizeCheck: (%v) additional size check required", f.Name) @@ -428,7 +428,7 @@ func (s *service) AdditionalSizeCheck(f domain.Filter, release *domain.Release) s.log.Trace().Msgf("filter.Service.AdditionalSizeCheck: (%v) preparing to download torrent metafile", f.Name) // if indexer doesn't have api, download torrent and add to tmpPath - if err := release.DownloadTorrentFile(); err != nil { + if err := release.DownloadTorrentFileCtx(ctx); err != nil { s.log.Error().Stack().Err(err).Msgf("filter.Service.AdditionalSizeCheck: (%v) could not download torrent file with id: '%v' from: %v", f.Name, release.TorrentID, release.Indexer) return false, err } @@ -485,11 +485,11 @@ func (s *service) CanDownloadShow(ctx context.Context, release *domain.Release) return s.releaseRepo.CanDownloadShow(ctx, release.Title, release.Season, release.Episode) } -func (s *service) execCmd(release *domain.Release, cmd string, args string) (int, error) { +func (s *service) execCmd(ctx context.Context, release *domain.Release, cmd string, args string) (int, error) { s.log.Debug().Msgf("filter exec release: %v", release.TorrentName) if release.TorrentTmpFile == "" && strings.Contains(args, "TorrentPathName") { - if err := release.DownloadTorrentFile(); err != nil { + if err := release.DownloadTorrentFileCtx(ctx); err != nil { return 0, errors.Wrap(err, "error downloading torrent file for release: %v", release.TorrentName) } } @@ -546,10 +546,10 @@ func (s *service) execCmd(release *domain.Release, cmd string, args string) (int return 0, nil } -func (s *service) webhook(release *domain.Release, url string, data string) (int, error) { +func (s *service) webhook(ctx context.Context, release *domain.Release, url string, data string) (int, error) { // if webhook data contains TorrentPathName or TorrentDataRawBytes, lets download the torrent file if release.TorrentTmpFile == "" && (strings.Contains(data, "TorrentPathName") || strings.Contains(data, "TorrentDataRawBytes")) { - if err := release.DownloadTorrentFile(); err != nil { + if err := release.DownloadTorrentFileCtx(ctx); err != nil { return 0, errors.Wrap(err, "webhook: could not download torrent file for release: %v", release.TorrentName) } } @@ -580,7 +580,7 @@ func (s *service) webhook(release *domain.Release, url string, data string) (int client := http.Client{Transport: t, Timeout: 15 * time.Second} - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBufferString(dataArgs)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBufferString(dataArgs)) if err != nil { return 0, errors.Wrap(err, "could not build request for webhook") } diff --git a/pkg/lidarr/lidarr.go b/pkg/lidarr/lidarr.go index 3e9ddd1..9e6bdc8 100644 --- a/pkg/lidarr/lidarr.go +++ b/pkg/lidarr/lidarr.go @@ -59,7 +59,8 @@ func New(config Config) Client { type Release struct { Title string `json:"title"` - DownloadUrl string `json:"downloadUrl"` + DownloadUrl string `json:"downloadUrl,omitempty"` + MagnetUrl string `json:"magnetUrl,omitempty"` Size int64 `json:"size"` Indexer string `json:"indexer"` DownloadProtocol string `json:"downloadProtocol"` diff --git a/pkg/porla/domain.go b/pkg/porla/domain.go index df3e446..0e0c63c 100644 --- a/pkg/porla/domain.go +++ b/pkg/porla/domain.go @@ -12,7 +12,8 @@ type SysVersionsPorla struct { type TorrentsAddReq struct { DownloadLimit int64 `json:"download_limit,omitempty"` SavePath string `json:"save_path,omitempty"` - Ti string `json:"ti"` + Ti string `json:"ti,omitempty"` + MagnetUri string `json:"magnet_uri,omitempty"` UploadLimit int64 `json:"upload_limit,omitempty"` } diff --git a/pkg/radarr/radarr.go b/pkg/radarr/radarr.go index 4841479..78fa844 100644 --- a/pkg/radarr/radarr.go +++ b/pkg/radarr/radarr.go @@ -58,7 +58,8 @@ func New(config Config) Client { type Release struct { Title string `json:"title"` - DownloadUrl string `json:"downloadUrl"` + DownloadUrl string `json:"downloadUrl,omitempty"` + MagnetUrl string `json:"magnetUrl,omitempty"` Size int64 `json:"size"` Indexer string `json:"indexer"` DownloadProtocol string `json:"downloadProtocol"` diff --git a/pkg/readarr/readarr.go b/pkg/readarr/readarr.go index 75697fa..aad80ab 100644 --- a/pkg/readarr/readarr.go +++ b/pkg/readarr/readarr.go @@ -61,7 +61,8 @@ func New(config Config) Client { type Release struct { Title string `json:"title"` - DownloadUrl string `json:"downloadUrl"` + DownloadUrl string `json:"downloadUrl,omitempty"` + MagnetUrl string `json:"magnetUrl,omitempty"` Size int64 `json:"size"` Indexer string `json:"indexer"` DownloadProtocol string `json:"downloadProtocol"` diff --git a/pkg/sonarr/sonarr.go b/pkg/sonarr/sonarr.go index de29007..0b3e0f7 100644 --- a/pkg/sonarr/sonarr.go +++ b/pkg/sonarr/sonarr.go @@ -61,7 +61,8 @@ func New(config Config) Client { type Release struct { Title string `json:"title"` - DownloadUrl string `json:"downloadUrl"` + DownloadUrl string `json:"downloadUrl,omitempty"` + MagnetUrl string `json:"magnetUrl,omitempty"` Size int64 `json:"size"` Indexer string `json:"indexer"` DownloadProtocol string `json:"downloadProtocol"` diff --git a/pkg/whisparr/whisparr.go b/pkg/whisparr/whisparr.go index 667aa57..6e522ec 100644 --- a/pkg/whisparr/whisparr.go +++ b/pkg/whisparr/whisparr.go @@ -57,7 +57,8 @@ func New(config Config) Client { type Release struct { Title string `json:"title"` - DownloadUrl string `json:"downloadUrl"` + DownloadUrl string `json:"downloadUrl,omitempty"` + MagnetUrl string `json:"magnetUrl,omitempty"` Size int64 `json:"size"` Indexer string `json:"indexer"` DownloadProtocol string `json:"downloadProtocol"` diff --git a/web/src/components/inputs/input_wide.tsx b/web/src/components/inputs/input_wide.tsx index f543361..a2270a4 100644 --- a/web/src/components/inputs/input_wide.tsx +++ b/web/src/components/inputs/input_wide.tsx @@ -213,10 +213,10 @@ export const SwitchGroupWide = ({ tooltip, defaultValue }: SwitchGroupWideProps) => ( -