mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(releases): replay actions (#932)
* feat(releases): replay actions * feat(releases): replay actions component * fix: update filter actions * fix: select filter_id from ras
This commit is contained in:
parent
97333d334f
commit
6898ad8315
16 changed files with 752 additions and 189 deletions
|
@ -99,7 +99,7 @@ func (s *service) RunAction(ctx context.Context, action *domain.Action, release
|
||||||
payload := &domain.NotificationPayload{
|
payload := &domain.NotificationPayload{
|
||||||
Event: domain.NotificationEventPushApproved,
|
Event: domain.NotificationEventPushApproved,
|
||||||
ReleaseName: release.TorrentName,
|
ReleaseName: release.TorrentName,
|
||||||
Filter: release.Filter.Name,
|
Filter: release.FilterName,
|
||||||
Indexer: release.Indexer,
|
Indexer: release.Indexer,
|
||||||
InfoHash: release.TorrentHash,
|
InfoHash: release.TorrentHash,
|
||||||
Size: release.Size,
|
Size: release.Size,
|
||||||
|
|
|
@ -19,7 +19,8 @@ import (
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Store(ctx context.Context, action domain.Action) (*domain.Action, error)
|
Store(ctx context.Context, action domain.Action) (*domain.Action, error)
|
||||||
List(ctx context.Context) ([]domain.Action, error)
|
List(ctx context.Context) ([]domain.Action, error)
|
||||||
Delete(actionID int) error
|
Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error)
|
||||||
|
Delete(ctx context.Context, req *domain.DeleteActionRequest) error
|
||||||
DeleteByFilterID(ctx context.Context, filterID int) error
|
DeleteByFilterID(ctx context.Context, filterID int) error
|
||||||
ToggleEnabled(actionID int) error
|
ToggleEnabled(actionID int) error
|
||||||
|
|
||||||
|
@ -51,18 +52,37 @@ func (s *service) Store(ctx context.Context, action domain.Action) (*domain.Acti
|
||||||
return s.repo.Store(ctx, action)
|
return s.repo.Store(ctx, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Delete(actionID int) error {
|
func (s *service) List(ctx context.Context) ([]domain.Action, error) {
|
||||||
return s.repo.Delete(actionID)
|
return s.repo.List(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error) {
|
||||||
|
a, err := s.repo.Get(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionally attach download client to action
|
||||||
|
if a.ClientID > 0 {
|
||||||
|
client, err := s.clientSvc.FindByID(ctx, a.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Delete(ctx context.Context, req *domain.DeleteActionRequest) error {
|
||||||
|
return s.repo.Delete(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) DeleteByFilterID(ctx context.Context, filterID int) error {
|
func (s *service) DeleteByFilterID(ctx context.Context, filterID int) error {
|
||||||
return s.repo.DeleteByFilterID(ctx, filterID)
|
return s.repo.DeleteByFilterID(ctx, filterID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) List(ctx context.Context) ([]domain.Action, error) {
|
|
||||||
return s.repo.List(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) ToggleEnabled(actionID int) error {
|
func (s *service) ToggleEnabled(actionID int) error {
|
||||||
return s.repo.ToggleEnabled(actionID)
|
return s.repo.ToggleEnabled(actionID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -280,30 +280,117 @@ func (r *ActionRepo) List(ctx context.Context) ([]domain.Action, error) {
|
||||||
a.ClientID = clientID.Int32
|
a.ClientID = clientID.Int32
|
||||||
|
|
||||||
actions = append(actions, a)
|
actions = append(actions, a)
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, errors.Wrap(err, "rows error")
|
return nil, errors.Wrap(err, "rows error")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions, nil
|
return actions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ActionRepo) Delete(actionID int) error {
|
func (r *ActionRepo) Get(ctx context.Context, req *domain.GetActionRequest) (*domain.Action, error) {
|
||||||
|
queryBuilder := r.db.squirrel.
|
||||||
|
Select(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"enabled",
|
||||||
|
"exec_cmd",
|
||||||
|
"exec_args",
|
||||||
|
"watch_folder",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"label",
|
||||||
|
"save_path",
|
||||||
|
"paused",
|
||||||
|
"ignore_rules",
|
||||||
|
"limit_download_speed",
|
||||||
|
"limit_upload_speed",
|
||||||
|
"limit_ratio",
|
||||||
|
"limit_seed_time",
|
||||||
|
"reannounce_skip",
|
||||||
|
"reannounce_delete",
|
||||||
|
"reannounce_interval",
|
||||||
|
"reannounce_max_attempts",
|
||||||
|
"webhook_host",
|
||||||
|
"webhook_type",
|
||||||
|
"webhook_method",
|
||||||
|
"webhook_data",
|
||||||
|
"client_id",
|
||||||
|
"filter_id",
|
||||||
|
).
|
||||||
|
From("action").
|
||||||
|
Where(sq.Eq{"id": req.Id})
|
||||||
|
|
||||||
|
query, args, err := queryBuilder.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error building query")
|
||||||
|
}
|
||||||
|
|
||||||
|
row := r.db.handler.QueryRowContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error executing query")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "rows error")
|
||||||
|
}
|
||||||
|
|
||||||
|
var a domain.Action
|
||||||
|
|
||||||
|
var execCmd, execArgs, watchFolder, category, tags, label, savePath, webhookHost, webhookType, webhookMethod, webhookData sql.NullString
|
||||||
|
var limitUl, limitDl, limitSeedTime sql.NullInt64
|
||||||
|
var limitRatio sql.NullFloat64
|
||||||
|
var clientID, filterID sql.NullInt32
|
||||||
|
var paused, ignoreRules sql.NullBool
|
||||||
|
|
||||||
|
if err := row.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &limitDl, &limitUl, &limitRatio, &limitSeedTime, &a.ReAnnounceSkip, &a.ReAnnounceDelete, &a.ReAnnounceInterval, &a.ReAnnounceMaxAttempts, &webhookHost, &webhookType, &webhookMethod, &webhookData, &clientID, &filterID); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Wrap(err, "error scanning row")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Category = category.String
|
||||||
|
a.Tags = tags.String
|
||||||
|
a.Label = label.String
|
||||||
|
a.SavePath = savePath.String
|
||||||
|
a.Paused = paused.Bool
|
||||||
|
a.IgnoreRules = ignoreRules.Bool
|
||||||
|
|
||||||
|
a.LimitDownloadSpeed = limitDl.Int64
|
||||||
|
a.LimitUploadSpeed = limitUl.Int64
|
||||||
|
a.LimitRatio = limitRatio.Float64
|
||||||
|
a.LimitSeedTime = limitSeedTime.Int64
|
||||||
|
|
||||||
|
a.WebhookHost = webhookHost.String
|
||||||
|
a.WebhookType = webhookType.String
|
||||||
|
a.WebhookMethod = webhookMethod.String
|
||||||
|
a.WebhookData = webhookData.String
|
||||||
|
|
||||||
|
a.ClientID = clientID.Int32
|
||||||
|
a.FilterID = int(filterID.Int32)
|
||||||
|
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActionRepo) Delete(ctx context.Context, req *domain.DeleteActionRequest) error {
|
||||||
queryBuilder := r.db.squirrel.
|
queryBuilder := r.db.squirrel.
|
||||||
Delete("action").
|
Delete("action").
|
||||||
Where(sq.Eq{"id": actionID})
|
Where(sq.Eq{"id": req.ActionId})
|
||||||
|
|
||||||
query, args, err := queryBuilder.ToSql()
|
query, args, err := queryBuilder.ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "error building query")
|
return errors.Wrap(err, "error building query")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = r.db.handler.Exec(query, args...)
|
if _, err = r.db.handler.ExecContext(ctx, query, args...); err != nil {
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error executing query")
|
return errors.Wrap(err, "error executing query")
|
||||||
}
|
}
|
||||||
|
|
||||||
r.log.Debug().Msgf("action.delete: %v", actionID)
|
r.log.Debug().Msgf("action.delete: %v", req.ActionId)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -506,113 +593,171 @@ func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []*domain.A
|
||||||
|
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
deleteQueryBuilder := r.db.squirrel.
|
|
||||||
Delete("action").
|
|
||||||
Where(sq.Eq{"filter_id": filterID})
|
|
||||||
|
|
||||||
deleteQuery, deleteArgs, err := deleteQueryBuilder.ToSql()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error building query")
|
|
||||||
}
|
|
||||||
_, err = tx.ExecContext(ctx, deleteQuery, deleteArgs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error executing query")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, action := range actions {
|
for _, action := range actions {
|
||||||
execCmd := toNullString(action.ExecCmd)
|
action := action
|
||||||
execArgs := toNullString(action.ExecArgs)
|
|
||||||
watchFolder := toNullString(action.WatchFolder)
|
|
||||||
category := toNullString(action.Category)
|
|
||||||
tags := toNullString(action.Tags)
|
|
||||||
label := toNullString(action.Label)
|
|
||||||
savePath := toNullString(action.SavePath)
|
|
||||||
contentLayout := toNullString(string(action.ContentLayout))
|
|
||||||
webhookHost := toNullString(action.WebhookHost)
|
|
||||||
webhookType := toNullString(action.WebhookType)
|
|
||||||
webhookMethod := toNullString(action.WebhookMethod)
|
|
||||||
webhookData := toNullString(action.WebhookData)
|
|
||||||
|
|
||||||
limitDL := toNullInt64(action.LimitDownloadSpeed)
|
if action.ID > 0 {
|
||||||
limitUL := toNullInt64(action.LimitUploadSpeed)
|
execCmd := toNullString(action.ExecCmd)
|
||||||
limitRatio := toNullFloat64(action.LimitRatio)
|
execArgs := toNullString(action.ExecArgs)
|
||||||
limitSeedTime := toNullInt64(action.LimitSeedTime)
|
watchFolder := toNullString(action.WatchFolder)
|
||||||
clientID := toNullInt32(action.ClientID)
|
category := toNullString(action.Category)
|
||||||
|
tags := toNullString(action.Tags)
|
||||||
|
label := toNullString(action.Label)
|
||||||
|
savePath := toNullString(action.SavePath)
|
||||||
|
contentLayout := toNullString(string(action.ContentLayout))
|
||||||
|
webhookHost := toNullString(action.WebhookHost)
|
||||||
|
webhookType := toNullString(action.WebhookType)
|
||||||
|
webhookMethod := toNullString(action.WebhookMethod)
|
||||||
|
webhookData := toNullString(action.WebhookData)
|
||||||
|
|
||||||
queryBuilder := r.db.squirrel.
|
limitDL := toNullInt64(action.LimitDownloadSpeed)
|
||||||
Insert("action").
|
limitUL := toNullInt64(action.LimitUploadSpeed)
|
||||||
Columns(
|
limitRatio := toNullFloat64(action.LimitRatio)
|
||||||
"name",
|
limitSeedTime := toNullInt64(action.LimitSeedTime)
|
||||||
"type",
|
|
||||||
"enabled",
|
|
||||||
"exec_cmd",
|
|
||||||
"exec_args",
|
|
||||||
"watch_folder",
|
|
||||||
"category",
|
|
||||||
"tags",
|
|
||||||
"label",
|
|
||||||
"save_path",
|
|
||||||
"paused",
|
|
||||||
"ignore_rules",
|
|
||||||
"skip_hash_check",
|
|
||||||
"content_layout",
|
|
||||||
"limit_upload_speed",
|
|
||||||
"limit_download_speed",
|
|
||||||
"limit_ratio",
|
|
||||||
"limit_seed_time",
|
|
||||||
"reannounce_skip",
|
|
||||||
"reannounce_delete",
|
|
||||||
"reannounce_interval",
|
|
||||||
"reannounce_max_attempts",
|
|
||||||
"webhook_host",
|
|
||||||
"webhook_type",
|
|
||||||
"webhook_method",
|
|
||||||
"webhook_data",
|
|
||||||
"client_id",
|
|
||||||
"filter_id",
|
|
||||||
).
|
|
||||||
Values(
|
|
||||||
action.Name,
|
|
||||||
action.Type,
|
|
||||||
action.Enabled,
|
|
||||||
execCmd,
|
|
||||||
execArgs,
|
|
||||||
watchFolder,
|
|
||||||
category,
|
|
||||||
tags,
|
|
||||||
label,
|
|
||||||
savePath,
|
|
||||||
action.Paused,
|
|
||||||
action.IgnoreRules,
|
|
||||||
action.SkipHashCheck,
|
|
||||||
contentLayout,
|
|
||||||
limitUL,
|
|
||||||
limitDL,
|
|
||||||
limitRatio,
|
|
||||||
limitSeedTime,
|
|
||||||
action.ReAnnounceSkip,
|
|
||||||
action.ReAnnounceDelete,
|
|
||||||
action.ReAnnounceInterval,
|
|
||||||
action.ReAnnounceMaxAttempts,
|
|
||||||
webhookHost,
|
|
||||||
webhookType,
|
|
||||||
webhookMethod,
|
|
||||||
webhookData,
|
|
||||||
clientID,
|
|
||||||
filterID,
|
|
||||||
).
|
|
||||||
Suffix("RETURNING id").RunWith(tx)
|
|
||||||
|
|
||||||
// return values
|
clientID := toNullInt32(action.ClientID)
|
||||||
var retID int
|
|
||||||
|
|
||||||
err = queryBuilder.QueryRowContext(ctx).Scan(&retID)
|
var err error
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error executing query")
|
queryBuilder := r.db.squirrel.
|
||||||
|
Update("action").
|
||||||
|
Set("name", action.Name).
|
||||||
|
Set("type", action.Type).
|
||||||
|
Set("enabled", action.Enabled).
|
||||||
|
Set("exec_cmd", execCmd).
|
||||||
|
Set("exec_args", execArgs).
|
||||||
|
Set("watch_folder", watchFolder).
|
||||||
|
Set("category", category).
|
||||||
|
Set("tags", tags).
|
||||||
|
Set("label", label).
|
||||||
|
Set("save_path", savePath).
|
||||||
|
Set("paused", action.Paused).
|
||||||
|
Set("ignore_rules", action.IgnoreRules).
|
||||||
|
Set("skip_hash_check", action.SkipHashCheck).
|
||||||
|
Set("content_layout", contentLayout).
|
||||||
|
Set("limit_upload_speed", limitUL).
|
||||||
|
Set("limit_download_speed", limitDL).
|
||||||
|
Set("limit_ratio", limitRatio).
|
||||||
|
Set("limit_seed_time", limitSeedTime).
|
||||||
|
Set("reannounce_skip", action.ReAnnounceSkip).
|
||||||
|
Set("reannounce_delete", action.ReAnnounceDelete).
|
||||||
|
Set("reannounce_interval", action.ReAnnounceInterval).
|
||||||
|
Set("reannounce_max_attempts", action.ReAnnounceMaxAttempts).
|
||||||
|
Set("webhook_host", webhookHost).
|
||||||
|
Set("webhook_type", webhookType).
|
||||||
|
Set("webhook_method", webhookMethod).
|
||||||
|
Set("webhook_data", webhookData).
|
||||||
|
Set("client_id", clientID).
|
||||||
|
Set("filter_id", filterID).
|
||||||
|
Where(sq.Eq{"id": action.ID})
|
||||||
|
|
||||||
|
query, args, err := queryBuilder.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error building query")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = tx.ExecContext(ctx, query, args...); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error executing query")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.log.Trace().Msgf("action.StoreFilterActions: update %d", action.ID)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
execCmd := toNullString(action.ExecCmd)
|
||||||
|
execArgs := toNullString(action.ExecArgs)
|
||||||
|
watchFolder := toNullString(action.WatchFolder)
|
||||||
|
category := toNullString(action.Category)
|
||||||
|
tags := toNullString(action.Tags)
|
||||||
|
label := toNullString(action.Label)
|
||||||
|
savePath := toNullString(action.SavePath)
|
||||||
|
contentLayout := toNullString(string(action.ContentLayout))
|
||||||
|
webhookHost := toNullString(action.WebhookHost)
|
||||||
|
webhookType := toNullString(action.WebhookType)
|
||||||
|
webhookMethod := toNullString(action.WebhookMethod)
|
||||||
|
webhookData := toNullString(action.WebhookData)
|
||||||
|
|
||||||
|
limitDL := toNullInt64(action.LimitDownloadSpeed)
|
||||||
|
limitUL := toNullInt64(action.LimitUploadSpeed)
|
||||||
|
limitRatio := toNullFloat64(action.LimitRatio)
|
||||||
|
limitSeedTime := toNullInt64(action.LimitSeedTime)
|
||||||
|
clientID := toNullInt32(action.ClientID)
|
||||||
|
|
||||||
|
queryBuilder := r.db.squirrel.
|
||||||
|
Insert("action").
|
||||||
|
Columns(
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"enabled",
|
||||||
|
"exec_cmd",
|
||||||
|
"exec_args",
|
||||||
|
"watch_folder",
|
||||||
|
"category",
|
||||||
|
"tags",
|
||||||
|
"label",
|
||||||
|
"save_path",
|
||||||
|
"paused",
|
||||||
|
"ignore_rules",
|
||||||
|
"skip_hash_check",
|
||||||
|
"content_layout",
|
||||||
|
"limit_upload_speed",
|
||||||
|
"limit_download_speed",
|
||||||
|
"limit_ratio",
|
||||||
|
"limit_seed_time",
|
||||||
|
"reannounce_skip",
|
||||||
|
"reannounce_delete",
|
||||||
|
"reannounce_interval",
|
||||||
|
"reannounce_max_attempts",
|
||||||
|
"webhook_host",
|
||||||
|
"webhook_type",
|
||||||
|
"webhook_method",
|
||||||
|
"webhook_data",
|
||||||
|
"client_id",
|
||||||
|
"filter_id",
|
||||||
|
).
|
||||||
|
Values(
|
||||||
|
action.Name,
|
||||||
|
action.Type,
|
||||||
|
action.Enabled,
|
||||||
|
execCmd,
|
||||||
|
execArgs,
|
||||||
|
watchFolder,
|
||||||
|
category,
|
||||||
|
tags,
|
||||||
|
label,
|
||||||
|
savePath,
|
||||||
|
action.Paused,
|
||||||
|
action.IgnoreRules,
|
||||||
|
action.SkipHashCheck,
|
||||||
|
contentLayout,
|
||||||
|
limitUL,
|
||||||
|
limitDL,
|
||||||
|
limitRatio,
|
||||||
|
limitSeedTime,
|
||||||
|
action.ReAnnounceSkip,
|
||||||
|
action.ReAnnounceDelete,
|
||||||
|
action.ReAnnounceInterval,
|
||||||
|
action.ReAnnounceMaxAttempts,
|
||||||
|
webhookHost,
|
||||||
|
webhookType,
|
||||||
|
webhookMethod,
|
||||||
|
webhookData,
|
||||||
|
clientID,
|
||||||
|
filterID,
|
||||||
|
).
|
||||||
|
Suffix("RETURNING id").RunWith(tx)
|
||||||
|
|
||||||
|
// return values
|
||||||
|
var retID int
|
||||||
|
|
||||||
|
if err = queryBuilder.QueryRowContext(ctx).Scan(&retID); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error executing query")
|
||||||
|
}
|
||||||
|
|
||||||
|
action.ID = retID
|
||||||
|
|
||||||
|
r.log.Trace().Msgf("action.StoreFilterActions: store %d", action.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
action.ID = retID
|
|
||||||
|
|
||||||
r.log.Debug().Msgf("action.StoreFilterActions: store '%v' type: '%v' on filter: %v", action.Name, action.Type, filterID)
|
r.log.Debug().Msgf("action.StoreFilterActions: store '%v' type: '%v' on filter: %v", action.Name, action.Type, filterID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -271,6 +271,7 @@ CREATE TABLE release_action_status
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
status TEXT,
|
status TEXT,
|
||||||
action TEXT NOT NULL,
|
action TEXT NOT NULL,
|
||||||
|
action_id INTEGER,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
client TEXT,
|
client TEXT,
|
||||||
filter TEXT,
|
filter TEXT,
|
||||||
|
@ -280,6 +281,7 @@ CREATE TABLE release_action_status
|
||||||
raw TEXT,
|
raw TEXT,
|
||||||
log TEXT,
|
log TEXT,
|
||||||
release_id INTEGER NOT NULL,
|
release_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (action_id) REFERENCES "action"(id),
|
||||||
FOREIGN KEY (release_id) REFERENCES "release"(id) ON DELETE CASCADE,
|
FOREIGN KEY (release_id) REFERENCES "release"(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (filter_id) REFERENCES "filter"(id) ON DELETE SET NULL
|
FOREIGN KEY (filter_id) REFERENCES "filter"(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
@ -691,4 +693,10 @@ ADD COLUMN topic text;`,
|
||||||
|
|
||||||
ALTER TABLE filter
|
ALTER TABLE filter
|
||||||
ADD COLUMN use_regex_description BOOLEAN DEFAULT FALSE;`,
|
ADD COLUMN use_regex_description BOOLEAN DEFAULT FALSE;`,
|
||||||
|
`ALTER TABLE release_action_status
|
||||||
|
ADD action_id INTEGER;
|
||||||
|
|
||||||
|
ALTER TABLE release_action_status
|
||||||
|
ADD CONSTRAINT release_action_status_action_id_fk
|
||||||
|
FOREIGN KEY (action_id) REFERENCES action;`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,8 +79,8 @@ func (repo *ReleaseRepo) StoreReleaseActionStatus(ctx context.Context, status *d
|
||||||
} else {
|
} else {
|
||||||
queryBuilder := repo.db.squirrel.
|
queryBuilder := repo.db.squirrel.
|
||||||
Insert("release_action_status").
|
Insert("release_action_status").
|
||||||
Columns("status", "action", "type", "client", "filter", "filter_id", "rejections", "timestamp", "release_id").
|
Columns("status", "action", "action_id", "type", "client", "filter", "filter_id", "rejections", "timestamp", "release_id").
|
||||||
Values(status.Status, status.Action, status.Type, status.Client, status.Filter, status.FilterID, pq.Array(status.Rejections), status.Timestamp.Format(time.RFC3339), status.ReleaseID).
|
Values(status.Status, status.Action, status.ActionID, status.Type, status.Client, status.Filter, status.FilterID, pq.Array(status.Rejections), status.Timestamp.Format(time.RFC3339), status.ReleaseID).
|
||||||
Suffix("RETURNING id").RunWith(repo.db.handler)
|
Suffix("RETURNING id").RunWith(repo.db.handler)
|
||||||
|
|
||||||
// return values
|
// return values
|
||||||
|
@ -207,7 +207,7 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
|
||||||
|
|
||||||
queryBuilder := repo.db.squirrel.
|
queryBuilder := repo.db.squirrel.
|
||||||
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.timestamp",
|
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.timestamp",
|
||||||
"ras.id", "ras.status", "ras.action", "ras.type", "ras.client", "ras.filter", "ras.rejections", "ras.timestamp").
|
"ras.id", "ras.status", "ras.action", "ras.action_id", "ras.type", "ras.client", "ras.filter", "ras.filter_id", "ras.release_id", "ras.rejections", "ras.timestamp").
|
||||||
Column(sq.Alias(countQuery, "page_total")).
|
Column(sq.Alias(countQuery, "page_total")).
|
||||||
From("release r").
|
From("release r").
|
||||||
OrderBy("r.id DESC").
|
OrderBy("r.id DESC").
|
||||||
|
@ -242,22 +242,25 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain
|
||||||
|
|
||||||
var rlsindexer, rlsfilter, infoUrl, downloadUrl sql.NullString
|
var rlsindexer, rlsfilter, infoUrl, downloadUrl sql.NullString
|
||||||
|
|
||||||
var rasId sql.NullInt64
|
var rasId, rasFilterId, rasReleaseId, rasActionId sql.NullInt64
|
||||||
var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString
|
var rasStatus, rasAction, rasType, rasClient, rasFilter sql.NullString
|
||||||
var rasRejections []sql.NullString
|
var rasRejections []sql.NullString
|
||||||
var rasTimestamp sql.NullTime
|
var rasTimestamp sql.NullTime
|
||||||
|
|
||||||
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasType, &rasClient, &rasFilter, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
|
if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &rlsindexer, &rlsfilter, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp, &rasId, &rasStatus, &rasAction, &rasActionId, &rasType, &rasClient, &rasFilter, &rasFilterId, &rasReleaseId, pq.Array(&rasRejections), &rasTimestamp, &countItems); err != nil {
|
||||||
return res, 0, 0, errors.Wrap(err, "error scanning row")
|
return res, 0, 0, errors.Wrap(err, "error scanning row")
|
||||||
}
|
}
|
||||||
|
|
||||||
ras.ID = rasId.Int64
|
ras.ID = rasId.Int64
|
||||||
ras.Status = domain.ReleasePushStatus(rasStatus.String)
|
ras.Status = domain.ReleasePushStatus(rasStatus.String)
|
||||||
ras.Action = rasAction.String
|
ras.Action = rasAction.String
|
||||||
|
ras.ActionID = rasActionId.Int64
|
||||||
ras.Type = domain.ActionType(rasType.String)
|
ras.Type = domain.ActionType(rasType.String)
|
||||||
ras.Client = rasClient.String
|
ras.Client = rasClient.String
|
||||||
ras.Filter = rasFilter.String
|
ras.Filter = rasFilter.String
|
||||||
|
ras.FilterID = rasFilterId.Int64
|
||||||
ras.Timestamp = rasTimestamp.Time
|
ras.Timestamp = rasTimestamp.Time
|
||||||
|
ras.ReleaseID = rasReleaseId.Int64
|
||||||
ras.Rejections = []string{}
|
ras.Rejections = []string{}
|
||||||
|
|
||||||
for _, rejection := range rasRejections {
|
for _, rejection := range rasRejections {
|
||||||
|
@ -351,7 +354,7 @@ func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error
|
||||||
func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]domain.ReleaseActionStatus, error) {
|
func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]domain.ReleaseActionStatus, error) {
|
||||||
|
|
||||||
queryBuilder := repo.db.squirrel.
|
queryBuilder := repo.db.squirrel.
|
||||||
Select("id", "status", "action", "type", "client", "filter", "rejections", "timestamp").
|
Select("id", "status", "action", "action_id", "type", "client", "filter", "release_id", "rejections", "timestamp").
|
||||||
From("release_action_status").
|
From("release_action_status").
|
||||||
Where(sq.Eq{"release_id": releaseID})
|
Where(sq.Eq{"release_id": releaseID})
|
||||||
|
|
||||||
|
@ -378,11 +381,13 @@ func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, release
|
||||||
var rls domain.ReleaseActionStatus
|
var rls domain.ReleaseActionStatus
|
||||||
|
|
||||||
var client, filter sql.NullString
|
var client, filter sql.NullString
|
||||||
|
var actionId sql.NullInt64
|
||||||
|
|
||||||
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, &client, &filter, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
|
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &actionId, &rls.Type, &client, &filter, &rls.ReleaseID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
|
||||||
return res, errors.Wrap(err, "error scanning row")
|
return res, errors.Wrap(err, "error scanning row")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rls.ActionID = actionId.Int64
|
||||||
rls.Client = client.String
|
rls.Client = client.String
|
||||||
rls.Filter = filter.String
|
rls.Filter = filter.String
|
||||||
|
|
||||||
|
@ -392,9 +397,96 @@ func (repo *ReleaseRepo) GetActionStatusByReleaseID(ctx context.Context, release
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repo *ReleaseRepo) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) {
|
||||||
|
queryBuilder := repo.db.squirrel.
|
||||||
|
Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.filter_id", "r.protocol", "r.info_url", "r.download_url", "r.title", "r.torrent_name", "r.size", "r.timestamp").
|
||||||
|
From("release r").
|
||||||
|
OrderBy("r.id DESC").
|
||||||
|
Where(sq.Eq{"r.id": req.Id})
|
||||||
|
|
||||||
|
query, args, err := queryBuilder.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error building query")
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args)
|
||||||
|
|
||||||
|
row := repo.db.handler.QueryRowContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error executing query")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error rows find release")
|
||||||
|
}
|
||||||
|
|
||||||
|
var rls domain.Release
|
||||||
|
|
||||||
|
var indexerName, filterName, infoUrl, downloadUrl sql.NullString
|
||||||
|
var filterId sql.NullInt64
|
||||||
|
|
||||||
|
if err := row.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &indexerName, &filterName, &filterId, &rls.Protocol, &infoUrl, &downloadUrl, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "error scanning row")
|
||||||
|
}
|
||||||
|
|
||||||
|
rls.Indexer = indexerName.String
|
||||||
|
rls.FilterName = filterName.String
|
||||||
|
rls.FilterID = int(filterId.Int64)
|
||||||
|
rls.ActionStatus = make([]domain.ReleaseActionStatus, 0)
|
||||||
|
rls.InfoURL = infoUrl.String
|
||||||
|
rls.TorrentURL = downloadUrl.String
|
||||||
|
|
||||||
|
return &rls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *ReleaseRepo) GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error) {
|
||||||
|
queryBuilder := repo.db.squirrel.
|
||||||
|
Select("id", "status", "action", "action_id", "type", "client", "filter", "filter_id", "release_id", "rejections", "timestamp").
|
||||||
|
From("release_action_status").
|
||||||
|
Where(sq.Eq{"id": req.Id})
|
||||||
|
|
||||||
|
query, args, err := queryBuilder.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error building query")
|
||||||
|
}
|
||||||
|
|
||||||
|
row := repo.db.handler.QueryRowContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error executing query")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
repo.log.Error().Stack().Err(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rls domain.ReleaseActionStatus
|
||||||
|
|
||||||
|
var client, filter sql.NullString
|
||||||
|
var actionId, filterId sql.NullInt64
|
||||||
|
|
||||||
|
if err := row.Scan(&rls.ID, &rls.Status, &rls.Action, &actionId, &rls.Type, &client, &filter, &filterId, &rls.ReleaseID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Wrap(err, "error scanning row")
|
||||||
|
}
|
||||||
|
|
||||||
|
rls.ActionID = actionId.Int64
|
||||||
|
rls.Client = client.String
|
||||||
|
rls.Filter = filter.String
|
||||||
|
rls.FilterID = filterId.Int64
|
||||||
|
|
||||||
|
return &rls, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, releaseID int64) ([]domain.ReleaseActionStatus, error) {
|
func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, releaseID int64) ([]domain.ReleaseActionStatus, error) {
|
||||||
queryBuilder := repo.db.squirrel.
|
queryBuilder := repo.db.squirrel.
|
||||||
Select("id", "status", "action", "type", "client", "filter", "filter_id", "rejections", "timestamp").
|
Select("id", "status", "action", "action_id", "type", "client", "filter", "filter_id", "release_id", "rejections", "timestamp").
|
||||||
From("release_action_status").
|
From("release_action_status").
|
||||||
Where(sq.Eq{"release_id": releaseID})
|
Where(sq.Eq{"release_id": releaseID})
|
||||||
|
|
||||||
|
@ -420,12 +512,13 @@ func (repo *ReleaseRepo) attachActionStatus(ctx context.Context, tx *Tx, release
|
||||||
var rls domain.ReleaseActionStatus
|
var rls domain.ReleaseActionStatus
|
||||||
|
|
||||||
var client, filter sql.NullString
|
var client, filter sql.NullString
|
||||||
var filterID sql.NullInt64
|
var actionId, filterID sql.NullInt64
|
||||||
|
|
||||||
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &rls.Type, &client, &filter, &filterID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
|
if err := rows.Scan(&rls.ID, &rls.Status, &rls.Action, &actionId, &rls.Type, &client, &filter, &filterID, &rls.ReleaseID, pq.Array(&rls.Rejections), &rls.Timestamp); err != nil {
|
||||||
return res, errors.Wrap(err, "error scanning row")
|
return res, errors.Wrap(err, "error scanning row")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rls.ActionID = actionId.Int64
|
||||||
rls.Client = client.String
|
rls.Client = client.String
|
||||||
rls.Filter = filter.String
|
rls.Filter = filter.String
|
||||||
rls.FilterID = filterID.Int64
|
rls.FilterID = filterID.Int64
|
||||||
|
|
|
@ -254,6 +254,9 @@ CREATE TABLE release_action_status
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
status TEXT,
|
status TEXT,
|
||||||
action TEXT NOT NULL,
|
action TEXT NOT NULL,
|
||||||
|
action_id INTEGER
|
||||||
|
CONSTRAINT release_action_status_action_id_fk
|
||||||
|
REFERENCES action,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
client TEXT,
|
client TEXT,
|
||||||
filter TEXT,
|
filter TEXT,
|
||||||
|
@ -1084,4 +1087,58 @@ ADD COLUMN topic text;`,
|
||||||
|
|
||||||
ALTER TABLE filter
|
ALTER TABLE filter
|
||||||
ADD COLUMN use_regex_description BOOLEAN DEFAULT FALSE;`,
|
ADD COLUMN use_regex_description BOOLEAN DEFAULT FALSE;`,
|
||||||
|
`create table release_action_status_dg_tmp
|
||||||
|
(
|
||||||
|
id INTEGER
|
||||||
|
primary key,
|
||||||
|
status TEXT,
|
||||||
|
action TEXT not null,
|
||||||
|
action_id INTEGER
|
||||||
|
constraint release_action_status_action_id_fk
|
||||||
|
references action,
|
||||||
|
type TEXT not null,
|
||||||
|
rejections TEXT default '{}' not null,
|
||||||
|
timestamp TIMESTAMP default CURRENT_TIMESTAMP,
|
||||||
|
raw TEXT,
|
||||||
|
log TEXT,
|
||||||
|
release_id INTEGER not null
|
||||||
|
constraint release_action_status_release_id_fkey
|
||||||
|
references "release"
|
||||||
|
on delete cascade,
|
||||||
|
client TEXT,
|
||||||
|
filter TEXT,
|
||||||
|
filter_id INTEGER
|
||||||
|
constraint release_action_status_filter_id_fk
|
||||||
|
references filter
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into release_action_status_dg_tmp(id, status, action, type, rejections, timestamp, raw, log, release_id, client,
|
||||||
|
filter, filter_id)
|
||||||
|
select id,
|
||||||
|
status,
|
||||||
|
action,
|
||||||
|
type,
|
||||||
|
rejections,
|
||||||
|
timestamp,
|
||||||
|
raw,
|
||||||
|
log,
|
||||||
|
release_id,
|
||||||
|
client,
|
||||||
|
filter,
|
||||||
|
filter_id
|
||||||
|
from release_action_status;
|
||||||
|
|
||||||
|
drop table release_action_status;
|
||||||
|
|
||||||
|
alter table release_action_status_dg_tmp
|
||||||
|
rename to release_action_status;
|
||||||
|
|
||||||
|
create index release_action_status_filter_id_index
|
||||||
|
on release_action_status (filter_id);
|
||||||
|
|
||||||
|
create index release_action_status_release_id_index
|
||||||
|
on release_action_status (release_id);
|
||||||
|
|
||||||
|
create index release_action_status_status_index
|
||||||
|
on release_action_status (status);`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,11 @@ import (
|
||||||
type ActionRepo interface {
|
type ActionRepo interface {
|
||||||
Store(ctx context.Context, action Action) (*Action, error)
|
Store(ctx context.Context, action Action) (*Action, error)
|
||||||
StoreFilterActions(ctx context.Context, actions []*Action, filterID int64) ([]*Action, error)
|
StoreFilterActions(ctx context.Context, actions []*Action, filterID int64) ([]*Action, error)
|
||||||
DeleteByFilterID(ctx context.Context, filterID int) error
|
|
||||||
FindByFilterID(ctx context.Context, filterID int) ([]*Action, error)
|
FindByFilterID(ctx context.Context, filterID int) ([]*Action, error)
|
||||||
List(ctx context.Context) ([]Action, error)
|
List(ctx context.Context) ([]Action, error)
|
||||||
Delete(actionID int) error
|
Get(ctx context.Context, req *GetActionRequest) (*Action, error)
|
||||||
|
Delete(ctx context.Context, req *DeleteActionRequest) error
|
||||||
|
DeleteByFilterID(ctx context.Context, filterID int) error
|
||||||
ToggleEnabled(actionID int) error
|
ToggleEnabled(actionID int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,3 +126,11 @@ const (
|
||||||
ActionContentLayoutSubfolderNone ActionContentLayout = "SUBFOLDER_NONE"
|
ActionContentLayoutSubfolderNone ActionContentLayout = "SUBFOLDER_NONE"
|
||||||
ActionContentLayoutSubfolderCreate ActionContentLayout = "SUBFOLDER_CREATE"
|
ActionContentLayoutSubfolderCreate ActionContentLayout = "SUBFOLDER_CREATE"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GetActionRequest struct {
|
||||||
|
Id int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteActionRequest struct {
|
||||||
|
ActionId int
|
||||||
|
}
|
||||||
|
|
|
@ -30,12 +30,14 @@ type ReleaseRepo interface {
|
||||||
Store(ctx context.Context, release *Release) (*Release, error)
|
Store(ctx context.Context, release *Release) (*Release, error)
|
||||||
Find(ctx context.Context, params ReleaseQueryParams) (res []*Release, nextCursor int64, count int64, err error)
|
Find(ctx context.Context, params ReleaseQueryParams) (res []*Release, nextCursor int64, count int64, err error)
|
||||||
FindRecent(ctx context.Context) ([]*Release, error)
|
FindRecent(ctx context.Context) ([]*Release, error)
|
||||||
|
Get(ctx context.Context, req *GetReleaseRequest) (*Release, error)
|
||||||
GetIndexerOptions(ctx context.Context) ([]string, error)
|
GetIndexerOptions(ctx context.Context) ([]string, error)
|
||||||
GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]ReleaseActionStatus, error)
|
|
||||||
Stats(ctx context.Context) (*ReleaseStats, error)
|
Stats(ctx context.Context) (*ReleaseStats, error)
|
||||||
StoreReleaseActionStatus(ctx context.Context, status *ReleaseActionStatus) error
|
|
||||||
Delete(ctx context.Context) error
|
Delete(ctx context.Context) error
|
||||||
CanDownloadShow(ctx context.Context, title string, season int, episode int) (bool, error)
|
CanDownloadShow(ctx context.Context, title string, season int, episode int) (bool, error)
|
||||||
|
|
||||||
|
GetActionStatus(ctx context.Context, req *GetReleaseActionStatusRequest) (*ReleaseActionStatus, error)
|
||||||
|
StoreReleaseActionStatus(ctx context.Context, status *ReleaseActionStatus) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Release struct {
|
type Release struct {
|
||||||
|
@ -100,13 +102,14 @@ type ReleaseActionStatus struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Status ReleasePushStatus `json:"status"`
|
Status ReleasePushStatus `json:"status"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
|
ActionID int64 `json:"action_id"`
|
||||||
Type ActionType `json:"type"`
|
Type ActionType `json:"type"`
|
||||||
Client string `json:"client"`
|
Client string `json:"client"`
|
||||||
Filter string `json:"filter"`
|
Filter string `json:"filter"`
|
||||||
FilterID int64 `json:"-"`
|
FilterID int64 `json:"filter_id"`
|
||||||
Rejections []string `json:"rejections"`
|
Rejections []string `json:"rejections"`
|
||||||
|
ReleaseID int64 `json:"release_id"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
ReleaseID int64 `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReleaseActionStatus(action *Action, release *Release) *ReleaseActionStatus {
|
func NewReleaseActionStatus(action *Action, release *Release) *ReleaseActionStatus {
|
||||||
|
@ -114,9 +117,10 @@ func NewReleaseActionStatus(action *Action, release *Release) *ReleaseActionStat
|
||||||
ID: 0,
|
ID: 0,
|
||||||
Status: ReleasePushStatusPending,
|
Status: ReleasePushStatusPending,
|
||||||
Action: action.Name,
|
Action: action.Name,
|
||||||
|
ActionID: int64(action.ID),
|
||||||
Type: action.Type,
|
Type: action.Type,
|
||||||
Filter: release.Filter.Name,
|
Filter: release.FilterName,
|
||||||
FilterID: int64(release.Filter.ID),
|
FilterID: int64(release.FilterID),
|
||||||
Rejections: []string{},
|
Rejections: []string{},
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
ReleaseID: release.ID,
|
ReleaseID: release.ID,
|
||||||
|
@ -153,6 +157,8 @@ const (
|
||||||
|
|
||||||
func (r ReleasePushStatus) String() string {
|
func (r ReleasePushStatus) String() string {
|
||||||
switch r {
|
switch r {
|
||||||
|
case ReleasePushStatusPending:
|
||||||
|
return "Pending"
|
||||||
case ReleasePushStatusApproved:
|
case ReleasePushStatusApproved:
|
||||||
return "Approved"
|
return "Approved"
|
||||||
case ReleasePushStatusRejected:
|
case ReleasePushStatusRejected:
|
||||||
|
@ -227,6 +233,20 @@ type ReleaseQueryParams struct {
|
||||||
Search string
|
Search string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReleaseActionRetryReq struct {
|
||||||
|
ReleaseId int
|
||||||
|
ActionStatusId int
|
||||||
|
ActionId int
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetReleaseRequest struct {
|
||||||
|
Id int
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetReleaseActionStatusRequest struct {
|
||||||
|
Id int
|
||||||
|
}
|
||||||
|
|
||||||
func NewRelease(indexer string) *Release {
|
func NewRelease(indexer string) *Release {
|
||||||
r := &Release{
|
r := &Release{
|
||||||
Indexer: indexer,
|
Indexer: indexer,
|
||||||
|
|
|
@ -11,13 +11,14 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type actionService interface {
|
type actionService interface {
|
||||||
List(ctx context.Context) ([]domain.Action, error)
|
List(ctx context.Context) ([]domain.Action, error)
|
||||||
Store(ctx context.Context, action domain.Action) (*domain.Action, error)
|
Store(ctx context.Context, action domain.Action) (*domain.Action, error)
|
||||||
Delete(actionID int) error
|
Delete(ctx context.Context, req *domain.DeleteActionRequest) error
|
||||||
ToggleEnabled(actionID int) error
|
ToggleEnabled(actionID int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,15 +37,19 @@ func newActionHandler(encoder encoder, service actionService) *actionHandler {
|
||||||
func (h actionHandler) Routes(r chi.Router) {
|
func (h actionHandler) Routes(r chi.Router) {
|
||||||
r.Get("/", h.getActions)
|
r.Get("/", h.getActions)
|
||||||
r.Post("/", h.storeAction)
|
r.Post("/", h.storeAction)
|
||||||
r.Delete("/{id}", h.deleteAction)
|
|
||||||
r.Put("/{id}", h.updateAction)
|
r.Route("/{id}", func(r chi.Router) {
|
||||||
r.Patch("/{id}/toggleEnabled", h.toggleActionEnabled)
|
r.Delete("/", h.deleteAction)
|
||||||
|
r.Put("/", h.updateAction)
|
||||||
|
r.Patch("/toggleEnabled", h.toggleActionEnabled)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h actionHandler) getActions(w http.ResponseWriter, r *http.Request) {
|
func (h actionHandler) getActions(w http.ResponseWriter, r *http.Request) {
|
||||||
actions, err := h.service.List(r.Context())
|
actions, err := h.service.List(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// encode error
|
h.encoder.Error(w, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.encoder.StatusResponse(w, http.StatusOK, actions)
|
h.encoder.StatusResponse(w, http.StatusOK, actions)
|
||||||
|
@ -57,13 +62,14 @@ func (h actionHandler) storeAction(w http.ResponseWriter, r *http.Request) {
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
// encode error
|
h.encoder.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
action, err := h.service.Store(ctx, data)
|
action, err := h.service.Store(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// encode error
|
h.encoder.Error(w, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.encoder.StatusResponse(w, http.StatusCreated, action)
|
h.encoder.StatusResponse(w, http.StatusCreated, action)
|
||||||
|
@ -76,13 +82,14 @@ func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) {
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
// encode error
|
h.encoder.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
action, err := h.service.Store(ctx, data)
|
action, err := h.service.Store(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// encode error
|
h.encoder.Error(w, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.encoder.StatusResponse(w, http.StatusCreated, action)
|
h.encoder.StatusResponse(w, http.StatusCreated, action)
|
||||||
|
@ -91,11 +98,13 @@ func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) {
|
func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) {
|
||||||
actionID, err := parseInt(chi.URLParam(r, "id"))
|
actionID, err := parseInt(chi.URLParam(r, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.encoder.StatusResponse(w, http.StatusBadRequest, errors.New("bad param id"))
|
h.encoder.StatusError(w, http.StatusBadRequest, errors.New("bad param id"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.service.Delete(actionID); err != nil {
|
if err := h.service.Delete(r.Context(), &domain.DeleteActionRequest{ActionId: actionID}); err != nil {
|
||||||
// encode error
|
h.encoder.Error(w, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.encoder.StatusResponse(w, http.StatusNoContent, nil)
|
h.encoder.StatusResponse(w, http.StatusNoContent, nil)
|
||||||
|
@ -104,11 +113,13 @@ func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h actionHandler) toggleActionEnabled(w http.ResponseWriter, r *http.Request) {
|
func (h actionHandler) toggleActionEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
actionID, err := parseInt(chi.URLParam(r, "id"))
|
actionID, err := parseInt(chi.URLParam(r, "id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.encoder.StatusResponse(w, http.StatusBadRequest, errors.New("bad param id"))
|
h.encoder.StatusError(w, http.StatusBadRequest, errors.New("bad param id"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.service.ToggleEnabled(actionID); err != nil {
|
if err := h.service.ToggleEnabled(actionID); err != nil {
|
||||||
// encode error
|
h.encoder.Error(w, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.encoder.StatusResponse(w, http.StatusCreated, nil)
|
h.encoder.StatusResponse(w, http.StatusCreated, nil)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/autobrr/autobrr/internal/domain"
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ type releaseService interface {
|
||||||
GetIndexerOptions(ctx context.Context) ([]string, error)
|
GetIndexerOptions(ctx context.Context) ([]string, error)
|
||||||
Stats(ctx context.Context) (*domain.ReleaseStats, error)
|
Stats(ctx context.Context) (*domain.ReleaseStats, error)
|
||||||
Delete(ctx context.Context) error
|
Delete(ctx context.Context) error
|
||||||
|
Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type releaseHandler struct {
|
type releaseHandler struct {
|
||||||
|
@ -39,6 +41,10 @@ func (h releaseHandler) Routes(r chi.Router) {
|
||||||
r.Get("/stats", h.getStats)
|
r.Get("/stats", h.getStats)
|
||||||
r.Get("/indexers", h.getIndexerOptions)
|
r.Get("/indexers", h.getIndexerOptions)
|
||||||
r.Delete("/all", h.deleteReleases)
|
r.Delete("/all", h.deleteReleases)
|
||||||
|
|
||||||
|
r.Route("/{releaseId}", func(r chi.Router) {
|
||||||
|
r.Post("/actions/{actionStatusId}/retry", h.retryAction)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
|
func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -186,3 +192,46 @@ func (h releaseHandler) deleteReleases(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
h.encoder.NoContent(w)
|
h.encoder.NoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h releaseHandler) retryAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
req *domain.ReleaseActionRetryReq
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
releaseIdParam := chi.URLParam(r, "releaseId")
|
||||||
|
if releaseIdParam == "" {
|
||||||
|
h.encoder.StatusError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseId, err := strconv.Atoi(releaseIdParam)
|
||||||
|
if err != nil {
|
||||||
|
h.encoder.StatusError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionStatusIdParam := chi.URLParam(r, "actionStatusId")
|
||||||
|
if actionStatusIdParam == "" {
|
||||||
|
h.encoder.StatusError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionStatusId, err := strconv.Atoi(actionStatusIdParam)
|
||||||
|
if err != nil {
|
||||||
|
h.encoder.StatusError(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req = &domain.ReleaseActionRetryReq{
|
||||||
|
ReleaseId: releaseId,
|
||||||
|
ActionStatusId: actionStatusId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.Retry(r.Context(), req); err != nil {
|
||||||
|
h.encoder.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.NoContent(w)
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import (
|
||||||
type Service interface {
|
type Service interface {
|
||||||
Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error)
|
Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error)
|
||||||
FindRecent(ctx context.Context) ([]*domain.Release, error)
|
FindRecent(ctx context.Context) ([]*domain.Release, error)
|
||||||
|
Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error)
|
||||||
|
GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error)
|
||||||
GetIndexerOptions(ctx context.Context) ([]string, error)
|
GetIndexerOptions(ctx context.Context) ([]string, error)
|
||||||
Stats(ctx context.Context) (*domain.ReleaseStats, error)
|
Stats(ctx context.Context) (*domain.ReleaseStats, error)
|
||||||
Store(ctx context.Context, release *domain.Release) error
|
Store(ctx context.Context, release *domain.Release) error
|
||||||
|
@ -27,6 +29,7 @@ type Service interface {
|
||||||
|
|
||||||
Process(release *domain.Release)
|
Process(release *domain.Release)
|
||||||
ProcessMultiple(releases []*domain.Release)
|
ProcessMultiple(releases []*domain.Release)
|
||||||
|
Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type actionClientTypeKey struct {
|
type actionClientTypeKey struct {
|
||||||
|
@ -59,6 +62,14 @@ func (s *service) FindRecent(ctx context.Context) (res []*domain.Release, err er
|
||||||
return s.repo.FindRecent(ctx)
|
return s.repo.FindRecent(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) Get(ctx context.Context, req *domain.GetReleaseRequest) (*domain.Release, error) {
|
||||||
|
return s.repo.Get(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetActionStatus(ctx context.Context, req *domain.GetReleaseActionStatusRequest) (*domain.ReleaseActionStatus, error) {
|
||||||
|
return s.repo.GetActionStatus(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) GetIndexerOptions(ctx context.Context) ([]string, error) {
|
func (s *service) GetIndexerOptions(ctx context.Context) ([]string, error) {
|
||||||
return s.repo.GetIndexerOptions(ctx)
|
return s.repo.GetIndexerOptions(ctx)
|
||||||
}
|
}
|
||||||
|
@ -138,13 +149,13 @@ func (s *service) Process(release *domain.Release) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !match {
|
if !match {
|
||||||
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s, no match. rejections: %s", release.Indexer, release.Filter.Name, release.TorrentName, release.RejectionsString())
|
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s, no match. rejections: %s", release.Indexer, release.FilterName, release.TorrentName, release.RejectionsString())
|
||||||
|
|
||||||
l.Debug().Msgf("release rejected: %s", release.RejectionsString())
|
l.Debug().Msgf("release rejected: %s", release.RejectionsString())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Info().Msgf("Matched '%s' (%s) for %s", release.TorrentName, release.Filter.Name, release.Indexer)
|
l.Info().Msgf("Matched '%s' (%s) for %s", release.TorrentName, release.FilterName, release.Indexer)
|
||||||
|
|
||||||
// save release here to only save those with rejections from actions instead of all releases
|
// save release here to only save those with rejections from actions instead of all releases
|
||||||
if release.ID == 0 {
|
if release.ID == 0 {
|
||||||
|
@ -158,7 +169,7 @@ func (s *service) Process(release *domain.Release) {
|
||||||
// sleep for the delay period specified in the filter before running actions
|
// sleep for the delay period specified in the filter before running actions
|
||||||
delay := release.Filter.Delay
|
delay := release.Filter.Delay
|
||||||
if delay > 0 {
|
if delay > 0 {
|
||||||
l.Debug().Msgf("Delaying processing of '%s' (%s) for %s by %d seconds as specified in the filter", release.TorrentName, release.Filter.Name, release.Indexer, delay)
|
l.Debug().Msgf("Delaying processing of '%s' (%s) for %s by %d seconds as specified in the filter", release.TorrentName, release.FilterName, release.Indexer, delay)
|
||||||
time.Sleep(time.Duration(delay) * time.Second)
|
time.Sleep(time.Duration(delay) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,30 +181,30 @@ func (s *service) Process(release *domain.Release) {
|
||||||
|
|
||||||
// only run enabled actions
|
// only run enabled actions
|
||||||
if !act.Enabled {
|
if !act.Enabled {
|
||||||
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action '%s' not enabled, skip", release.Indexer, release.Filter.Name, release.TorrentName, act.Name)
|
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action '%s' not enabled, skip", release.Indexer, release.FilterName, release.TorrentName, act.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s , run action: %s", release.Indexer, release.Filter.Name, release.TorrentName, act.Name)
|
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s , run action: %s", release.Indexer, release.FilterName, release.TorrentName, act.Name)
|
||||||
|
|
||||||
// keep track of actiom clients to avoid sending the same thing all over again
|
// keep track of actiom clients to avoid sending the same thing all over again
|
||||||
_, tried := triedActionClients[actionClientTypeKey{Type: act.Type, ClientID: act.ClientID}]
|
_, tried := triedActionClients[actionClientTypeKey{Type: act.Type, ClientID: act.ClientID}]
|
||||||
if tried {
|
if tried {
|
||||||
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action client already tried, skip", release.Indexer, release.Filter.Name, release.TorrentName)
|
l.Trace().Msgf("release.Process: indexer: %s, filter: %s release: %s action client already tried, skip", release.Indexer, release.FilterName, release.TorrentName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// run action
|
// run action
|
||||||
status, err := s.runAction(ctx, act, release)
|
status, err := s.runAction(ctx, act, release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Error().Stack().Err(err).Msgf("release.Process: error running actions for filter: %s", release.Filter.Name)
|
l.Error().Stack().Err(err).Msgf("release.Process: error running actions for filter: %s", release.FilterName)
|
||||||
//continue
|
//continue
|
||||||
}
|
}
|
||||||
|
|
||||||
rejections = status.Rejections
|
rejections = status.Rejections
|
||||||
|
|
||||||
if err := s.StoreReleaseActionStatus(ctx, status); err != nil {
|
if err := s.StoreReleaseActionStatus(ctx, status); err != nil {
|
||||||
s.log.Error().Err(err).Msgf("release.Process: error storing action status for filter: %s", release.Filter.Name)
|
s.log.Error().Err(err).Msgf("release.Process: error storing action status for filter: %s", release.FilterName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rejections) > 0 {
|
if len(rejections) > 0 {
|
||||||
|
@ -237,12 +248,12 @@ func (s *service) runAction(ctx context.Context, action *domain.Action, release
|
||||||
status := domain.NewReleaseActionStatus(action, release)
|
status := domain.NewReleaseActionStatus(action, release)
|
||||||
|
|
||||||
if err := s.StoreReleaseActionStatus(ctx, status); err != nil {
|
if err := s.StoreReleaseActionStatus(ctx, status); err != nil {
|
||||||
s.log.Error().Err(err).Msgf("release.runAction: error storing action for filter: %s", release.Filter.Name)
|
s.log.Error().Err(err).Msgf("release.runAction: error storing action for filter: %s", release.FilterName)
|
||||||
}
|
}
|
||||||
|
|
||||||
rejections, err := s.actionSvc.RunAction(ctx, action, release)
|
rejections, err := s.actionSvc.RunAction(ctx, action, release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error().Stack().Err(err).Msgf("release.runAction: error running actions for filter: %s", release.Filter.Name)
|
s.log.Error().Stack().Err(err).Msgf("release.runAction: error running actions for filter: %s", release.FilterName)
|
||||||
|
|
||||||
status.Status = domain.ReleasePushStatusErr
|
status.Status = domain.ReleasePushStatusErr
|
||||||
status.Rejections = []string{err.Error()}
|
status.Rejections = []string{err.Error()}
|
||||||
|
@ -261,3 +272,54 @@ func (s *service) runAction(ctx context.Context, action *domain.Action, release
|
||||||
|
|
||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) retryAction(ctx context.Context, action *domain.Action, release *domain.Release) error {
|
||||||
|
actionStatus, err := s.runAction(ctx, action, release)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error().Err(err).Msgf("release.retryAction: error running actions for filter: %s", release.FilterName)
|
||||||
|
|
||||||
|
if err := s.StoreReleaseActionStatus(ctx, actionStatus); err != nil {
|
||||||
|
s.log.Error().Err(err).Msgf("release.retryAction: error storing filterAction status for filter: %s", release.FilterName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.StoreReleaseActionStatus(ctx, actionStatus); err != nil {
|
||||||
|
s.log.Error().Err(err).Msgf("release.retryAction: error storing filterAction status for filter: %s", release.FilterName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error {
|
||||||
|
// get release
|
||||||
|
release, err := s.Get(ctx, &domain.GetReleaseRequest{Id: req.ReleaseId})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get release filter action status
|
||||||
|
status, err := s.GetActionStatus(ctx, &domain.GetReleaseActionStatusRequest{Id: req.ActionStatusId})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get filter action with action id from status
|
||||||
|
filterAction, err := s.actionSvc.Get(ctx, &domain.GetActionRequest{Id: int(status.ActionID)})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// run filterAction
|
||||||
|
if err := s.retryAction(ctx, filterAction, release); err != nil {
|
||||||
|
s.log.Error().Err(err).Msgf("release.Retry: error re-running action: %s", filterAction.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Info().Msgf("successfully replayed action %s for release %s", filterAction.Name, release.TorrentName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -193,7 +193,8 @@ export const APIClient = {
|
||||||
},
|
},
|
||||||
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
|
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
|
||||||
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
||||||
delete: () => appClient.Delete("api/release/all")
|
delete: () => appClient.Delete("api/release/all"),
|
||||||
|
replayAction: (releaseId: number, actionId: number) => appClient.Post(`api/release/${releaseId}/actions/${actionId}/retry`)
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
check: () => appClient.Get("api/updates/check"),
|
check: () => appClient.Get("api/updates/check"),
|
||||||
|
|
|
@ -5,11 +5,17 @@
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { formatDistanceToNowStrict } from "date-fns";
|
import { formatDistanceToNowStrict } from "date-fns";
|
||||||
import { CheckIcon } from "@heroicons/react/24/solid";
|
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||||
import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
|
import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { classNames, simplifyDate } from "@utils";
|
import { classNames, simplifyDate } from "@utils";
|
||||||
import { Tooltip } from "../tooltips/Tooltip";
|
import { Tooltip } from "../tooltips/Tooltip";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { APIClient } from "@api/APIClient";
|
||||||
|
import { filterKeys } from "@screens/filters/list";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import Toast from "@components/notifications/Toast";
|
||||||
|
import { RingResizeSpinner } from "@components/Icons";
|
||||||
|
|
||||||
interface CellProps {
|
interface CellProps {
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -57,6 +63,46 @@ export const TitleCell = ({ value }: CellProps) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface RetryActionButtonProps {
|
||||||
|
status: ReleaseActionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RetryAction {
|
||||||
|
releaseId: number;
|
||||||
|
actionId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RetryActionButton = ({ status }: RetryActionButtonProps) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (vars: RetryAction) => APIClient.release.replayAction(vars.releaseId, vars.actionId),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||||
|
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
|
||||||
|
|
||||||
|
toast.custom((t) => (
|
||||||
|
<Toast type="success" body={`${status?.action} replayed`} t={t} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const replayAction = () => {
|
||||||
|
console.log("replay action");
|
||||||
|
mutation.mutate({ releaseId: status.release_id,actionId: status.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className="flex items-center px-1.5 py-1 ml-2 border-gray-500 bg-gray-700 rounded hover:bg-gray-600" onClick={replayAction}>
|
||||||
|
<span className="mr-1.5">Retry</span>
|
||||||
|
{mutation.isLoading
|
||||||
|
? <RingResizeSpinner className="text-blue-500 w-4 h-4 iconHeight" aria-hidden="true" />
|
||||||
|
: <ArrowPathIcon className="h-4 w-4" />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ReleaseStatusCellProps {
|
interface ReleaseStatusCellProps {
|
||||||
value: ReleaseActionStatus[];
|
value: ReleaseActionStatus[];
|
||||||
}
|
}
|
||||||
|
@ -64,69 +110,89 @@ interface ReleaseStatusCellProps {
|
||||||
interface StatusCellMapEntry {
|
interface StatusCellMapEntry {
|
||||||
colors: string;
|
colors: string;
|
||||||
icon: React.ReactElement;
|
icon: React.ReactElement;
|
||||||
textFormatter: (text: string) => React.ReactElement;
|
textFormatter: (status: ReleaseActionStatus) => React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusCellMap: Record<string, StatusCellMapEntry> = {
|
const StatusCellMap: Record<string, StatusCellMapEntry> = {
|
||||||
"PUSH_ERROR": {
|
"PUSH_ERROR": {
|
||||||
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
|
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
|
||||||
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />,
|
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />,
|
||||||
textFormatter: (text: string) => (
|
textFormatter: (status: ReleaseActionStatus) => (
|
||||||
<>
|
<>
|
||||||
|
<span>
|
||||||
Action
|
Action
|
||||||
{" "}
|
{" "}
|
||||||
<span className="font-bold underline underline-offset-2 decoration-2 decoration-red-500">
|
<span className="font-bold underline underline-offset-2 decoration-2 decoration-red-500">
|
||||||
error
|
error
|
||||||
|
</span>
|
||||||
|
{": "}
|
||||||
|
{status.action}
|
||||||
</span>
|
</span>
|
||||||
{": "}
|
<div>
|
||||||
{text}
|
{status.action_id > 0 && <RetryActionButton status={status} />}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"PUSH_REJECTED": {
|
"PUSH_REJECTED": {
|
||||||
colors: "bg-blue-100 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
|
colors: "bg-blue-100 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
|
||||||
icon: <NoSymbolIcon className="h-5 w-5" aria-hidden="true" />,
|
icon: <NoSymbolIcon className="h-5 w-5" aria-hidden="true" />,
|
||||||
textFormatter: (text: string) => (
|
textFormatter: (status: ReleaseActionStatus) => (
|
||||||
<>
|
<>
|
||||||
|
<span>
|
||||||
Action
|
Action
|
||||||
{" "}
|
{" "}
|
||||||
<span
|
<span
|
||||||
className="font-bold underline underline-offset-2 decoration-2 decoration-sky-500"
|
className="font-bold underline underline-offset-2 decoration-2 decoration-sky-500"
|
||||||
>
|
>
|
||||||
rejected
|
rejected
|
||||||
|
</span>
|
||||||
|
{": "}
|
||||||
|
{status.action}
|
||||||
</span>
|
</span>
|
||||||
{": "}
|
<div>
|
||||||
{text}
|
{status.action_id > 0 && <RetryActionButton status={status} />}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"PUSH_APPROVED": {
|
"PUSH_APPROVED": {
|
||||||
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
||||||
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />,
|
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />,
|
||||||
textFormatter: (text: string) => (
|
textFormatter: (status: ReleaseActionStatus) => (
|
||||||
<>
|
<>
|
||||||
Action
|
<span>
|
||||||
{" "}
|
Action
|
||||||
<span className="font-bold underline underline-offset-2 decoration-2 decoration-green-500">
|
{" "}
|
||||||
|
<span className="font-bold underline underline-offset-2 decoration-2 decoration-green-500">
|
||||||
approved
|
approved
|
||||||
|
</span>
|
||||||
|
{": "}
|
||||||
|
{status.action}
|
||||||
</span>
|
</span>
|
||||||
{": "}
|
{/*<div>*/}
|
||||||
{text}
|
{/* {status.action_id > 0 && <RetryActionButton status={status} />}*/}
|
||||||
|
{/*</div>*/}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"PENDING": {
|
"PENDING": {
|
||||||
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
|
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
|
||||||
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />,
|
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />,
|
||||||
textFormatter: (text: string) => (
|
textFormatter: (status: ReleaseActionStatus) => (
|
||||||
<>
|
<>
|
||||||
Action
|
<span>
|
||||||
{" "}
|
Action
|
||||||
<span className="font-bold underline underline-offset-2 decoration-2 decoration-yellow-500">
|
{" "}
|
||||||
|
<span className="font-bold underline underline-offset-2 decoration-2 decoration-yellow-500">
|
||||||
pending
|
pending
|
||||||
|
</span>
|
||||||
|
{": "}
|
||||||
|
{status.action}
|
||||||
</span>
|
</span>
|
||||||
{": "}
|
<div>
|
||||||
{text}
|
{status.action_id > 0 && <RetryActionButton status={status} />}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -156,7 +222,7 @@ export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
|
||||||
>
|
>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={StatusCellMap[v.status].icon}
|
label={StatusCellMap[v.status].icon}
|
||||||
title={StatusCellMap[v.status].textFormatter(v.action)}
|
title={StatusCellMap[v.status].textFormatter(v)}
|
||||||
>
|
>
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
<CellLine title="Type">{v.type}</CellLine>
|
<CellLine title="Type">{v.type}</CellLine>
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const Tooltip = ({
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{title ? (
|
{title ? (
|
||||||
<div className="p-2 border-b border-gray-300 bg-gray-100 dark:border-gray-700 dark:bg-gray-800 rounded-t-md">
|
<div className="flex justify-between items-center p-2 border-b border-gray-300 bg-gray-100 dark:border-gray-700 dark:bg-gray-800 rounded-t-md">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -4,11 +4,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useEffect, useRef, useState } from "react";
|
import React, { Fragment, useEffect, useRef, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Field, FieldArray, FieldProps, FormikValues, useFormikContext } from "formik";
|
import { Field, FieldArray, FieldProps, FormikValues, useFormikContext } from "formik";
|
||||||
import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react";
|
import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react";
|
||||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionContentLayoutOptions,
|
ActionContentLayoutOptions,
|
||||||
|
@ -25,6 +26,7 @@ import { classNames } from "@utils";
|
||||||
import { DeleteModal } from "@components/modals";
|
import { DeleteModal } from "@components/modals";
|
||||||
import { CollapsableSection } from "./details";
|
import { CollapsableSection } from "./details";
|
||||||
import { TextArea } from "@components/inputs/input";
|
import { TextArea } from "@components/inputs/input";
|
||||||
|
import Toast from "@components/notifications/Toast";
|
||||||
|
|
||||||
interface FilterActionsProps {
|
interface FilterActionsProps {
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
|
@ -543,6 +545,23 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
const [edit, toggleEdit] = useToggle(initialEdit);
|
const [edit, toggleEdit] = useToggle(initialEdit);
|
||||||
|
|
||||||
|
const removeMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => APIClient.actions.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
remove(idx);
|
||||||
|
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||||
|
// queryClient.invalidateQueries({ queryKey: filterKeys.detail(id) });
|
||||||
|
|
||||||
|
toast.custom((t) => (
|
||||||
|
<Toast type="success" body={`Action ${action?.name} was deleted`} t={t} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeAction = (id: number) => {
|
||||||
|
removeMutation.mutate(id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
|
@ -622,7 +641,7 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
|
||||||
isOpen={deleteModalIsOpen}
|
isOpen={deleteModalIsOpen}
|
||||||
buttonRef={cancelButtonRef}
|
buttonRef={cancelButtonRef}
|
||||||
toggle={toggleDeleteModal}
|
toggle={toggleDeleteModal}
|
||||||
deleteAction={() => remove(idx)}
|
deleteAction={() => removeAction(action.id)}
|
||||||
title="Remove filter action"
|
title="Remove filter action"
|
||||||
text="Are you sure you want to remove this action? This action cannot be undone."
|
text="Are you sure you want to remove this action? This action cannot be undone."
|
||||||
/>
|
/>
|
||||||
|
|
3
web/src/types/Release.d.ts
vendored
3
web/src/types/Release.d.ts
vendored
|
@ -23,9 +23,12 @@ interface ReleaseActionStatus {
|
||||||
id: number;
|
id: number;
|
||||||
status: string;
|
status: string;
|
||||||
action: string;
|
action: string;
|
||||||
|
action_id: number;
|
||||||
type: string;
|
type: string;
|
||||||
client: string;
|
client: string;
|
||||||
filter: string;
|
filter: string;
|
||||||
|
filter_id: number;
|
||||||
|
release_id: number;
|
||||||
rejections: string[];
|
rejections: string[];
|
||||||
timestamp: string
|
timestamp: string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue