autobrr/internal/list/service.go
Fabricio Silva b724429b97
feat(lists): add anilist support (#1949)
* fix(lists): clear selected list

* chore(web): improve onChange set values for select_wide

* feat(web): add anilist lists

* feat(web): Filter is required on ListForm

* fix(web): ListForm reset url when change type

* feat(lists): add anilist support

* feat(lists): filter duplicates for anilist

* feat(anilist): handle special characters

* fix(lists): better title matching
fix(lists): add alternatives to apostrophe replacement

* test(title): add some anime cases

* feat(anilist): replace unicodes with regex

* feat(lists): move additional anilist processing to autobrr instead of brr api

* feat(lists): clean Unicode Block “Latin Extended-A” chars

---------

Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
2025-01-31 19:17:23 +01:00

343 lines
8.2 KiB
Go

// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
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)
case domain.ListTypeAniList:
err = s.anilist(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")
}
}