mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(filters): duplicate (#168)
This commit is contained in:
parent
72b74f9d19
commit
cb6cbb83d5
6 changed files with 115 additions and 12 deletions
|
@ -217,7 +217,7 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e
|
||||||
return filters, nil
|
return filters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
|
func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) {
|
||||||
//r.db.lock.RLock()
|
//r.db.lock.RLock()
|
||||||
//defer r.db.lock.RUnlock()
|
//defer r.db.lock.RUnlock()
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
|
||||||
} else {
|
} else {
|
||||||
var res sql.Result
|
var res sql.Result
|
||||||
|
|
||||||
res, err = r.db.handler.Exec(`INSERT INTO filter (
|
res, err = r.db.handler.ExecContext(ctx, `INSERT INTO filter (
|
||||||
name,
|
name,
|
||||||
enabled,
|
enabled,
|
||||||
min_size,
|
min_size,
|
||||||
|
|
|
@ -14,7 +14,7 @@ type FilterRepo interface {
|
||||||
FindByID(ctx context.Context, filterID int) (*Filter, error)
|
FindByID(ctx context.Context, filterID int) (*Filter, error)
|
||||||
FindByIndexerIdentifier(indexer string) ([]Filter, error)
|
FindByIndexerIdentifier(indexer string) ([]Filter, error)
|
||||||
ListFilters(ctx context.Context) ([]Filter, error)
|
ListFilters(ctx context.Context) ([]Filter, error)
|
||||||
Store(filter Filter) (*Filter, error)
|
Store(ctx context.Context, filter Filter) (*Filter, error)
|
||||||
Update(ctx context.Context, filter Filter) (*Filter, error)
|
Update(ctx context.Context, filter Filter) (*Filter, error)
|
||||||
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
|
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
|
||||||
Delete(ctx context.Context, filterID int) error
|
Delete(ctx context.Context, filterID int) error
|
||||||
|
|
|
@ -3,6 +3,7 @@ package filter
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -16,8 +17,9 @@ type Service interface {
|
||||||
FindByIndexerIdentifier(indexer string) ([]domain.Filter, error)
|
FindByIndexerIdentifier(indexer string) ([]domain.Filter, error)
|
||||||
FindAndCheckFilters(release *domain.Release) (bool, *domain.Filter, error)
|
FindAndCheckFilters(release *domain.Release) (bool, *domain.Filter, error)
|
||||||
ListFilters(ctx context.Context) ([]domain.Filter, error)
|
ListFilters(ctx context.Context) ([]domain.Filter, error)
|
||||||
Store(filter domain.Filter) (*domain.Filter, error)
|
Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
|
||||||
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
|
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
|
||||||
|
Duplicate(ctx context.Context, filterID int) (*domain.Filter, error)
|
||||||
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
|
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
|
||||||
Delete(ctx context.Context, filterID int) error
|
Delete(ctx context.Context, filterID int) error
|
||||||
}
|
}
|
||||||
|
@ -96,11 +98,11 @@ func (s *service) FindByIndexerIdentifier(indexer string) ([]domain.Filter, erro
|
||||||
return filters, nil
|
return filters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Store(filter domain.Filter) (*domain.Filter, error) {
|
func (s *service) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) {
|
||||||
// validate data
|
// validate data
|
||||||
|
|
||||||
// store
|
// store
|
||||||
f, err := s.repo.Store(filter)
|
f, err := s.repo.Store(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("could not store filter: %v", filter)
|
log.Error().Err(err).Msgf("could not store filter: %v", filter)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -140,6 +142,56 @@ func (s *service) Update(ctx context.Context, filter domain.Filter) (*domain.Fil
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) {
|
||||||
|
// find filter
|
||||||
|
baseFilter, err := s.repo.FindByID(ctx, filterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
baseFilter.ID = 0
|
||||||
|
baseFilter.Name = fmt.Sprintf("%v Copy", baseFilter.Name)
|
||||||
|
baseFilter.Enabled = false
|
||||||
|
|
||||||
|
// find actions and attach
|
||||||
|
filterActions, err := s.actionRepo.FindByFilterID(ctx, filterID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("could not find filter actions: %+v", &filterID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// find indexers and attach
|
||||||
|
filterIndexers, err := s.indexerSvc.FindByFilterID(ctx, filterID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not find indexers for filter: %+v", &baseFilter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
filter, err := s.repo.Store(ctx, *baseFilter)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not update filter: %v", baseFilter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// take care of connected indexers
|
||||||
|
if err = s.repo.StoreIndexerConnections(ctx, filter.ID, filterIndexers); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filter.Indexers = filterIndexers
|
||||||
|
|
||||||
|
// take care of filter actions
|
||||||
|
actions, err := s.actionRepo.StoreFilterActions(ctx, filterActions, int64(filter.ID))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not store filter actions: %v", filter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.Actions = actions
|
||||||
|
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *service) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error {
|
func (s *service) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error {
|
||||||
if err := s.repo.ToggleEnabled(ctx, filterID, enabled); err != nil {
|
if err := s.repo.ToggleEnabled(ctx, filterID, enabled); err != nil {
|
||||||
log.Error().Err(err).Msg("could not update filter enabled")
|
log.Error().Err(err).Msg("could not update filter enabled")
|
||||||
|
|
|
@ -14,9 +14,10 @@ import (
|
||||||
type filterService interface {
|
type filterService interface {
|
||||||
ListFilters(ctx context.Context) ([]domain.Filter, error)
|
ListFilters(ctx context.Context) ([]domain.Filter, error)
|
||||||
FindByID(ctx context.Context, filterID int) (*domain.Filter, error)
|
FindByID(ctx context.Context, filterID int) (*domain.Filter, error)
|
||||||
Store(filter domain.Filter) (*domain.Filter, error)
|
Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
|
||||||
Delete(ctx context.Context, filterID int) error
|
Delete(ctx context.Context, filterID int) error
|
||||||
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
|
Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error)
|
||||||
|
Duplicate(ctx context.Context, filterID int) (*domain.Filter, error)
|
||||||
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
|
ToggleEnabled(ctx context.Context, filterID int, enabled bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ func newFilterHandler(encoder encoder, service filterService) *filterHandler {
|
||||||
func (h filterHandler) Routes(r chi.Router) {
|
func (h filterHandler) Routes(r chi.Router) {
|
||||||
r.Get("/", h.getFilters)
|
r.Get("/", h.getFilters)
|
||||||
r.Get("/{filterID}", h.getByID)
|
r.Get("/{filterID}", h.getByID)
|
||||||
|
r.Get("/{filterID}/duplicate", h.duplicate)
|
||||||
r.Post("/", h.store)
|
r.Post("/", h.store)
|
||||||
r.Put("/{filterID}", h.update)
|
r.Put("/{filterID}", h.update)
|
||||||
r.Put("/{filterID}/enabled", h.toggleEnabled)
|
r.Put("/{filterID}/enabled", h.toggleEnabled)
|
||||||
|
@ -69,6 +71,23 @@ func (h filterHandler) getByID(w http.ResponseWriter, r *http.Request) {
|
||||||
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
|
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) duplicate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
filterID = chi.URLParam(r, "filterID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(filterID)
|
||||||
|
|
||||||
|
filter, err := h.service.Duplicate(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
h.encoder.StatusNotFound(ctx, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
|
func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
ctx = r.Context()
|
ctx = r.Context()
|
||||||
|
@ -80,7 +99,7 @@ func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filter, err := h.service.Store(data)
|
filter, err := h.service.Store(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// encode error
|
// encode error
|
||||||
return
|
return
|
||||||
|
|
|
@ -72,6 +72,7 @@ export const APIClient = {
|
||||||
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
|
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
|
||||||
create: (filter: Filter) => appClient.Post("api/filters", filter),
|
create: (filter: Filter) => appClient.Post("api/filters", filter),
|
||||||
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
||||||
|
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
|
||||||
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
|
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
|
||||||
delete: (id: number) => appClient.Delete(`api/filters/${id}`),
|
delete: (id: number) => appClient.Delete(`api/filters/${id}`),
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
PencilAltIcon,
|
PencilAltIcon,
|
||||||
SwitchHorizontalIcon,
|
SwitchHorizontalIcon,
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon, DuplicateIcon,
|
||||||
} from "@heroicons/react/outline";
|
} from "@heroicons/react/outline";
|
||||||
|
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
|
@ -125,6 +125,17 @@ const FilterItemDropdown = ({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const duplicateMutation = useMutation(
|
||||||
|
(id: number) => APIClient.filters.duplicate(id),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries("filters");
|
||||||
|
|
||||||
|
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div">
|
<Menu as="div">
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
|
@ -197,8 +208,6 @@ const FilterItemDropdown = ({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
|
||||||
<div className="px-1 py-1">
|
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
|
@ -206,11 +215,33 @@ const FilterItemDropdown = ({
|
||||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
)}
|
)}
|
||||||
|
onClick={() => duplicateMutation.mutate(filter.id)}
|
||||||
|
>
|
||||||
|
<DuplicateIcon
|
||||||
|
className={classNames(
|
||||||
|
active ? "text-white" : "text-blue-500",
|
||||||
|
"w-5 h-5 mr-2"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
<div className="px-1 py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
|
)}
|
||||||
onClick={() => toggleDeleteModal()}
|
onClick={() => toggleDeleteModal()}
|
||||||
>
|
>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-red-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue