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

* feat(lists): integrate Omegabrr * feat(lists): add missing lists index * feat(lists): add db repo * feat(lists): add db migrations * feat(lists): labels * feat(lists): url lists and more arrs * fix(lists): db migrations client_id wrong type * fix(lists): db fields * feat(lists): create list form wip * feat(lists): show in list and create * feat(lists): update and delete * feat(lists): trigger via webhook * feat(lists): add webhook handler * fix(arr): encode json to pointer * feat(lists): rename endpoint to lists * feat(lists): fetch tags from arr * feat(lists): process plaintext lists * feat(lists): add background refresh job * run every 6th hour with a random start delay between 1-35 seconds * feat(lists): refresh on save and improve logging * feat(lists): cast arr client to pointer * feat(lists): improve error handling * feat(lists): reset shows field with match release * feat(lists): filter opts all lists * feat(lists): trigger on update if enabled * feat(lists): update option for lists * feat(lists): show connected filters in list * feat(lists): missing listSvc dep * feat(lists): cleanup * feat(lists): typo arr list * feat(lists): radarr include original * feat(lists): rename ExcludeAlternateTitle to IncludeAlternateTitle * fix(lists): arr client type conversion to pointer * fix(actions): only log panic recover if err not nil * feat(lists): show spinner on save * feat(lists): show icon in filters list * feat(lists): change icon color in filters list * feat(lists): delete relations on filter delete
337 lines
8 KiB
Go
337 lines
8 KiB
Go
package list
|
|
|
|
import (
|
|
"context"
|
|
stdErr "errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/autobrr/autobrr/internal/domain"
|
|
"github.com/autobrr/autobrr/internal/download_client"
|
|
"github.com/autobrr/autobrr/internal/filter"
|
|
"github.com/autobrr/autobrr/internal/logger"
|
|
"github.com/autobrr/autobrr/internal/scheduler"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
type Service interface {
|
|
List(ctx context.Context) ([]*domain.List, error)
|
|
FindByID(ctx context.Context, id int64) (*domain.List, error)
|
|
Store(ctx context.Context, list *domain.List) error
|
|
Update(ctx context.Context, list *domain.List) error
|
|
Delete(ctx context.Context, id int64) error
|
|
RefreshAll(ctx context.Context) error
|
|
RefreshList(ctx context.Context, listID int64) error
|
|
RefreshArrLists(ctx context.Context) error
|
|
RefreshOtherLists(ctx context.Context) error
|
|
Start()
|
|
}
|
|
|
|
type service struct {
|
|
log zerolog.Logger
|
|
repo domain.ListRepo
|
|
|
|
httpClient *http.Client
|
|
scheduler scheduler.Service
|
|
downloadClientSvc download_client.Service
|
|
filterSvc filter.Service
|
|
}
|
|
|
|
func NewService(log logger.Logger, repo domain.ListRepo, downloadClientSvc download_client.Service, filterSvc filter.Service, schedulerSvc scheduler.Service) Service {
|
|
return &service{
|
|
log: log.With().Str("module", "list").Logger(),
|
|
repo: repo,
|
|
httpClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
downloadClientSvc: downloadClientSvc,
|
|
filterSvc: filterSvc,
|
|
scheduler: schedulerSvc,
|
|
}
|
|
}
|
|
|
|
func (s *service) List(ctx context.Context) ([]*domain.List, error) {
|
|
data, err := s.repo.List(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// attach filters
|
|
for _, list := range data {
|
|
filters, err := s.repo.GetListFilters(ctx, list.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list.Filters = filters
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (s *service) FindByID(ctx context.Context, id int64) (*domain.List, error) {
|
|
list, err := s.repo.FindByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// attach filters
|
|
filters, err := s.repo.GetListFilters(ctx, list.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list.Filters = filters
|
|
|
|
return list, nil
|
|
}
|
|
|
|
func (s *service) Store(ctx context.Context, list *domain.List) error {
|
|
if err := list.Validate(); err != nil {
|
|
s.log.Error().Err(err).Msgf("could not validate list %s", list.Name)
|
|
return err
|
|
}
|
|
|
|
if err := s.repo.Store(ctx, list); err != nil {
|
|
s.log.Error().Err(err).Msgf("could not store list %s", list.Name)
|
|
return err
|
|
}
|
|
|
|
s.log.Debug().Msgf("successfully created list %s", list.Name)
|
|
|
|
if list.Enabled {
|
|
if err := s.refreshList(ctx, list); err != nil {
|
|
s.log.Error().Err(err).Msgf("could not refresh list %s", list.Name)
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) Update(ctx context.Context, list *domain.List) error {
|
|
if err := list.Validate(); err != nil {
|
|
s.log.Error().Err(err).Msgf("could not validate list %s", list.Name)
|
|
return err
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, list); err != nil {
|
|
s.log.Error().Err(err).Msgf("could not update list %s", list.Name)
|
|
return err
|
|
}
|
|
|
|
s.log.Debug().Msgf("successfully updated list %s", list.Name)
|
|
|
|
if list.Enabled {
|
|
if err := s.refreshList(ctx, list); err != nil {
|
|
s.log.Error().Err(err).Msgf("could not refresh list %s", list.Name)
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) Delete(ctx context.Context, id int64) error {
|
|
err := s.repo.Delete(ctx, id)
|
|
if err != nil {
|
|
s.log.Error().Err(err).Msgf("could not delete list by id %d", id)
|
|
return err
|
|
}
|
|
|
|
s.log.Debug().Msgf("successfully deleted list %d", id)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) RefreshAll(ctx context.Context) error {
|
|
lists, err := s.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.log.Debug().Msgf("found %d lists to refresh", len(lists))
|
|
|
|
if err := s.refreshAll(ctx, lists); err != nil {
|
|
return err
|
|
}
|
|
|
|
s.log.Debug().Msgf("successfully refreshed all lists")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) refreshAll(ctx context.Context, lists []*domain.List) error {
|
|
var processingErrors []error
|
|
|
|
for _, listItem := range lists {
|
|
if !listItem.Enabled {
|
|
s.log.Debug().Msgf("list %s is disabled, skipping...", listItem.Name)
|
|
continue
|
|
}
|
|
|
|
if err := s.refreshList(ctx, listItem); err != nil {
|
|
s.log.Error().Err(err).Str("type", string(listItem.Type)).Str("list", listItem.Name).Msgf("error while refreshing %s, continuing with other lists", listItem.Type)
|
|
|
|
processingErrors = append(processingErrors, errors.Wrapf(err, "error while refreshing %s", listItem.Name))
|
|
}
|
|
}
|
|
|
|
if len(processingErrors) > 0 {
|
|
err := stdErr.Join(processingErrors...)
|
|
|
|
s.log.Error().Err(err).Msg("Errors encountered during processing Arrs:")
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) refreshList(ctx context.Context, listItem *domain.List) error {
|
|
s.log.Debug().Msgf("refresh list %s - %s", listItem.Type, listItem.Name)
|
|
|
|
var err error
|
|
|
|
switch listItem.Type {
|
|
case domain.ListTypeRadarr:
|
|
err = s.radarr(ctx, listItem)
|
|
|
|
case domain.ListTypeSonarr:
|
|
err = s.sonarr(ctx, listItem)
|
|
|
|
case domain.ListTypeWhisparr:
|
|
err = s.sonarr(ctx, listItem)
|
|
|
|
case domain.ListTypeReadarr:
|
|
err = s.readarr(ctx, listItem)
|
|
|
|
case domain.ListTypeLidarr:
|
|
err = s.lidarr(ctx, listItem)
|
|
|
|
case domain.ListTypeMDBList:
|
|
err = s.mdblist(ctx, listItem)
|
|
|
|
case domain.ListTypeMetacritic:
|
|
err = s.metacritic(ctx, listItem)
|
|
|
|
case domain.ListTypeSteam:
|
|
err = s.steam(ctx, listItem)
|
|
|
|
case domain.ListTypeTrakt:
|
|
err = s.trakt(ctx, listItem)
|
|
|
|
case domain.ListTypePlaintext:
|
|
err = s.plaintext(ctx, listItem)
|
|
|
|
default:
|
|
err = errors.Errorf("unsupported list type: %s", listItem.Type)
|
|
}
|
|
|
|
if err != nil {
|
|
s.log.Error().Err(err).Str("type", string(listItem.Type)).Str("list", listItem.Name).Msgf("error refreshing %s list", listItem.Name)
|
|
|
|
// update last run for list and set errs and status
|
|
listItem.LastRefreshStatus = domain.ListRefreshStatusError
|
|
listItem.LastRefreshData = err.Error()
|
|
listItem.LastRefreshTime = time.Now()
|
|
|
|
if updateErr := s.repo.UpdateLastRefresh(ctx, listItem); updateErr != nil {
|
|
s.log.Error().Err(updateErr).Str("type", string(listItem.Type)).Str("list", listItem.Name).Msgf("error updating last refresh for %s list", listItem.Name)
|
|
return updateErr
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
listItem.LastRefreshStatus = domain.ListRefreshStatusSuccess
|
|
//listItem.LastRefreshData = err.Error()
|
|
listItem.LastRefreshTime = time.Now()
|
|
|
|
if updateErr := s.repo.UpdateLastRefresh(ctx, listItem); updateErr != nil {
|
|
s.log.Error().Err(updateErr).Str("type", string(listItem.Type)).Str("list", listItem.Name).Msgf("error updating last refresh for %s list", listItem.Name)
|
|
return updateErr
|
|
}
|
|
|
|
s.log.Debug().Msgf("successfully refreshed list %s", listItem.Name)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) RefreshList(ctx context.Context, listID int64) error {
|
|
list, err := s.FindByID(ctx, listID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.refreshList(ctx, list); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) RefreshArrLists(ctx context.Context) error {
|
|
lists, err := s.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var selectedLists []*domain.List
|
|
for _, list := range lists {
|
|
if list.ListTypeArr() && list.Enabled {
|
|
selectedLists = append(selectedLists, list)
|
|
}
|
|
}
|
|
|
|
if err := s.refreshAll(ctx, selectedLists); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) RefreshOtherLists(ctx context.Context) error {
|
|
lists, err := s.List(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var selectedLists []*domain.List
|
|
for _, list := range lists {
|
|
if list.ListTypeList() && list.Enabled {
|
|
selectedLists = append(selectedLists, list)
|
|
}
|
|
}
|
|
|
|
if err := s.refreshAll(ctx, selectedLists); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// scheduleJob start list updater in the background
|
|
func (s *service) scheduleJob() error {
|
|
identifierKey := "lists-updater"
|
|
|
|
job := NewRefreshListsJob(s.log.With().Str("job", identifierKey).Logger(), s)
|
|
|
|
// schedule job to run every 6th hour
|
|
id, err := s.scheduler.AddJob(job, "0 */6 * * *", identifierKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.log.Debug().Msgf("scheduled job with id %d", id)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *service) Start() {
|
|
if err := s.scheduleJob(); err != nil {
|
|
s.log.Error().Err(err).Msg("error while scheduling job")
|
|
}
|
|
}
|