diff --git a/cmd/autobrr/main.go b/cmd/autobrr/main.go index d297619..bf7219f 100644 --- a/cmd/autobrr/main.go +++ b/cmd/autobrr/main.go @@ -101,7 +101,7 @@ func main() { downloadClientService = download_client.NewService(log, downloadClientRepo) actionService = action.NewService(log, actionRepo, downloadClientService, bus) indexerService = indexer.NewService(log, cfg.Config, indexerRepo, indexerAPIService, schedulingService) - filterService = filter.NewService(log, filterRepo, actionRepo, indexerAPIService, indexerService) + filterService = filter.NewService(log, filterRepo, actionRepo, releaseRepo, indexerAPIService, indexerService) releaseService = release.NewService(log, releaseRepo, actionService, filterService) ircService = irc.NewService(log, ircRepo, releaseService, indexerService, notificationService) feedService = feed.NewService(log, feedRepo, feedCacheRepo, releaseService, schedulingService) diff --git a/internal/database/filter.go b/internal/database/filter.go index 64b676a..d4346b4 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -190,6 +190,7 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter "scene", "freeleech", "freeleech_percent", + "smart_episode", "shows", "seasons", "episodes", @@ -249,7 +250,7 @@ func (r *FilterRepo) FindByID(ctx context.Context, filterID int) (*domain.Filter var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool var delay, maxDownloads, logScore, extWebhookStatus, extScriptStatus sql.NullInt32 - if err := row.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil { + if err := row.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &f.SmartEpisode, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil { return nil, errors.Wrap(err, "error scanning row") } @@ -346,6 +347,7 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe "f.scene", "f.freeleech", "f.freeleech_percent", + "f.smart_episode", "f.shows", "f.seasons", "f.episodes", @@ -415,7 +417,7 @@ func (r *FilterRepo) findByIndexerIdentifier(ctx context.Context, tx *Tx, indexe var useRegex, scene, freeleech, hasLog, hasCue, perfectFlac, extScriptEnabled, extWebhookEnabled sql.NullBool var delay, maxDownloads, logScore, extWebhookStatus, extScriptStatus sql.NullInt32 - if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil { + if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &f.Priority, &maxDownloads, &maxDownloadsUnit, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &matchReleaseTags, &exceptReleaseTags, &f.UseRegexReleaseTags, &scene, &freeleech, &freeleechPercent, &f.SmartEpisode, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), pq.Array(&f.MatchHDR), pq.Array(&f.ExceptHDR), pq.Array(&f.MatchOther), pq.Array(&f.ExceptOther), &years, &artists, &albums, pq.Array(&f.MatchReleaseTypes), pq.Array(&f.Formats), pq.Array(&f.Quality), pq.Array(&f.Media), &logScore, &hasLog, &hasCue, &perfectFlac, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, pq.Array(&f.Origins), pq.Array(&f.ExceptOrigins), &extScriptEnabled, &extScriptCmd, &extScriptArgs, &extScriptStatus, &extWebhookEnabled, &extWebhookHost, &extWebhookData, &extWebhookStatus, &f.CreatedAt, &f.UpdatedAt); err != nil { return nil, errors.Wrap(err, "error scanning row") } @@ -490,6 +492,7 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F "scene", "freeleech", "freeleech_percent", + "smart_episode", "shows", "seasons", "episodes", @@ -549,6 +552,7 @@ func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.F filter.Scene, filter.Freeleech, filter.FreeleechPercent, + filter.SmartEpisode, filter.Shows, filter.Seasons, filter.Episodes, @@ -627,6 +631,7 @@ func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain. Set("scene", filter.Scene). Set("freeleech", filter.Freeleech). Set("freeleech_percent", filter.FreeleechPercent). + Set("smart_episode", filter.SmartEpisode). Set("shows", filter.Shows). Set("seasons", filter.Seasons). Set("episodes", filter.Episodes). @@ -743,6 +748,9 @@ func (r *FilterRepo) UpdatePartial(ctx context.Context, filter domain.FilterUpda if filter.FreeleechPercent != nil { q = q.Set("freeleech_percent", filter.FreeleechPercent) } + if filter.SmartEpisode != nil { + q = q.Set("smart_episode", filter.SmartEpisode) + } if filter.Shows != nil { q = q.Set("shows", filter.Shows) } diff --git a/internal/database/postgres_migrate.go b/internal/database/postgres_migrate.go index 1117cb7..407082f 100644 --- a/internal/database/postgres_migrate.go +++ b/internal/database/postgres_migrate.go @@ -83,6 +83,7 @@ CREATE TABLE filter scene BOOLEAN, freeleech BOOLEAN, freeleech_percent TEXT, + smart_episode BOOLEAN DEFAULT FALSE, shows TEXT, seasons TEXT, episodes TEXT, @@ -619,4 +620,7 @@ CREATE INDEX indexer_identifier_index `ALTER TABLE indexer ADD COLUMN base_url TEXT; `, + `ALTER TABLE "filter" + ADD COLUMN smart_episode BOOLEAN DEFAULT false; + `, } diff --git a/internal/database/release.go b/internal/database/release.go index 26fa578..369903c 100644 --- a/internal/database/release.go +++ b/internal/database/release.go @@ -447,3 +447,70 @@ func (repo *ReleaseRepo) Delete(ctx context.Context) error { return nil } + +func (repo *ReleaseRepo) CanDownloadShow(ctx context.Context, title string, season int, episode int) (bool, error) { + // TODO support non season episode shows + // if rls.Day > 0 { + // // Maybe in the future + // // SELECT '' FROM release WHERE Title LIKE %q AND ((Year == %d AND Month == %d AND Day > %d) OR (Year == %d AND Month > %d) OR (Year > %d))" + // qs := sql.Query("SELECT torrent_name FROM release WHERE Title LIKE %q AND Year >= %d", rls.Title, rls.Year) + // + // for q := range qs.Rows() { + // r := rls.ParseTitle(q) + // if r.Year > rls.Year { + // return false, fmt.Errorf("stale release year") + // } + // + // if r.Month > rls.Month { + // return false, fmt.Errorf("stale release month") + // } + // + // if r.Month == rls.Month && r.Day > rls.Day { + // return false, fmt.Errorf("stale release day") + // } + // } + //} + + queryBuilder := repo.db.squirrel. + Select("COUNT(*)"). + From("release"). + Where("title LIKE ?", fmt.Sprint("%", title, "%")) + + if season > 0 && episode > 0 { + queryBuilder = queryBuilder.Where(sq.Or{ + sq.And{ + sq.Eq{"season": season}, + sq.Gt{"episode": episode}, + }, + sq.Gt{"season": season}, + }) + } else if season > 0 && episode == 0 { + queryBuilder = queryBuilder.Where(sq.Gt{"season": season}) + } else { + /* No support for this scenario today. Specifically multi-part specials. + * The Database presently does not have Subtitle as a field, but is coming at a future date. */ + return true, nil + } + + query, args, err := queryBuilder.ToSql() + if err != nil { + return false, errors.Wrap(err, "error building query") + } + + row := repo.db.handler.QueryRowContext(ctx, query, args...) + if err := row.Err(); err != nil { + return false, err + } + + var count int + + if err := row.Scan(&count); err != nil { + return false, err + } + + if count > 0 { + return false, nil + } + + return true, nil +} diff --git a/internal/database/sqlite_migrate.go b/internal/database/sqlite_migrate.go index 7a7b8db..7850c08 100644 --- a/internal/database/sqlite_migrate.go +++ b/internal/database/sqlite_migrate.go @@ -83,6 +83,7 @@ CREATE TABLE filter scene BOOLEAN, freeleech BOOLEAN, freeleech_percent TEXT, + smart_episode BOOLEAN DEFAULT FALSE, shows TEXT, seasons TEXT, episodes TEXT, @@ -963,4 +964,7 @@ ALTER TABLE irc_network_dg_tmp `ALTER TABLE indexer ADD COLUMN base_url TEXT; `, + `ALTER TABLE "filter" + ADD COLUMN smart_episode BOOLEAN DEFAULT false; + `, } diff --git a/internal/domain/filter.go b/internal/domain/filter.go index 2a52fa3..e86bead 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -81,6 +81,7 @@ type Filter struct { Bonus []string `json:"bonus,omitempty"` Freeleech bool `json:"freeleech,omitempty"` FreeleechPercent string `json:"freeleech_percent,omitempty"` + SmartEpisode bool `json:"smart_episode"` Shows string `json:"shows,omitempty"` Seasons string `json:"seasons,omitempty"` Episodes string `json:"episodes,omitempty"` @@ -153,6 +154,7 @@ type FilterUpdate struct { Bonus *[]string `json:"bonus,omitempty"` Freeleech *bool `json:"freeleech,omitempty"` FreeleechPercent *string `json:"freeleech_percent,omitempty"` + SmartEpisode *bool `json:"smart_episode,omitempty"` Shows *string `json:"shows,omitempty"` Seasons *string `json:"seasons,omitempty"` Episodes *string `json:"episodes,omitempty"` diff --git a/internal/domain/release.go b/internal/domain/release.go index 0d3d49d..ae71445 100644 --- a/internal/domain/release.go +++ b/internal/domain/release.go @@ -32,6 +32,7 @@ type ReleaseRepo interface { Stats(ctx context.Context) (*ReleaseStats, error) StoreReleaseActionStatus(ctx context.Context, actionStatus *ReleaseActionStatus) error Delete(ctx context.Context) error + CanDownloadShow(ctx context.Context, title string, season int, episode int) (bool, error) } type Release struct { diff --git a/internal/filter/service.go b/internal/filter/service.go index 500bad4..071f84c 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -25,7 +25,7 @@ type Service interface { FindByID(ctx context.Context, filterID int) (*domain.Filter, error) FindByIndexerIdentifier(indexer string) ([]domain.Filter, error) Find(ctx context.Context, params domain.FilterQueryParams) ([]domain.Filter, error) - CheckFilter(f domain.Filter, release *domain.Release) (bool, error) + CheckFilter(ctx context.Context, f domain.Filter, release *domain.Release) (bool, error) ListFilters(ctx context.Context) ([]domain.Filter, error) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) @@ -33,23 +33,27 @@ 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) + CanDownloadShow(ctx context.Context, release *domain.Release) (bool, error) } type service struct { - log zerolog.Logger - repo domain.FilterRepo - actionRepo domain.ActionRepo - indexerSvc indexer.Service - apiService indexer.APIService + log zerolog.Logger + repo domain.FilterRepo + actionRepo domain.ActionRepo + releaseRepo domain.ReleaseRepo + indexerSvc indexer.Service + apiService indexer.APIService } -func NewService(log logger.Logger, repo domain.FilterRepo, actionRepo domain.ActionRepo, apiService indexer.APIService, indexerSvc indexer.Service) Service { +func NewService(log logger.Logger, repo domain.FilterRepo, actionRepo domain.ActionRepo, releaseRepo domain.ReleaseRepo, apiService indexer.APIService, indexerSvc indexer.Service) Service { return &service{ - log: log.With().Str("module", "filter").Logger(), - repo: repo, - actionRepo: actionRepo, - apiService: apiService, - indexerSvc: indexerSvc, + log: log.With().Str("module", "filter").Logger(), + repo: repo, + actionRepo: actionRepo, + releaseRepo: releaseRepo, + apiService: apiService, + indexerSvc: indexerSvc, } } @@ -293,7 +297,7 @@ func (s *service) Delete(ctx context.Context, filterID int) error { return nil } -func (s *service) CheckFilter(f domain.Filter, release *domain.Release) (bool, error) { +func (s *service) CheckFilter(ctx context.Context, f domain.Filter, release *domain.Release) (bool, error) { s.log.Trace().Msgf("filter.Service.CheckFilter: checking filter: %v %+v", f.Name, f) s.log.Trace().Msgf("filter.Service.CheckFilter: checking filter: %v for release: %+v", f.Name, release) @@ -305,6 +309,21 @@ func (s *service) CheckFilter(f domain.Filter, release *domain.Release) (bool, e } if matchedFilter { + // smartEpisode check + if f.SmartEpisode { + canDownloadShow, err := s.CanDownloadShow(ctx, release) + if err != nil { + s.log.Trace().Msgf("filter.Service.CheckFilter: failed smart episode check: %s", f.Name) + return false, nil + } + + if !canDownloadShow { + s.log.Trace().Msgf("filter.Service.CheckFilter: failed smart episode check: %s", f.Name) + release.AddRejectionF("smart episode check: not new: (%s) season: %d ep: %d", release.Title, release.Season, release.Episode) + return false, nil + } + } + // if matched, do additional size check if needed, attach actions and return the filter s.log.Debug().Msgf("filter.Service.CheckFilter: found and matched filter: %+v", f.Name) @@ -462,6 +481,10 @@ func checkSizeFilter(minSize string, maxSize string, releaseSize uint64) (bool, return true, nil } +func (s *service) CanDownloadShow(ctx context.Context, release *domain.Release) (bool, error) { + return s.releaseRepo.CanDownloadShow(ctx, release.Title, release.Season, release.Episode) +} + func (s *service) execCmd(release *domain.Release, cmd string, args string) (int, error) { s.log.Debug().Msgf("filter exec release: %v", release.TorrentName) diff --git a/internal/release/service.go b/internal/release/service.go index 3c60a31..a612021 100644 --- a/internal/release/service.go +++ b/internal/release/service.go @@ -117,7 +117,7 @@ func (s *service) Process(release *domain.Release) { release.FilterID = f.ID // test filter - match, err := s.filterSvc.CheckFilter(f, release) + match, err := s.filterSvc.CheckFilter(ctx, f, release) if err != nil { l.Error().Err(err).Msg("release.Process: error checking filter") return diff --git a/web/src/screens/filters/details.tsx b/web/src/screens/filters/details.tsx index 0690b4f..3ec0805 100644 --- a/web/src/screens/filters/details.tsx +++ b/web/src/screens/filters/details.tsx @@ -251,6 +251,7 @@ export default function FilterDetails() { except_other: filter.except_other || [], seasons: filter.seasons, episodes: filter.episodes, + smart_episode: filter.smart_episode, match_releases: filter.match_releases, except_releases: filter.except_releases, match_release_groups: filter.match_release_groups, @@ -375,6 +376,10 @@ export function MoviesTv() { + +
+ {/*Do not match older or already existing episodes.*/} +
diff --git a/web/src/types/Filter.d.ts b/web/src/types/Filter.d.ts index 9733143..fbb3c92 100644 --- a/web/src/types/Filter.d.ts +++ b/web/src/types/Filter.d.ts @@ -26,6 +26,7 @@ interface Filter { shows: string; seasons: string; episodes: string; + smart_episode: boolean; resolutions: string[]; codecs: string[]; sources: string[];