mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
feat(lists): integrate Omegabrr (#1885)
* 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
This commit is contained in:
parent
b68ae334ca
commit
221bc35371
77 changed files with 5025 additions and 254 deletions
172
internal/list/process_arr_lidarr.go
Normal file
172
internal/list/process_arr_lidarr.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr/lidarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *service) lidarr(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("list", list.Name).Str("type", "lidarr").Int("client", list.ClientID).Logger()
|
||||
|
||||
l.Debug().Msgf("gathering titles...")
|
||||
|
||||
titles, artists, err := s.processLidarr(ctx, list, &l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Debug().Msgf("got %d filter titles", len(titles))
|
||||
|
||||
// Process titles
|
||||
var processedTitles []string
|
||||
for _, title := range titles {
|
||||
processedTitles = append(processedTitles, processTitle(title, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
if len(processedTitles) == 0 {
|
||||
l.Debug().Msgf("no titles found to update for list: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update filter based on MatchRelease
|
||||
var f domain.FilterUpdate
|
||||
|
||||
if list.MatchRelease {
|
||||
joinedTitles := strings.Join(processedTitles, ",")
|
||||
if len(joinedTitles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
f.MatchReleases = &joinedTitles
|
||||
} else {
|
||||
// Process artists only if MatchRelease is false
|
||||
var processedArtists []string
|
||||
for _, artist := range artists {
|
||||
processedArtists = append(processedArtists, processTitle(artist, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(processedTitles, ",")
|
||||
|
||||
l.Trace().Str("albums", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
joinedArtists := strings.Join(processedArtists, ",")
|
||||
if len(joinedTitles) == 0 && len(joinedArtists) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Trace().Str("artists", joinedArtists).Msgf("found %d titles", len(joinedArtists))
|
||||
|
||||
f.Albums = &joinedTitles
|
||||
f.Artists = &joinedArtists
|
||||
}
|
||||
|
||||
//joinedTitles := strings.Join(titles, ",")
|
||||
//
|
||||
//l.Trace().Msgf("%v", joinedTitles)
|
||||
//
|
||||
//if len(joinedTitles) == 0 {
|
||||
// return nil
|
||||
//}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
l.Debug().Msgf("updating filter: %v", filter.ID)
|
||||
|
||||
f.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, f); err != nil {
|
||||
return errors.Wrap(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) processLidarr(ctx context.Context, list *domain.List, logger *zerolog.Logger) ([]string, []string, error) {
|
||||
downloadClient, err := s.downloadClientSvc.GetClient(ctx, int32(list.ClientID))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "could not get client with id %d", list.ClientID)
|
||||
}
|
||||
|
||||
if !downloadClient.Enabled {
|
||||
return nil, nil, errors.New("client %s %s not enabled", downloadClient.Type, downloadClient.Name)
|
||||
}
|
||||
|
||||
client := downloadClient.Client.(*lidarr.Client)
|
||||
|
||||
//var tags []*arr.Tag
|
||||
//if len(list.TagsExclude) > 0 || len(list.TagsInclude) > 0 {
|
||||
// t, err := client.GetTags(ctx)
|
||||
// if err != nil {
|
||||
// logger.Debug().Msg("could not get tags")
|
||||
// }
|
||||
// tags = t
|
||||
//}
|
||||
|
||||
albums, err := client.GetAlbums(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
logger.Debug().Msgf("found %d albums to process", len(albums))
|
||||
|
||||
var titles []string
|
||||
var artists []string
|
||||
seenArtists := make(map[string]struct{})
|
||||
|
||||
for _, album := range albums {
|
||||
if !list.ShouldProcessItem(album.Monitored) {
|
||||
continue
|
||||
}
|
||||
|
||||
//if len(list.TagsInclude) > 0 {
|
||||
// if len(album.Tags) == 0 {
|
||||
// continue
|
||||
// }
|
||||
// if !containsTag(tags, album.Tags, list.TagsInclude) {
|
||||
// continue
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//if len(list.TagsExclude) > 0 {
|
||||
// if containsTag(tags, album.Tags, list.TagsExclude) {
|
||||
// continue
|
||||
// }
|
||||
//}
|
||||
|
||||
// Fetch the artist details
|
||||
artist, err := client.GetArtistByID(ctx, album.ArtistID)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("Error fetching artist details for album: %v", album.Title)
|
||||
continue // Skip this album if there's an error fetching the artist
|
||||
}
|
||||
|
||||
if artist.Monitored {
|
||||
processedTitles := processTitle(album.Title, list.MatchRelease)
|
||||
titles = append(titles, processedTitles...)
|
||||
|
||||
// Debug logging
|
||||
logger.Debug().Msgf("Processing artist: %s", artist.ArtistName)
|
||||
|
||||
if _, exists := seenArtists[artist.ArtistName]; !exists {
|
||||
artists = append(artists, artist.ArtistName)
|
||||
seenArtists[artist.ArtistName] = struct{}{}
|
||||
logger.Debug().Msgf("Added artist: %s", artist.ArtistName) // Log when an artist is added
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//sort.Strings(titles)
|
||||
logger.Debug().Msgf("Processed %d monitored albums with monitored artists, created %d titles, found %d unique artists", len(titles), len(titles), len(artists))
|
||||
|
||||
return titles, artists, nil
|
||||
}
|
146
internal/list/process_arr_radarr.go
Normal file
146
internal/list/process_arr_radarr.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/radarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *service) radarr(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("list", list.Name).Str("type", "radarr").Int("client", list.ClientID).Logger()
|
||||
|
||||
l.Debug().Msgf("gathering titles...")
|
||||
|
||||
titles, err := s.processRadarr(ctx, list, &l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Debug().Msgf("got %d filter titles", len(titles))
|
||||
|
||||
if len(titles) == 0 {
|
||||
l.Debug().Msgf("no titles found to update for list: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(titles, ",")
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{Shows: &joinedTitles}
|
||||
|
||||
if list.MatchRelease {
|
||||
filterUpdate.Shows = &nullString
|
||||
filterUpdate.MatchReleases = &joinedTitles
|
||||
}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
l.Debug().Msgf("updating filter: %v", filter.ID)
|
||||
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrap(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) processRadarr(ctx context.Context, list *domain.List, logger *zerolog.Logger) ([]string, error) {
|
||||
downloadClient, err := s.downloadClientSvc.GetClient(ctx, int32(list.ClientID))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get client with id %d", list.ClientID)
|
||||
}
|
||||
|
||||
if !downloadClient.Enabled {
|
||||
return nil, errors.New("client %s %s not enabled", downloadClient.Type, downloadClient.Name)
|
||||
}
|
||||
|
||||
client := downloadClient.Client.(*radarr.Client)
|
||||
|
||||
var tags []*arr.Tag
|
||||
if len(list.TagsExclude) > 0 || len(list.TagsInclude) > 0 {
|
||||
t, err := client.GetTags(ctx)
|
||||
if err != nil {
|
||||
logger.Debug().Msg("could not get tags")
|
||||
}
|
||||
tags = t
|
||||
}
|
||||
|
||||
movies, err := client.GetMovies(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug().Msgf("found %d movies to process", len(movies))
|
||||
|
||||
titleSet := make(map[string]struct{})
|
||||
var processedTitles int
|
||||
|
||||
for _, movie := range movies {
|
||||
if !list.ShouldProcessItem(movie.Monitored) {
|
||||
continue
|
||||
}
|
||||
|
||||
//if !s.shouldProcessItem(movie.Monitored, list) {
|
||||
// continue
|
||||
//}
|
||||
|
||||
if len(list.TagsInclude) > 0 {
|
||||
if len(movie.Tags) == 0 {
|
||||
continue
|
||||
}
|
||||
if !containsTag(tags, movie.Tags, list.TagsInclude) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(list.TagsExclude) > 0 {
|
||||
if containsTag(tags, movie.Tags, list.TagsExclude) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
processedTitles++
|
||||
|
||||
// Taking the international title and the original title and appending them to the titles array.
|
||||
for _, title := range []string{movie.Title, movie.OriginalTitle} {
|
||||
if title != "" {
|
||||
for _, t := range processTitle(title, list.MatchRelease) {
|
||||
titleSet[t] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if list.IncludeAlternateTitles {
|
||||
for _, title := range movie.AlternateTitles {
|
||||
altTitles := processTitle(title.Title, list.MatchRelease)
|
||||
for _, altTitle := range altTitles {
|
||||
titleSet[altTitle] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uniqueTitles := make([]string, 0, len(titleSet))
|
||||
for title := range titleSet {
|
||||
uniqueTitles = append(uniqueTitles, title)
|
||||
}
|
||||
|
||||
sort.Strings(uniqueTitles)
|
||||
logger.Debug().Msgf("from a total of %d movies we found %d titles and created %d release titles", len(movies), processedTitles, len(uniqueTitles))
|
||||
|
||||
return uniqueTitles, nil
|
||||
}
|
||||
|
||||
var nullString = ""
|
113
internal/list/process_arr_readarr.go
Normal file
113
internal/list/process_arr_readarr.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr/readarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *service) readarr(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("list", list.Name).Str("type", "readarr").Int("client", list.ClientID).Logger()
|
||||
|
||||
l.Debug().Msgf("gathering titles...")
|
||||
|
||||
titles, err := s.processReadarr(ctx, list, &l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Debug().Msgf("got %d filter titles", len(titles))
|
||||
|
||||
if len(titles) == 0 {
|
||||
l.Debug().Msgf("no titles found to update for list: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(titles, ",")
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{MatchReleases: &joinedTitles}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
l.Debug().Msgf("updating filter: %v", filter.ID)
|
||||
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrap(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) processReadarr(ctx context.Context, list *domain.List, logger *zerolog.Logger) ([]string, error) {
|
||||
downloadClient, err := s.downloadClientSvc.GetClient(ctx, int32(list.ClientID))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get client with id %d", list.ClientID)
|
||||
}
|
||||
|
||||
if !downloadClient.Enabled {
|
||||
return nil, errors.New("client %s %s not enabled", downloadClient.Type, downloadClient.Name)
|
||||
}
|
||||
|
||||
client := downloadClient.Client.(*readarr.Client)
|
||||
|
||||
//var tags []*arr.Tag
|
||||
//if len(list.TagsExclude) > 0 || len(list.TagsInclude) > 0 {
|
||||
// t, err := client.GetTags(ctx)
|
||||
// if err != nil {
|
||||
// logger.Debug().Msg("could not get tags")
|
||||
// }
|
||||
// tags = t
|
||||
//}
|
||||
|
||||
books, err := client.GetBooks(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug().Msgf("found %d books to process", len(books))
|
||||
|
||||
var titles []string
|
||||
var processedTitles int
|
||||
|
||||
for _, book := range books {
|
||||
if !list.ShouldProcessItem(book.Monitored) {
|
||||
continue
|
||||
}
|
||||
|
||||
//if len(list.TagsInclude) > 0 {
|
||||
// if len(book.Tags) == 0 {
|
||||
// continue
|
||||
// }
|
||||
// if !containsTag(tags, book.Tags, list.TagsInclude) {
|
||||
// continue
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//if len(list.TagsExclude) > 0 {
|
||||
// if containsTag(tags, book.Tags, list.TagsExclude) {
|
||||
// continue
|
||||
// }
|
||||
//}
|
||||
|
||||
processedTitles++
|
||||
|
||||
titles = append(titles, processTitle(book.Title, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
sort.Strings(titles)
|
||||
logger.Debug().Msgf("from a total of %d books we found %d titles and created %d release titles", len(books), processedTitles, len(titles))
|
||||
|
||||
return titles, nil
|
||||
}
|
147
internal/list/process_arr_sonarr.go
Normal file
147
internal/list/process_arr_sonarr.go
Normal file
|
@ -0,0 +1,147 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/sonarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func (s *service) sonarr(ctx context.Context, list *domain.List) error {
|
||||
var arrType string
|
||||
if list.Type == domain.ListTypeWhisparr {
|
||||
arrType = "whisparr"
|
||||
} else {
|
||||
arrType = "sonarr"
|
||||
}
|
||||
|
||||
l := s.log.With().Str("list", list.Name).Str("type", arrType).Int("client", list.ClientID).Logger()
|
||||
|
||||
l.Debug().Msgf("gathering titles...")
|
||||
|
||||
titles, err := s.processSonarr(ctx, list, &l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Debug().Msgf("got %d filter titles", len(titles))
|
||||
|
||||
if len(titles) == 0 {
|
||||
l.Debug().Msgf("no titles found to update for list: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(titles, ",")
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{Shows: &joinedTitles}
|
||||
|
||||
if list.MatchRelease {
|
||||
filterUpdate.Shows = &nullString
|
||||
filterUpdate.MatchReleases = &joinedTitles
|
||||
}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
l.Debug().Msgf("updating filter: %v", filter.ID)
|
||||
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrap(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) processSonarr(ctx context.Context, list *domain.List, logger *zerolog.Logger) ([]string, error) {
|
||||
downloadClient, err := s.downloadClientSvc.GetClient(ctx, int32(list.ClientID))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not get client with id %d", list.ClientID)
|
||||
}
|
||||
|
||||
if !downloadClient.Enabled {
|
||||
return nil, errors.New("client %s %s not enabled", downloadClient.Type, downloadClient.Name)
|
||||
}
|
||||
|
||||
client := downloadClient.Client.(*sonarr.Client)
|
||||
|
||||
var tags []*arr.Tag
|
||||
if len(list.TagsExclude) > 0 || len(list.TagsInclude) > 0 {
|
||||
t, err := client.GetTags(ctx)
|
||||
if err != nil {
|
||||
logger.Debug().Msg("could not get tags")
|
||||
}
|
||||
tags = t
|
||||
}
|
||||
|
||||
shows, err := client.GetAllSeries(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug().Msgf("found %d shows to process", len(shows))
|
||||
|
||||
titleSet := make(map[string]struct{})
|
||||
var processedTitles int
|
||||
|
||||
for _, show := range shows {
|
||||
if !list.ShouldProcessItem(show.Monitored) {
|
||||
continue
|
||||
}
|
||||
|
||||
//if !s.shouldProcessItem(show.Monitored, list) {
|
||||
// continue
|
||||
//}
|
||||
|
||||
if len(list.TagsInclude) > 0 {
|
||||
if len(show.Tags) == 0 {
|
||||
continue
|
||||
}
|
||||
if !containsTag(tags, show.Tags, list.TagsInclude) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(list.TagsExclude) > 0 {
|
||||
if containsTag(tags, show.Tags, list.TagsExclude) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
processedTitles++
|
||||
|
||||
titles := processTitle(show.Title, list.MatchRelease)
|
||||
for _, title := range titles {
|
||||
titleSet[title] = struct{}{}
|
||||
}
|
||||
|
||||
if list.IncludeAlternateTitles {
|
||||
for _, title := range show.AlternateTitles {
|
||||
altTitles := processTitle(title.Title, list.MatchRelease)
|
||||
for _, altTitle := range altTitles {
|
||||
titleSet[altTitle] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uniqueTitles := make([]string, 0, len(titleSet))
|
||||
for title := range titleSet {
|
||||
uniqueTitles = append(uniqueTitles, title)
|
||||
}
|
||||
|
||||
sort.Strings(uniqueTitles)
|
||||
logger.Debug().Msgf("from a total of %d shows we found %d titles and created %d release titles", len(shows), processedTitles, len(uniqueTitles))
|
||||
|
||||
return uniqueTitles, nil
|
||||
}
|
87
internal/list/process_list_mdblist.go
Normal file
87
internal/list/process_list_mdblist.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *service) mdblist(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("type", "mdblist").Str("list", list.Name).Logger()
|
||||
|
||||
if list.URL == "" {
|
||||
return errors.New("no URL provided for Mdblist")
|
||||
}
|
||||
|
||||
//var titles []string
|
||||
|
||||
//green := color.New(color.FgGreen).SprintFunc()
|
||||
l.Debug().Msgf("fetching titles from %s", list.URL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, list.URL, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not make new request for URL: %s", list.URL)
|
||||
}
|
||||
|
||||
list.SetRequestHeaders(req)
|
||||
|
||||
//setUserAgent(req)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
var data []struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return errors.Wrapf(err, "failed to decode JSON data from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
filterTitles := []string{}
|
||||
for _, item := range data {
|
||||
filterTitles = append(filterTitles, processTitle(item.Title, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
if len(filterTitles) == 0 {
|
||||
l.Debug().Msgf("no titles found to update for list: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(filterTitles, ",")
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{Shows: &joinedTitles}
|
||||
|
||||
if list.MatchRelease {
|
||||
filterUpdate.Shows = &nullString
|
||||
filterUpdate.MatchReleases = &joinedTitles
|
||||
}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
l.Debug().Msgf("updating filter: %v", filter.ID)
|
||||
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrapf(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
123
internal/list/process_list_metacritic.go
Normal file
123
internal/list/process_list_metacritic.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *service) metacritic(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("type", "metacritic").Str("list", list.Name).Logger()
|
||||
|
||||
if list.URL == "" {
|
||||
return errors.New("no URL provided for metacritic")
|
||||
}
|
||||
|
||||
l.Debug().Msgf("fetching titles from %s", list.URL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, list.URL, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not make new request for URL: %s", list.URL)
|
||||
}
|
||||
|
||||
list.SetRequestHeaders(req)
|
||||
|
||||
//setUserAgent(req)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return errors.Errorf("No endpoint found at %v. (404 Not Found)", list.URL)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "application/json") {
|
||||
return errors.Wrapf(err, "unexpected content type for URL: %s expected application/json got %s", list.URL, contentType)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Title string `json:"title"`
|
||||
Albums []struct {
|
||||
Artist string `json:"artist"`
|
||||
Title string `json:"title"`
|
||||
} `json:"albums"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return errors.Wrapf(err, "failed to decode JSON data from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
var titles []string
|
||||
var artists []string
|
||||
|
||||
for _, album := range data.Albums {
|
||||
titles = append(titles, album.Title)
|
||||
artists = append(artists, album.Artist)
|
||||
}
|
||||
|
||||
// Deduplicate artists
|
||||
uniqueArtists := []string{}
|
||||
seenArtists := map[string]struct{}{}
|
||||
for _, artist := range artists {
|
||||
if _, ok := seenArtists[artist]; !ok {
|
||||
uniqueArtists = append(uniqueArtists, artist)
|
||||
seenArtists[artist] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
filterTitles := []string{}
|
||||
for _, title := range titles {
|
||||
filterTitles = append(filterTitles, processTitle(title, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
filterArtists := []string{}
|
||||
for _, artist := range uniqueArtists {
|
||||
filterArtists = append(filterArtists, processTitle(artist, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
if len(filterTitles) == 0 && len(filterArtists) == 0 {
|
||||
l.Debug().Msgf("no titles found to update filter: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedArtists := strings.Join(filterArtists, ",")
|
||||
joinedTitles := strings.Join(filterTitles, ",")
|
||||
|
||||
l.Trace().Str("albums", joinedTitles).Msgf("found %d album titles", len(joinedTitles))
|
||||
l.Trace().Str("artists", joinedTitles).Msgf("found %d artit titles", len(joinedArtists))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{Albums: &joinedTitles, Artists: &joinedArtists}
|
||||
|
||||
if list.MatchRelease {
|
||||
filterUpdate.Albums = &nullString
|
||||
filterUpdate.Artists = &nullString
|
||||
filterUpdate.MatchReleases = &joinedTitles
|
||||
}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
l.Debug().Msgf("updating filter: %v", filter.ID)
|
||||
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrapf(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
96
internal/list/process_list_plaintext.go
Normal file
96
internal/list/process_list_plaintext.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *service) plaintext(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("type", "plaintext").Str("list", list.Name).Logger()
|
||||
|
||||
if list.URL == "" {
|
||||
return errors.New("no URL provided for plaintext")
|
||||
}
|
||||
|
||||
l.Debug().Msgf("fetching titles from %s", list.URL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, list.URL, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not make new request for URL: %s", list.URL)
|
||||
}
|
||||
|
||||
list.SetRequestHeaders(req)
|
||||
|
||||
//setUserAgent(req)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "text/plain") {
|
||||
return errors.Wrapf(err, "unexpected content type for URL: %s expected text/plain got %s", list.URL, contentType)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read response body from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
var titles []string
|
||||
titleLines := strings.Split(string(body), "\n")
|
||||
for _, titleLine := range titleLines {
|
||||
title := strings.TrimSpace(titleLine)
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
titles = append(titles, title)
|
||||
}
|
||||
|
||||
filterTitles := []string{}
|
||||
for _, title := range titles {
|
||||
filterTitles = append(filterTitles, processTitle(title, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
if len(filterTitles) == 0 {
|
||||
l.Debug().Msgf("no titles found to update for list: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(filterTitles, ",")
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{Shows: &joinedTitles}
|
||||
|
||||
if list.MatchRelease {
|
||||
filterUpdate.Shows = &nullString
|
||||
filterUpdate.MatchReleases = &joinedTitles
|
||||
}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
l.Debug().Msgf("updating filter: %v", filter.ID)
|
||||
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrapf(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
82
internal/list/process_list_steam.go
Normal file
82
internal/list/process_list_steam.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *service) steam(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("type", "steam").Str("list", list.Name).Logger()
|
||||
|
||||
if list.URL == "" {
|
||||
return errors.New("no URL provided for steam")
|
||||
}
|
||||
|
||||
l.Debug().Msgf("fetching titles from %s", list.URL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, list.URL, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not make new request for URL: %s", list.URL)
|
||||
}
|
||||
|
||||
list.SetRequestHeaders(req)
|
||||
|
||||
//setUserAgent(req)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("failed to fetch titles, non-OK status recieved: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var data map[string]struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return errors.Wrapf(err, "failed to decode JSON data from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
var titles []string
|
||||
for _, item := range data {
|
||||
titles = append(titles, item.Name)
|
||||
}
|
||||
|
||||
filterTitles := []string{}
|
||||
for _, title := range titles {
|
||||
filterTitles = append(filterTitles, processTitle(title, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
if len(filterTitles) == 0 {
|
||||
l.Debug().Msgf("no titles found for list to update: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(filterTitles, ",")
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{MatchReleases: &joinedTitles}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrapf(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
118
internal/list/process_list_trakt.go
Normal file
118
internal/list/process_list_trakt.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *service) trakt(ctx context.Context, list *domain.List) error {
|
||||
l := s.log.With().Str("type", "trakt").Str("list", list.Name).Logger()
|
||||
|
||||
if list.URL == "" {
|
||||
errMsg := "no URL provided for steam"
|
||||
l.Error().Msg(errMsg)
|
||||
return fmt.Errorf(errMsg)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("fetching titles from %s", list.URL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, list.URL, nil)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msg("could not make new request")
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("trakt-api-version", "2")
|
||||
|
||||
if list.APIKey != "" {
|
||||
req.Header.Set("trakt-api-key", list.APIKey)
|
||||
}
|
||||
|
||||
list.SetRequestHeaders(req)
|
||||
|
||||
//setUserAgent(req)
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
l.Error().Err(err).Msgf("failed to fetch titles from URL: %s", list.URL)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
l.Error().Msgf("failed to fetch titles from URL: %s", list.URL)
|
||||
return fmt.Errorf("failed to fetch titles from URL: %s", list.URL)
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "application/json") {
|
||||
errMsg := fmt.Sprintf("invalid content type for URL: %s, content type should be application/json", list.URL)
|
||||
return fmt.Errorf(errMsg)
|
||||
}
|
||||
|
||||
var data []struct {
|
||||
Title string `json:"title"`
|
||||
Movie struct {
|
||||
Title string `json:"title"`
|
||||
} `json:"movie"`
|
||||
Show struct {
|
||||
Title string `json:"title"`
|
||||
} `json:"show"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
l.Error().Err(err).Msgf("failed to decode JSON data from URL: %s", list.URL)
|
||||
return err
|
||||
}
|
||||
|
||||
var titles []string
|
||||
for _, item := range data {
|
||||
titles = append(titles, item.Title)
|
||||
if item.Movie.Title != "" {
|
||||
titles = append(titles, item.Movie.Title)
|
||||
}
|
||||
if item.Show.Title != "" {
|
||||
titles = append(titles, item.Show.Title)
|
||||
}
|
||||
}
|
||||
|
||||
filterTitles := []string{}
|
||||
for _, title := range titles {
|
||||
filterTitles = append(filterTitles, processTitle(title, list.MatchRelease)...)
|
||||
}
|
||||
|
||||
if len(filterTitles) == 0 {
|
||||
l.Debug().Msgf("no titles found to update for list: %v", list.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
joinedTitles := strings.Join(filterTitles, ",")
|
||||
|
||||
l.Trace().Str("titles", joinedTitles).Msgf("found %d titles", len(joinedTitles))
|
||||
|
||||
filterUpdate := domain.FilterUpdate{Shows: &joinedTitles}
|
||||
|
||||
if list.MatchRelease {
|
||||
filterUpdate.Shows = &nullString
|
||||
filterUpdate.MatchReleases = &joinedTitles
|
||||
}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
filterUpdate.ID = filter.ID
|
||||
|
||||
if err := s.filterSvc.UpdatePartial(ctx, filterUpdate); err != nil {
|
||||
return errors.Wrapf(err, "error updating filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
l.Debug().Msgf("successfully updated filter: %v", filter.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
68
internal/list/scheduled_jobs.go
Normal file
68
internal/list/scheduled_jobs.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Job interface {
|
||||
cron.Job
|
||||
RunE(ctx context.Context) error
|
||||
}
|
||||
|
||||
type RefreshListSvc interface {
|
||||
RefreshAll(ctx context.Context) error
|
||||
}
|
||||
|
||||
type RefreshListsJob struct {
|
||||
log zerolog.Logger
|
||||
listSvc RefreshListSvc
|
||||
}
|
||||
|
||||
func NewRefreshListsJob(log zerolog.Logger, listSvc RefreshListSvc) Job {
|
||||
return &RefreshListsJob{log: log, listSvc: listSvc}
|
||||
}
|
||||
|
||||
func (job *RefreshListsJob) Run() {
|
||||
ctx := context.Background()
|
||||
if err := job.RunE(ctx); err != nil {
|
||||
job.log.Error().Err(err).Msg("error refreshing lists")
|
||||
}
|
||||
}
|
||||
|
||||
func (job *RefreshListsJob) RunE(ctx context.Context) error {
|
||||
if err := job.run(ctx); err != nil {
|
||||
job.log.Error().Err(err).Msg("error refreshing lists")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (job *RefreshListsJob) run(ctx context.Context) error {
|
||||
job.log.Debug().Msg("running refresh lists job")
|
||||
|
||||
// Seed the random number generator
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// Generate a random duration between 1 and 35 seconds
|
||||
delay := time.Duration(rand.Intn(35-1+1)+1) * time.Second // (35-1+1)+1 => range: 1 to 35
|
||||
|
||||
job.log.Debug().Msgf("delaying for %v...", delay)
|
||||
|
||||
// Sleep for the calculated duration
|
||||
time.Sleep(delay)
|
||||
|
||||
if err := job.listSvc.RefreshAll(ctx); err != nil {
|
||||
job.log.Error().Err(err).Msg("error refreshing lists")
|
||||
return err
|
||||
}
|
||||
|
||||
job.log.Debug().Msg("finished refresh lists job")
|
||||
|
||||
return nil
|
||||
}
|
337
internal/list/service.go
Normal file
337
internal/list/service.go
Normal file
|
@ -0,0 +1,337 @@
|
|||
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")
|
||||
}
|
||||
}
|
28
internal/list/tags.go
Normal file
28
internal/list/tags.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package list
|
||||
|
||||
import "github.com/autobrr/autobrr/pkg/arr"
|
||||
|
||||
func containsTag(tags []*arr.Tag, titleTags []int, checkTags []string) bool {
|
||||
tagLabels := []string{}
|
||||
|
||||
// match tag id's with labels
|
||||
for _, movieTag := range titleTags {
|
||||
for _, tag := range tags {
|
||||
tag := tag
|
||||
if movieTag == tag.ID {
|
||||
tagLabels = append(tagLabels, tag.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check included tags and set ret to true if we have a match
|
||||
for _, includeTag := range checkTags {
|
||||
for _, label := range tagLabels {
|
||||
if includeTag == label {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
103
internal/list/title.go
Normal file
103
internal/list/title.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Regex patterns
|
||||
// https://www.regular-expressions.info/unicode.html#category
|
||||
// https://www.ncbi.nlm.nih.gov/staff/beck/charents/hex.html
|
||||
var (
|
||||
replaceRegexp = regexp.MustCompile(`[\p{P}\p{Z}\x{00C0}-\x{017E}\x{00AE}]`)
|
||||
questionmarkRegexp = regexp.MustCompile(`[?]{2,}`)
|
||||
regionCodeRegexp = regexp.MustCompile(`\(.+\)$`)
|
||||
parenthesesEndRegexp = regexp.MustCompile(`\)$`)
|
||||
)
|
||||
|
||||
func processTitle(title string, matchRelease bool) []string {
|
||||
// Checking if the title is empty.
|
||||
if strings.TrimSpace(title) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleans year like (2020) from arr title
|
||||
//var re = regexp.MustCompile(`(?m)\s(\(\d+\))`)
|
||||
//title = re.ReplaceAllString(title, "")
|
||||
|
||||
t := NewTitleSlice()
|
||||
|
||||
if replaceRegexp.ReplaceAllString(title, "") == "" {
|
||||
t.Add(title, matchRelease)
|
||||
} else {
|
||||
// title with all non-alphanumeric characters replaced by "?"
|
||||
apostropheTitle := parenthesesEndRegexp.ReplaceAllString(title, "?")
|
||||
apostropheTitle = replaceRegexp.ReplaceAllString(apostropheTitle, "?")
|
||||
apostropheTitle = questionmarkRegexp.ReplaceAllString(apostropheTitle, "*")
|
||||
|
||||
t.Add(apostropheTitle, matchRelease)
|
||||
t.Add(strings.TrimRight(apostropheTitle, "?* "), matchRelease)
|
||||
|
||||
// title with apostrophes removed and all non-alphanumeric characters replaced by "?"
|
||||
noApostropheTitle := parenthesesEndRegexp.ReplaceAllString(title, "?")
|
||||
noApostropheTitle = strings.ReplaceAll(noApostropheTitle, "'", "")
|
||||
noApostropheTitle = replaceRegexp.ReplaceAllString(noApostropheTitle, "?")
|
||||
noApostropheTitle = questionmarkRegexp.ReplaceAllString(noApostropheTitle, "*")
|
||||
|
||||
t.Add(noApostropheTitle, matchRelease)
|
||||
t.Add(strings.TrimRight(noApostropheTitle, "?* "), matchRelease)
|
||||
|
||||
// title with regions in parentheses removed and all non-alphanumeric characters replaced by "?"
|
||||
removedRegionCodeApostrophe := regionCodeRegexp.ReplaceAllString(title, "")
|
||||
removedRegionCodeApostrophe = strings.TrimRight(removedRegionCodeApostrophe, " ")
|
||||
removedRegionCodeApostrophe = replaceRegexp.ReplaceAllString(removedRegionCodeApostrophe, "?")
|
||||
removedRegionCodeApostrophe = questionmarkRegexp.ReplaceAllString(removedRegionCodeApostrophe, "*")
|
||||
|
||||
t.Add(removedRegionCodeApostrophe, matchRelease)
|
||||
t.Add(strings.TrimRight(removedRegionCodeApostrophe, "?* "), matchRelease)
|
||||
|
||||
// title with regions in parentheses and apostrophes removed and all non-alphanumeric characters replaced by "?"
|
||||
removedRegionCodeNoApostrophe := regionCodeRegexp.ReplaceAllString(title, "")
|
||||
removedRegionCodeNoApostrophe = strings.TrimRight(removedRegionCodeNoApostrophe, " ")
|
||||
removedRegionCodeNoApostrophe = strings.ReplaceAll(removedRegionCodeNoApostrophe, "'", "")
|
||||
removedRegionCodeNoApostrophe = replaceRegexp.ReplaceAllString(removedRegionCodeNoApostrophe, "?")
|
||||
removedRegionCodeNoApostrophe = questionmarkRegexp.ReplaceAllString(removedRegionCodeNoApostrophe, "*")
|
||||
|
||||
t.Add(removedRegionCodeNoApostrophe, matchRelease)
|
||||
t.Add(strings.TrimRight(removedRegionCodeNoApostrophe, "?* "), matchRelease)
|
||||
}
|
||||
|
||||
return t.Titles()
|
||||
}
|
||||
|
||||
type Titles struct {
|
||||
tm map[string]struct{}
|
||||
}
|
||||
|
||||
func NewTitleSlice() *Titles {
|
||||
ts := Titles{
|
||||
tm: map[string]struct{}{},
|
||||
}
|
||||
return &ts
|
||||
}
|
||||
|
||||
func (ts *Titles) Add(title string, matchRelease bool) {
|
||||
if matchRelease {
|
||||
title = strings.Trim(title, "?")
|
||||
title = fmt.Sprintf("*%v*", title)
|
||||
}
|
||||
|
||||
_, ok := ts.tm[title]
|
||||
if !ok {
|
||||
ts.tm[title] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *Titles) Titles() []string {
|
||||
titles := []string{}
|
||||
for key := range ts.tm {
|
||||
titles = append(titles, key)
|
||||
}
|
||||
return titles
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue