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
|
@ -8,8 +8,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr/lidarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
"github.com/autobrr/autobrr/pkg/lidarr"
|
||||
)
|
||||
|
||||
func (s *service) lidarr(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
|
||||
|
@ -27,7 +27,7 @@ func (s *service) lidarr(ctx context.Context, action *domain.Action, release dom
|
|||
return nil, errors.New("client %s %s not enabled", client.Type, client.Name)
|
||||
}
|
||||
|
||||
arr := client.Client.(lidarr.Client)
|
||||
arr := client.Client.(*lidarr.Client)
|
||||
|
||||
r := lidarr.Release{
|
||||
Title: release.TorrentName,
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr/radarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
"github.com/autobrr/autobrr/pkg/radarr"
|
||||
)
|
||||
|
||||
func (s *service) radarr(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
|
||||
|
@ -27,7 +27,7 @@ func (s *service) radarr(ctx context.Context, action *domain.Action, release dom
|
|||
return nil, errors.New("client %s %s not enabled", client.Type, client.Name)
|
||||
}
|
||||
|
||||
arr := client.Client.(radarr.Client)
|
||||
arr := client.Client.(*radarr.Client)
|
||||
|
||||
r := radarr.Release{
|
||||
Title: release.TorrentName,
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr/readarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
"github.com/autobrr/autobrr/pkg/readarr"
|
||||
)
|
||||
|
||||
func (s *service) readarr(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
|
||||
|
@ -27,7 +27,7 @@ func (s *service) readarr(ctx context.Context, action *domain.Action, release do
|
|||
return nil, errors.New("client %s %s not enabled", client.Type, client.Name)
|
||||
}
|
||||
|
||||
arr := client.Client.(readarr.Client)
|
||||
arr := client.Client.(*readarr.Client)
|
||||
|
||||
r := readarr.Release{
|
||||
Title: release.TorrentName,
|
||||
|
|
|
@ -18,17 +18,11 @@ import (
|
|||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *service) RunAction(ctx context.Context, action *domain.Action, release *domain.Release) ([]string, error) {
|
||||
var (
|
||||
err error
|
||||
rejections []string
|
||||
)
|
||||
|
||||
func (s *service) RunAction(ctx context.Context, action *domain.Action, release *domain.Release) (rejections []string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
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
|
||||
errors.RecoverPanic(recover(), &err)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msgf("recovering from panic in run action %s", action.Name)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ type Service interface {
|
|||
DeleteByFilterID(ctx context.Context, filterID int) error
|
||||
ToggleEnabled(actionID int) error
|
||||
|
||||
RunAction(ctx context.Context, action *domain.Action, release *domain.Release) ([]string, error)
|
||||
RunAction(ctx context.Context, action *domain.Action, release *domain.Release) (rejections []string, err error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr/sonarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
"github.com/autobrr/autobrr/pkg/sonarr"
|
||||
)
|
||||
|
||||
func (s *service) sonarr(ctx context.Context, action *domain.Action, release domain.Release) ([]string, error) {
|
||||
|
@ -27,7 +27,7 @@ func (s *service) sonarr(ctx context.Context, action *domain.Action, release dom
|
|||
return nil, errors.New("client %s %s not enabled", client.Type, client.Name)
|
||||
}
|
||||
|
||||
arr := client.Client.(sonarr.Client)
|
||||
arr := client.Client.(*sonarr.Client)
|
||||
|
||||
r := sonarr.Release{
|
||||
Title: release.TorrentName,
|
||||
|
|
|
@ -52,6 +52,10 @@ func (r *FilterRepo) find(ctx context.Context, params domain.FilterQueryParams)
|
|||
Where("a.filter_id = f.id").
|
||||
Where("a.enabled = '1'")
|
||||
|
||||
isAutoUpdated := r.db.squirrel.Select("CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END").
|
||||
From("list_filter lf").
|
||||
Where("lf.filter_id = f.id")
|
||||
|
||||
queryBuilder := r.db.squirrel.
|
||||
Select(
|
||||
"f.id",
|
||||
|
@ -64,6 +68,7 @@ func (r *FilterRepo) find(ctx context.Context, params domain.FilterQueryParams)
|
|||
Distinct().
|
||||
Column(sq.Alias(actionCountQuery, "action_count")).
|
||||
Column(sq.Alias(actionEnabledCountQuery, "actions_enabled_count")).
|
||||
Column(sq.Alias(isAutoUpdated, "is_auto_updated")).
|
||||
LeftJoin("filter_indexer fi ON f.id = fi.filter_id").
|
||||
LeftJoin("indexer i ON i.id = fi.indexer_id").
|
||||
From("filter f")
|
||||
|
@ -103,7 +108,7 @@ func (r *FilterRepo) find(ctx context.Context, params domain.FilterQueryParams)
|
|||
for rows.Next() {
|
||||
var f domain.Filter
|
||||
|
||||
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Priority, &f.CreatedAt, &f.UpdatedAt, &f.ActionsCount, &f.ActionsEnabledCount); err != nil {
|
||||
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Priority, &f.CreatedAt, &f.UpdatedAt, &f.ActionsCount, &f.ActionsEnabledCount, &f.IsAutoUpdated); err != nil {
|
||||
return nil, errors.Wrap(err, "error scanning row")
|
||||
}
|
||||
|
||||
|
@ -1288,6 +1293,13 @@ func (r *FilterRepo) DeleteFilterExternal(ctx context.Context, filterID int) err
|
|||
}
|
||||
|
||||
func (r *FilterRepo) Delete(ctx context.Context, filterID int) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error begin transaction")
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
queryBuilder := r.db.squirrel.
|
||||
Delete("filter").
|
||||
Where(sq.Eq{"id": filterID})
|
||||
|
@ -1297,7 +1309,7 @@ func (r *FilterRepo) Delete(ctx context.Context, filterID int) error {
|
|||
return errors.Wrap(err, "error building query")
|
||||
}
|
||||
|
||||
result, err := r.db.handler.ExecContext(ctx, query, args...)
|
||||
result, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error executing query")
|
||||
}
|
||||
|
@ -1308,6 +1320,22 @@ func (r *FilterRepo) Delete(ctx context.Context, filterID int) error {
|
|||
return domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
listFilterQueryBuilder := r.db.squirrel.Delete("list_filter").Where(sq.Eq{"filter_id": filterID})
|
||||
|
||||
deleteListFilterQuery, deleteListFilterArgs, err := listFilterQueryBuilder.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, deleteListFilterQuery, deleteListFilterArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return errors.Wrap(err, "error storing list and filters")
|
||||
}
|
||||
|
||||
r.log.Debug().Msgf("filter.delete: successfully deleted: %v", filterID)
|
||||
|
||||
return nil
|
||||
|
|
456
internal/database/list.go
Normal file
456
internal/database/list.go
Normal file
|
@ -0,0 +1,456 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/internal/logger"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/lib/pq"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type ListRepo struct {
|
||||
log zerolog.Logger
|
||||
db *DB
|
||||
}
|
||||
|
||||
func NewListRepo(log logger.Logger, db *DB) domain.ListRepo {
|
||||
return &ListRepo{
|
||||
log: log.With().Str("repo", "list").Logger(),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ListRepo) List(ctx context.Context) ([]*domain.List, error) {
|
||||
qb := r.db.squirrel.Select(
|
||||
"id",
|
||||
"name",
|
||||
"enabled",
|
||||
"type",
|
||||
"client_id",
|
||||
"url",
|
||||
"headers",
|
||||
"api_key",
|
||||
"match_release",
|
||||
"tags_included",
|
||||
"tags_excluded",
|
||||
"include_unmonitored",
|
||||
"include_alternate_titles",
|
||||
"last_refresh_time",
|
||||
"last_refresh_status",
|
||||
"last_refresh_data",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
).
|
||||
From("list").
|
||||
OrderBy("name ASC")
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := r.db.handler.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
lists := make([]*domain.List, 0)
|
||||
for rows.Next() {
|
||||
var list domain.List
|
||||
|
||||
var url, apiKey, lastRefreshStatus, lastRefreshData sql.Null[string]
|
||||
var lastRefreshTime sql.Null[time.Time]
|
||||
|
||||
err = rows.Scan(&list.ID, &list.Name, &list.Enabled, &list.Type, &list.ClientID, &url, pq.Array(&list.Headers), &list.APIKey, &list.MatchRelease, pq.Array(&list.TagsInclude), pq.Array(&list.TagsExclude), &list.IncludeUnmonitored, &list.IncludeAlternateTitles, &lastRefreshTime, &lastRefreshStatus, &lastRefreshData, &list.CreatedAt, &list.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list.URL = url.V
|
||||
list.APIKey = apiKey.V
|
||||
list.LastRefreshTime = lastRefreshTime.V
|
||||
list.LastRefreshData = lastRefreshData.V
|
||||
list.LastRefreshStatus = domain.ListRefreshStatus(lastRefreshStatus.V)
|
||||
list.Filters = make([]domain.ListFilter, 0)
|
||||
|
||||
lists = append(lists, &list)
|
||||
}
|
||||
|
||||
return lists, nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) FindByID(ctx context.Context, listID int64) (*domain.List, error) {
|
||||
qb := r.db.squirrel.Select(
|
||||
"id",
|
||||
"name",
|
||||
"enabled",
|
||||
"type",
|
||||
"client_id",
|
||||
"url",
|
||||
"headers",
|
||||
"api_key",
|
||||
"match_release",
|
||||
"tags_included",
|
||||
"tags_excluded",
|
||||
"include_unmonitored",
|
||||
"include_alternate_titles",
|
||||
"last_refresh_time",
|
||||
"last_refresh_status",
|
||||
"last_refresh_data",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
).
|
||||
From("list").
|
||||
Where(sq.Eq{"id": listID})
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row := r.db.handler.QueryRowContext(ctx, query, args...)
|
||||
if err := row.Err(); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var list domain.List
|
||||
|
||||
var url, apiKey sql.Null[string]
|
||||
|
||||
err = row.Scan(&list.ID, &list.Name, &list.Enabled, &list.Type, &list.ClientID, &url, pq.Array(&list.Headers), &list.APIKey, &list.MatchRelease, pq.Array(&list.TagsInclude), pq.Array(&list.TagsExclude), &list.IncludeUnmonitored, &list.IncludeAlternateTitles, &list.LastRefreshTime, &list.LastRefreshStatus, &list.LastRefreshData, &list.CreatedAt, &list.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list.URL = url.V
|
||||
list.APIKey = apiKey.V
|
||||
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) Store(ctx context.Context, list *domain.List) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error begin transaction")
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
qb := r.db.squirrel.Insert("list").
|
||||
Columns(
|
||||
"name",
|
||||
"enabled",
|
||||
"type",
|
||||
"client_id",
|
||||
"url",
|
||||
"headers",
|
||||
"api_key",
|
||||
"match_release",
|
||||
"tags_included",
|
||||
"tags_excluded",
|
||||
"include_unmonitored",
|
||||
"include_alternate_titles",
|
||||
).
|
||||
Values(
|
||||
list.Name,
|
||||
list.Enabled,
|
||||
list.Type,
|
||||
list.ClientID,
|
||||
list.URL,
|
||||
pq.Array(list.Headers),
|
||||
list.APIKey,
|
||||
list.MatchRelease,
|
||||
pq.Array(list.TagsInclude),
|
||||
pq.Array(list.TagsExclude),
|
||||
list.IncludeUnmonitored,
|
||||
list.IncludeAlternateTitles,
|
||||
).Suffix("RETURNING id").RunWith(tx)
|
||||
|
||||
//query, args, err := qb.ToSql()
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
if err := qb.QueryRowContext(ctx).Scan(&list.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.StoreListFilterConnection(ctx, tx, list); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return errors.Wrap(err, "error storing list and filters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) Update(ctx context.Context, list *domain.List) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error begin transaction")
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
qb := r.db.squirrel.Update("list").
|
||||
Set("name", list.Name).
|
||||
Set("enabled", list.Enabled).
|
||||
Set("type", list.Type).
|
||||
Set("client_id", list.ClientID).
|
||||
Set("url", list.URL).
|
||||
Set("headers", pq.Array(list.Headers)).
|
||||
Set("api_key", list.APIKey).
|
||||
Set("match_release", list.MatchRelease).
|
||||
Set("tags_included", pq.Array(list.TagsInclude)).
|
||||
Set("tags_excluded", pq.Array(list.TagsExclude)).
|
||||
Set("include_unmonitored", list.IncludeUnmonitored).
|
||||
Set("include_alternate_titles", list.IncludeAlternateTitles).
|
||||
Set("updated_at", sq.Expr("CURRENT_TIMESTAMP")).
|
||||
Where(sq.Eq{"id": list.ID})
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.StoreListFilterConnection(ctx, tx, list); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return errors.Wrap(err, "error updating filter actions")
|
||||
}
|
||||
|
||||
rowsAffected, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return domain.ErrUpdateFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) UpdateLastRefresh(ctx context.Context, list *domain.List) error {
|
||||
qb := r.db.squirrel.Update("list").
|
||||
Set("last_refresh_time", list.LastRefreshTime).
|
||||
Set("last_refresh_status", list.LastRefreshStatus).
|
||||
Set("last_refresh_data", list.LastRefreshData).
|
||||
Set("updated_at", sq.Expr("CURRENT_TIMESTAMP")).
|
||||
Where(sq.Eq{"id": list.ID})
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := r.db.handler.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return domain.ErrUpdateFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) Delete(ctx context.Context, listID int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error begin transaction")
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
qb := r.db.squirrel.Delete("list").From("list").Where(sq.Eq{"id": listID})
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return domain.ErrDeleteFailed
|
||||
}
|
||||
|
||||
listFilterQueryBuilder := r.db.squirrel.Delete("list_filter").Where(sq.Eq{"list_id": listID})
|
||||
|
||||
deleteListFilterQuery, deleteListFilterArgs, err := listFilterQueryBuilder.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, deleteListFilterQuery, deleteListFilterArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return errors.Wrap(err, "error storing list and filters")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) ToggleEnabled(ctx context.Context, listID int64, enabled bool) error {
|
||||
qb := r.db.squirrel.Update("list").
|
||||
Set("enabled", enabled).
|
||||
Set("updated_at", sq.Expr("CURRENT_TIMESTAMP")).
|
||||
Where(sq.Eq{"id": listID})
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := r.db.handler.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return domain.ErrUpdateFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) StoreListFilterConnection(ctx context.Context, tx *Tx, list *domain.List) error {
|
||||
qb := r.db.squirrel.Delete("list_filter").Where(sq.Eq{"list_id": list.ID})
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.log.Trace().Int64("rows_affected", rowsAffected).Msg("deleted list filters")
|
||||
|
||||
//if rowsAffected == 0 {
|
||||
// return domain.ErrUpdateFailed
|
||||
//}
|
||||
|
||||
for _, filter := range list.Filters {
|
||||
qb := r.db.squirrel.Insert("list_filter").
|
||||
Columns(
|
||||
"list_id",
|
||||
"filter_id",
|
||||
).
|
||||
Values(
|
||||
list.ID,
|
||||
filter.ID,
|
||||
)
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return domain.ErrUpdateFailed
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ListRepo) GetListFilters(ctx context.Context, listID int64) ([]domain.ListFilter, error) {
|
||||
qb := r.db.squirrel.Select(
|
||||
"f.id",
|
||||
"f.name",
|
||||
).
|
||||
From("list_filter lf").
|
||||
Join(
|
||||
"filter f ON f.id = lf.filter_id",
|
||||
).
|
||||
OrderBy(
|
||||
"f.name ASC",
|
||||
).
|
||||
Where(sq.Eq{"lf.list_id": listID})
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := r.db.handler.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
filters := make([]domain.ListFilter, 0)
|
||||
for rows.Next() {
|
||||
var filter domain.ListFilter
|
||||
err = rows.Scan(&filter.ID, &filter.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
|
||||
return filters, nil
|
||||
}
|
|
@ -415,6 +415,38 @@ CREATE TABLE api_key
|
|||
scopes TEXT [] DEFAULT '{}' NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE list
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
enabled BOOLEAN,
|
||||
type TEXT NOT NULL,
|
||||
client_id INTEGER,
|
||||
url TEXT,
|
||||
headers TEXT [] DEFAULT '{}' NOT NULL,
|
||||
api_key TEXT,
|
||||
match_release BOOLEAN,
|
||||
tags_included TEXT [] DEFAULT '{}' NOT NULL,
|
||||
tags_excluded TEXT [] DEFAULT '{}' NOT NULL,
|
||||
include_unmonitored BOOLEAN,
|
||||
include_alternate_titles BOOLEAN,
|
||||
last_refresh_time TIMESTAMP,
|
||||
last_refresh_status TEXT,
|
||||
last_refresh_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE list_filter
|
||||
(
|
||||
list_id INTEGER,
|
||||
filter_id INTEGER,
|
||||
FOREIGN KEY (list_id) REFERENCES list(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (list_id, filter_id)
|
||||
);
|
||||
`
|
||||
|
||||
var postgresMigrations = []string{
|
||||
|
@ -1002,5 +1034,37 @@ UPDATE irc_network
|
|||
|
||||
ALTER TABLE filter
|
||||
ADD COLUMN announce_types TEXT [] DEFAULT '{}';
|
||||
`,
|
||||
`CREATE TABLE list
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
enabled BOOLEAN,
|
||||
type TEXT NOT NULL,
|
||||
client_id INTEGER,
|
||||
url TEXT,
|
||||
headers TEXT [] DEFAULT '{}' NOT NULL,
|
||||
api_key TEXT,
|
||||
match_release BOOLEAN,
|
||||
tags_included TEXT [] DEFAULT '{}' NOT NULL,
|
||||
tags_excluded TEXT [] DEFAULT '{}' NOT NULL,
|
||||
include_unmonitored BOOLEAN,
|
||||
include_alternate_titles BOOLEAN,
|
||||
last_refresh_time TIMESTAMP,
|
||||
last_refresh_status TEXT,
|
||||
last_refresh_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE list_filter
|
||||
(
|
||||
list_id INTEGER,
|
||||
filter_id INTEGER,
|
||||
FOREIGN KEY (list_id) REFERENCES list(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (list_id, filter_id)
|
||||
);
|
||||
`,
|
||||
}
|
||||
|
|
|
@ -411,6 +411,38 @@ CREATE TABLE api_key
|
|||
scopes TEXT [] DEFAULT '{}' NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE list
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
enabled BOOLEAN,
|
||||
type TEXT NOT NULL,
|
||||
client_id INTEGER,
|
||||
url TEXT,
|
||||
headers TEXT [] DEFAULT '{}' NOT NULL,
|
||||
api_key TEXT,
|
||||
match_release BOOLEAN,
|
||||
tags_included TEXT [] DEFAULT '{}' NOT NULL,
|
||||
tags_excluded TEXT [] DEFAULT '{}' NOT NULL,
|
||||
include_unmonitored BOOLEAN,
|
||||
include_alternate_titles BOOLEAN,
|
||||
last_refresh_time TIMESTAMP,
|
||||
last_refresh_status TEXT,
|
||||
last_refresh_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE list_filter
|
||||
(
|
||||
list_id INTEGER,
|
||||
filter_id INTEGER,
|
||||
FOREIGN KEY (list_id) REFERENCES list(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (list_id, filter_id)
|
||||
);
|
||||
`
|
||||
|
||||
var sqliteMigrations = []string{
|
||||
|
@ -1644,5 +1676,37 @@ UPDATE irc_network
|
|||
|
||||
ALTER TABLE filter
|
||||
ADD COLUMN announce_types TEXT [] DEFAULT '{}';
|
||||
`,
|
||||
`CREATE TABLE list
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
enabled BOOLEAN,
|
||||
type TEXT NOT NULL,
|
||||
client_id INTEGER,
|
||||
url TEXT,
|
||||
headers TEXT [] DEFAULT '{}' NOT NULL,
|
||||
api_key TEXT,
|
||||
match_release BOOLEAN,
|
||||
tags_included TEXT [] DEFAULT '{}' NOT NULL,
|
||||
tags_excluded TEXT [] DEFAULT '{}' NOT NULL,
|
||||
include_unmonitored BOOLEAN,
|
||||
include_alternate_titles BOOLEAN,
|
||||
last_refresh_time TIMESTAMP,
|
||||
last_refresh_status TEXT,
|
||||
last_refresh_data TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE list_filter
|
||||
(
|
||||
list_id INTEGER,
|
||||
filter_id INTEGER,
|
||||
FOREIGN KEY (list_id) REFERENCES list(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (filter_id) REFERENCES filter(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (list_id, filter_id)
|
||||
);
|
||||
`,
|
||||
}
|
||||
|
|
|
@ -205,3 +205,8 @@ func (c DownloadClient) qbitBuildLegacyHost() (string, error) {
|
|||
// make into new string and return
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
type ArrTag struct {
|
||||
ID int `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
|
|
@ -164,6 +164,7 @@ type Filter struct {
|
|||
MaxLeechers int `json:"max_leechers,omitempty"`
|
||||
ActionsCount int `json:"actions_count"`
|
||||
ActionsEnabledCount int `json:"actions_enabled_count"`
|
||||
IsAutoUpdated bool `json:"is_auto_updated"`
|
||||
Actions []*Action `json:"actions,omitempty"`
|
||||
External []FilterExternal `json:"external,omitempty"`
|
||||
Indexers []Indexer `json:"indexers"`
|
||||
|
|
133
internal/domain/list.go
Normal file
133
internal/domain/list.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
)
|
||||
|
||||
type ListRepo interface {
|
||||
List(ctx context.Context) ([]*List, error)
|
||||
FindByID(ctx context.Context, listID int64) (*List, error)
|
||||
Store(ctx context.Context, listID *List) error
|
||||
Update(ctx context.Context, listID *List) error
|
||||
UpdateLastRefresh(ctx context.Context, list *List) error
|
||||
ToggleEnabled(ctx context.Context, listID int64, enabled bool) error
|
||||
Delete(ctx context.Context, listID int64) error
|
||||
GetListFilters(ctx context.Context, listID int64) ([]ListFilter, error)
|
||||
}
|
||||
|
||||
type ListType string
|
||||
|
||||
const (
|
||||
ListTypeRadarr ListType = "RADARR"
|
||||
ListTypeSonarr ListType = "SONARR"
|
||||
ListTypeLidarr ListType = "LIDARR"
|
||||
ListTypeReadarr ListType = "READARR"
|
||||
ListTypeWhisparr ListType = "WHISPARR"
|
||||
ListTypeMDBList ListType = "MDBLIST"
|
||||
ListTypeMetacritic ListType = "METACRITIC"
|
||||
ListTypePlaintext ListType = "PLAINTEXT"
|
||||
ListTypeTrakt ListType = "TRAKT"
|
||||
ListTypeSteam ListType = "STEAM"
|
||||
)
|
||||
|
||||
type ListRefreshStatus string
|
||||
|
||||
const (
|
||||
ListRefreshStatusSuccess ListRefreshStatus = "SUCCESS"
|
||||
ListRefreshStatusError ListRefreshStatus = "ERROR"
|
||||
)
|
||||
|
||||
type List struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type ListType `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ClientID int `json:"client_id"`
|
||||
URL string `json:"url"`
|
||||
Headers []string `json:"headers"`
|
||||
APIKey string `json:"api_key"`
|
||||
Filters []ListFilter `json:"filters"`
|
||||
MatchRelease bool `json:"match_release"`
|
||||
TagsInclude []string `json:"tags_included"`
|
||||
TagsExclude []string `json:"tags_excluded"`
|
||||
IncludeUnmonitored bool `json:"include_unmonitored"`
|
||||
IncludeAlternateTitles bool `json:"include_alternate_titles"`
|
||||
LastRefreshTime time.Time `json:"last_refresh_time"`
|
||||
LastRefreshData string `json:"last_refresh_error"`
|
||||
LastRefreshStatus ListRefreshStatus `json:"last_refresh_status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (l *List) Validate() error {
|
||||
if l.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
if l.Type == "" {
|
||||
return errors.New("type is required")
|
||||
}
|
||||
|
||||
if !l.ListTypeArr() && !l.ListTypeList() {
|
||||
return errors.New("invalid list type: %s", l.Type)
|
||||
}
|
||||
|
||||
if l.ListTypeArr() && l.ClientID == 0 {
|
||||
return errors.New("arr client id is required")
|
||||
}
|
||||
|
||||
if l.ListTypeList() {
|
||||
if l.URL == "" {
|
||||
return errors.New("list url is required")
|
||||
}
|
||||
|
||||
_, err := url.Parse(l.URL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse list url: %s", l.URL)
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.Filters) == 0 {
|
||||
return errors.New("at least one filter is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *List) ListTypeArr() bool {
|
||||
return l.Type == ListTypeRadarr || l.Type == ListTypeSonarr || l.Type == ListTypeLidarr || l.Type == ListTypeReadarr || l.Type == ListTypeWhisparr
|
||||
}
|
||||
|
||||
func (l *List) ListTypeList() bool {
|
||||
return l.Type == ListTypeMDBList || l.Type == ListTypeMetacritic || l.Type == ListTypePlaintext || l.Type == ListTypeTrakt || l.Type == ListTypeSteam
|
||||
}
|
||||
|
||||
func (l *List) ShouldProcessItem(monitored bool) bool {
|
||||
if l.IncludeUnmonitored {
|
||||
return true
|
||||
}
|
||||
|
||||
return monitored
|
||||
}
|
||||
|
||||
// SetRequestHeaders set headers from list on the request
|
||||
func (l *List) SetRequestHeaders(req *http.Request) {
|
||||
for _, header := range l.Headers {
|
||||
parts := strings.Split(header, "=")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
req.Header.Set(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
type ListFilter struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
|
@ -12,13 +12,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/pkg/arr/lidarr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/radarr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/readarr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/sonarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
"github.com/autobrr/autobrr/pkg/lidarr"
|
||||
"github.com/autobrr/autobrr/pkg/porla"
|
||||
"github.com/autobrr/autobrr/pkg/radarr"
|
||||
"github.com/autobrr/autobrr/pkg/readarr"
|
||||
"github.com/autobrr/autobrr/pkg/sabnzbd"
|
||||
"github.com/autobrr/autobrr/pkg/sonarr"
|
||||
"github.com/autobrr/autobrr/pkg/transmission"
|
||||
"github.com/autobrr/autobrr/pkg/whisparr"
|
||||
|
||||
|
|
|
@ -15,13 +15,13 @@ import (
|
|||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
"github.com/autobrr/autobrr/internal/logger"
|
||||
"github.com/autobrr/autobrr/pkg/arr/lidarr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/radarr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/readarr"
|
||||
"github.com/autobrr/autobrr/pkg/arr/sonarr"
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
"github.com/autobrr/autobrr/pkg/lidarr"
|
||||
"github.com/autobrr/autobrr/pkg/porla"
|
||||
"github.com/autobrr/autobrr/pkg/radarr"
|
||||
"github.com/autobrr/autobrr/pkg/readarr"
|
||||
"github.com/autobrr/autobrr/pkg/sabnzbd"
|
||||
"github.com/autobrr/autobrr/pkg/sonarr"
|
||||
"github.com/autobrr/autobrr/pkg/transmission"
|
||||
"github.com/autobrr/autobrr/pkg/whisparr"
|
||||
|
||||
|
@ -41,6 +41,7 @@ type Service interface {
|
|||
Delete(ctx context.Context, clientID int32) error
|
||||
Test(ctx context.Context, client domain.DownloadClient) error
|
||||
|
||||
GetArrTags(ctx context.Context, id int32) ([]*domain.ArrTag, error)
|
||||
GetClient(ctx context.Context, clientId int32) (*domain.DownloadClient, error)
|
||||
}
|
||||
|
||||
|
@ -94,6 +95,57 @@ func (s *service) FindByID(ctx context.Context, id int32) (*domain.DownloadClien
|
|||
return client, nil
|
||||
}
|
||||
|
||||
func (s *service) GetArrTags(ctx context.Context, id int32) ([]*domain.ArrTag, error) {
|
||||
data := make([]*domain.ArrTag, 0)
|
||||
|
||||
client, err := s.GetClient(ctx, id)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msgf("could not find download client by id: %v", id)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
switch client.Type {
|
||||
case "RADARR":
|
||||
arrClient := client.Client.(*radarr.Client)
|
||||
tags, err := arrClient.GetTags(ctx)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msgf("could not get tags from radarr: %v", id)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
emt := &domain.ArrTag{
|
||||
ID: tag.ID,
|
||||
Label: tag.Label,
|
||||
}
|
||||
data = append(data, emt)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
case "SONARR":
|
||||
arrClient := client.Client.(*sonarr.Client)
|
||||
tags, err := arrClient.GetTags(ctx)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msgf("could not get tags from sonarr: %v", id)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
emt := &domain.ArrTag{
|
||||
ID: tag.ID,
|
||||
Label: tag.Label,
|
||||
}
|
||||
data = append(data, emt)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Store(ctx context.Context, client *domain.DownloadClient) error {
|
||||
// basic validation of client
|
||||
if err := client.Validate(); err != nil {
|
||||
|
|
|
@ -490,8 +490,7 @@ func (s *service) CheckFilter(ctx context.Context, f *domain.Filter, release *do
|
|||
// it is necessary to download the torrent file and parse it to make the size
|
||||
// check. We use the API where available to minimize the number of torrents we
|
||||
// need to download.
|
||||
func (s *service) AdditionalSizeCheck(ctx context.Context, f *domain.Filter, release *domain.Release) (bool, error) {
|
||||
var err error
|
||||
func (s *service) AdditionalSizeCheck(ctx context.Context, f *domain.Filter, release *domain.Release) (ok bool, err error) {
|
||||
defer func() {
|
||||
// try recover panic if anything went wrong with API or size checks
|
||||
errors.RecoverPanic(recover(), &err)
|
||||
|
@ -554,8 +553,7 @@ func (s *service) AdditionalSizeCheck(ctx context.Context, f *domain.Filter, rel
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (s *service) AdditionalUploaderCheck(ctx context.Context, f *domain.Filter, release *domain.Release) (bool, error) {
|
||||
var err error
|
||||
func (s *service) AdditionalUploaderCheck(ctx context.Context, f *domain.Filter, release *domain.Release) (ok bool, err error) {
|
||||
defer func() {
|
||||
// try recover panic if anything went wrong with API or size checks
|
||||
errors.RecoverPanic(recover(), &err)
|
||||
|
@ -634,9 +632,7 @@ func (s *service) CheckSmartEpisodeCanDownload(ctx context.Context, params *doma
|
|||
return s.releaseRepo.CheckSmartEpisodeCanDownload(ctx, params)
|
||||
}
|
||||
|
||||
func (s *service) RunExternalFilters(ctx context.Context, f *domain.Filter, externalFilters []domain.FilterExternal, release *domain.Release) (bool, error) {
|
||||
var err error
|
||||
|
||||
func (s *service) RunExternalFilters(ctx context.Context, f *domain.Filter, externalFilters []domain.FilterExternal, release *domain.Release) (ok bool, err error) {
|
||||
defer func() {
|
||||
// try recover panic if anything went wrong with the external filter checks
|
||||
errors.RecoverPanic(recover(), &err)
|
||||
|
|
|
@ -22,6 +22,7 @@ type downloadClientService interface {
|
|||
Update(ctx context.Context, client *domain.DownloadClient) error
|
||||
Delete(ctx context.Context, clientID int32) error
|
||||
Test(ctx context.Context, client domain.DownloadClient) error
|
||||
GetArrTags(ctx context.Context, id int32) ([]*domain.ArrTag, error)
|
||||
}
|
||||
|
||||
type downloadClientHandler struct {
|
||||
|
@ -45,6 +46,8 @@ func (h downloadClientHandler) Routes(r chi.Router) {
|
|||
r.Route("/{clientID}", func(r chi.Router) {
|
||||
r.Get("/", h.findByID)
|
||||
r.Delete("/", h.delete)
|
||||
|
||||
r.Get("/arr/tags", h.findArrTagsByID)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -78,6 +81,26 @@ func (h downloadClientHandler) findByID(w http.ResponseWriter, r *http.Request)
|
|||
h.encoder.StatusResponse(w, http.StatusOK, client)
|
||||
}
|
||||
|
||||
func (h downloadClientHandler) findArrTagsByID(w http.ResponseWriter, r *http.Request) {
|
||||
clientID, err := strconv.ParseInt(chi.URLParam(r, "clientID"), 10, 32)
|
||||
if err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := h.service.GetArrTags(r.Context(), int32(clientID))
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrRecordNotFound) {
|
||||
h.encoder.NotFoundErr(w, errors.New("download client with id %d not found", clientID))
|
||||
}
|
||||
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.StatusResponse(w, http.StatusOK, client)
|
||||
}
|
||||
|
||||
func (h downloadClientHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||
var data *domain.DownloadClient
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
|
|
124
internal/http/list.go
Normal file
124
internal/http/list.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type listService interface {
|
||||
Store(ctx context.Context, list *domain.List) error
|
||||
Update(ctx context.Context, list *domain.List) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
FindByID(ctx context.Context, id int64) (*domain.List, error)
|
||||
List(ctx context.Context) ([]*domain.List, error)
|
||||
RefreshList(ctx context.Context, id int64) error
|
||||
RefreshAll(ctx context.Context) error
|
||||
RefreshArrLists(ctx context.Context) error
|
||||
RefreshOtherLists(ctx context.Context) error
|
||||
}
|
||||
|
||||
type listHandler struct {
|
||||
encoder encoder
|
||||
listSvc listService
|
||||
}
|
||||
|
||||
func newListHandler(encoder encoder, service listService) *listHandler {
|
||||
return &listHandler{encoder: encoder, listSvc: service}
|
||||
}
|
||||
|
||||
func (h listHandler) Routes(r chi.Router) {
|
||||
r.Get("/", h.list)
|
||||
r.Post("/", h.store)
|
||||
r.Post("/refresh", h.refreshAll)
|
||||
|
||||
r.Route("/{listID}", func(r chi.Router) {
|
||||
r.Post("/refresh", h.refreshList)
|
||||
r.Put("/", h.update)
|
||||
r.Delete("/", h.delete)
|
||||
})
|
||||
}
|
||||
|
||||
func (h listHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := h.listSvc.List(r.Context())
|
||||
if err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.StatusResponse(w, http.StatusOK, data)
|
||||
}
|
||||
|
||||
func (h listHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||
var data *domain.List
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.listSvc.Store(r.Context(), data); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.StatusCreatedData(w, data)
|
||||
}
|
||||
|
||||
func (h listHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||
var data *domain.List
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.listSvc.Update(r.Context(), data); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
||||
|
||||
func (h listHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||
listID, err := strconv.Atoi(chi.URLParam(r, "listID"))
|
||||
if err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.listSvc.Delete(r.Context(), int64(listID)); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
||||
|
||||
func (h listHandler) refreshAll(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.listSvc.RefreshAll(r.Context()); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
||||
|
||||
func (h listHandler) refreshList(w http.ResponseWriter, r *http.Request) {
|
||||
listID, err := strconv.Atoi(chi.URLParam(r, "listID"))
|
||||
if err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.listSvc.RefreshList(r.Context(), int64(listID)); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
|
@ -42,13 +42,14 @@ type Server struct {
|
|||
feedService feedService
|
||||
indexerService indexerService
|
||||
ircService ircService
|
||||
listService listService
|
||||
notificationService notificationService
|
||||
proxyService proxyService
|
||||
releaseService releaseService
|
||||
updateService updateService
|
||||
}
|
||||
|
||||
func NewServer(log logger.Logger, config *config.AppConfig, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, apiService apikeyService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, notificationSvc notificationService, proxySvc proxyService, releaseSvc releaseService, updateSvc updateService) Server {
|
||||
func NewServer(log logger.Logger, config *config.AppConfig, sse *sse.Server, db *database.DB, version string, commit string, date string, actionService actionService, apiService apikeyService, authService authService, downloadClientSvc downloadClientService, filterSvc filterService, feedSvc feedService, indexerSvc indexerService, ircSvc ircService, listSvc listService, notificationSvc notificationService, proxySvc proxyService, releaseSvc releaseService, updateSvc updateService) Server {
|
||||
return Server{
|
||||
log: log.With().Str("module", "http").Logger(),
|
||||
config: config,
|
||||
|
@ -68,6 +69,7 @@ func NewServer(log logger.Logger, config *config.AppConfig, sse *sse.Server, db
|
|||
feedService: feedSvc,
|
||||
indexerService: indexerSvc,
|
||||
ircService: ircSvc,
|
||||
listService: listSvc,
|
||||
notificationService: notificationSvc,
|
||||
proxyService: proxySvc,
|
||||
releaseService: releaseSvc,
|
||||
|
@ -142,12 +144,14 @@ func (s Server) Handler() http.Handler {
|
|||
r.Route("/feeds", newFeedHandler(encoder, s.feedService).Routes)
|
||||
r.Route("/irc", newIrcHandler(encoder, s.sse, s.ircService).Routes)
|
||||
r.Route("/indexer", newIndexerHandler(encoder, s.indexerService, s.ircService).Routes)
|
||||
r.Route("/lists", newListHandler(encoder, s.listService).Routes)
|
||||
r.Route("/keys", newAPIKeyHandler(encoder, s.apiService).Routes)
|
||||
r.Route("/logs", newLogsHandler(s.config).Routes)
|
||||
r.Route("/notification", newNotificationHandler(encoder, s.notificationService).Routes)
|
||||
r.Route("/proxy", newProxyHandler(encoder, s.proxyService).Routes)
|
||||
r.Route("/release", newReleaseHandler(encoder, s.releaseService).Routes)
|
||||
r.Route("/updates", newUpdateHandler(encoder, s.updateService).Routes)
|
||||
r.Route("/webhook", newWebhookHandler(encoder, s.listService).Routes)
|
||||
|
||||
r.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
|
|
71
internal/http/webhook.go
Normal file
71
internal/http/webhook.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type webhookHandler struct {
|
||||
encoder encoder
|
||||
listSvc listService
|
||||
}
|
||||
|
||||
func newWebhookHandler(encoder encoder, listSvc listService) *webhookHandler {
|
||||
return &webhookHandler{encoder: encoder, listSvc: listSvc}
|
||||
}
|
||||
|
||||
func (h *webhookHandler) Routes(r chi.Router) {
|
||||
r.Route("/lists", func(r chi.Router) {
|
||||
r.Post("/trigger", h.refreshAll)
|
||||
r.Post("/trigger/arr", h.refreshArr)
|
||||
r.Post("/trigger/lists", h.refreshLists)
|
||||
|
||||
r.Get("/trigger", h.refreshAll)
|
||||
r.Get("/trigger/arr", h.refreshArr)
|
||||
r.Get("/trigger/lists", h.refreshLists)
|
||||
|
||||
r.Post("/trigger/{listID}", h.refreshByID)
|
||||
})
|
||||
}
|
||||
|
||||
func (h *webhookHandler) refreshAll(w http.ResponseWriter, r *http.Request) {
|
||||
go h.listSvc.RefreshAll(context.Background())
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
||||
|
||||
func (h *webhookHandler) refreshByID(w http.ResponseWriter, r *http.Request) {
|
||||
listID, err := strconv.Atoi(chi.URLParam(r, "listID"))
|
||||
if err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.listSvc.RefreshList(context.Background(), int64(listID)); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
||||
|
||||
func (h *webhookHandler) refreshArr(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.listSvc.RefreshArrLists(r.Context()); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
||||
|
||||
func (h *webhookHandler) refreshLists(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.listSvc.RefreshOtherLists(r.Context()); err != nil {
|
||||
h.encoder.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.encoder.NoContent(w)
|
||||
}
|
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
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/autobrr/autobrr/internal/feed"
|
||||
"github.com/autobrr/autobrr/internal/indexer"
|
||||
"github.com/autobrr/autobrr/internal/irc"
|
||||
"github.com/autobrr/autobrr/internal/list"
|
||||
"github.com/autobrr/autobrr/internal/logger"
|
||||
"github.com/autobrr/autobrr/internal/scheduler"
|
||||
"github.com/autobrr/autobrr/internal/update"
|
||||
|
@ -27,19 +28,21 @@ type Server struct {
|
|||
ircService irc.Service
|
||||
feedService feed.Service
|
||||
scheduler scheduler.Service
|
||||
listService list.Service
|
||||
updateService *update.Service
|
||||
|
||||
stopWG sync.WaitGroup
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewServer(log logger.Logger, config *domain.Config, ircSvc irc.Service, indexerSvc indexer.Service, feedSvc feed.Service, scheduler scheduler.Service, updateSvc *update.Service) *Server {
|
||||
func NewServer(log logger.Logger, config *domain.Config, ircSvc irc.Service, indexerSvc indexer.Service, feedSvc feed.Service, listSvc list.Service, scheduler scheduler.Service, updateSvc *update.Service) *Server {
|
||||
return &Server{
|
||||
log: log.With().Str("module", "server").Logger(),
|
||||
config: config,
|
||||
indexerService: indexerSvc,
|
||||
ircService: ircSvc,
|
||||
feedService: feedSvc,
|
||||
listService: listSvc,
|
||||
scheduler: scheduler,
|
||||
updateService: updateSvc,
|
||||
}
|
||||
|
@ -65,6 +68,9 @@ func (s *Server) Start() error {
|
|||
s.log.Error().Err(err).Msg("Could not start feed service")
|
||||
}
|
||||
|
||||
// start lists background updater
|
||||
go s.listService.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue