From 8600d3a2ab9cbdfd6dfc5818525b3bf06c293646 Mon Sep 17 00:00:00 2001 From: Fabricio Silva Date: Tue, 3 Oct 2023 20:57:11 +0100 Subject: [PATCH] fix(indexes): toggle on and off with switch (#1164) * chore(indexers): replace array position with id * fix(indexers): enable and disable without editing * feat(indexer): add toggle endpoint and refactoring --------- Co-authored-by: ze0s --- internal/database/indexer.go | 22 +++++ internal/domain/indexer.go | 1 + internal/http/indexer.go | 33 +++++++- internal/indexer/service.go | 121 ++++++++++++++++++--------- web/src/api/APIClient.ts | 7 +- web/src/screens/settings/Indexer.tsx | 28 +++++-- web/src/screens/settings/Irc.tsx | 5 +- 7 files changed, 167 insertions(+), 50 deletions(-) diff --git a/internal/database/indexer.go b/internal/database/indexer.go index 26e8048..d297e98 100644 --- a/internal/database/indexer.go +++ b/internal/database/indexer.go @@ -232,3 +232,25 @@ func (r *IndexerRepo) Delete(ctx context.Context, id int) error { return nil } + +func (r *IndexerRepo) ToggleEnabled(ctx context.Context, indexerID int, enabled bool) error { + var err error + + queryBuilder := r.db.squirrel. + Update("indexer"). + Set("enabled", enabled). + Set("updated_at", sq.Expr("CURRENT_TIMESTAMP")). + Where(sq.Eq{"id": indexerID}) + + query, args, err := queryBuilder.ToSql() + if err != nil { + return errors.Wrap(err, "error building query") + } + + _, err = r.db.handler.ExecContext(ctx, query, args...) + if err != nil { + return errors.Wrap(err, "error executing query") + } + + return nil +} diff --git a/internal/domain/indexer.go b/internal/domain/indexer.go index 2c572bd..a7eb45a 100644 --- a/internal/domain/indexer.go +++ b/internal/domain/indexer.go @@ -22,6 +22,7 @@ type IndexerRepo interface { Delete(ctx context.Context, id int) error FindByFilterID(ctx context.Context, id int) ([]Indexer, error) FindByID(ctx context.Context, id int) (*Indexer, error) + ToggleEnabled(ctx context.Context, indexerID int, enabled bool) error } type Indexer struct { diff --git a/internal/http/indexer.go b/internal/http/indexer.go index 814f796..7ed03b8 100644 --- a/internal/http/indexer.go +++ b/internal/http/indexer.go @@ -22,6 +22,7 @@ type indexerService interface { GetTemplates() ([]domain.IndexerDefinition, error) Delete(ctx context.Context, id int) error TestApi(ctx context.Context, req domain.IndexerTestApiRequest) error + ToggleEnabled(ctx context.Context, indexerID int, enabled bool) error } type indexerHandler struct { @@ -41,13 +42,15 @@ func newIndexerHandler(encoder encoder, service indexerService, ircSvc ircServic func (h indexerHandler) Routes(r chi.Router) { r.Get("/schema", h.getSchema) r.Post("/", h.store) - r.Put("/", h.update) r.Get("/", h.getAll) r.Get("/options", h.list) r.Route("/{indexerID}", func(r chi.Router) { + r.Put("/", h.update) r.Delete("/", h.delete) r.Post("/api/test", h.testApi) + + r.Patch("/enabled", h.toggleEnabled) }) } @@ -178,3 +181,31 @@ func (h indexerHandler) testApi(w http.ResponseWriter, r *http.Request) { h.encoder.StatusResponse(w, http.StatusOK, res) } + +func (h indexerHandler) toggleEnabled(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + indexerID = chi.URLParam(r, "indexerID") + data struct { + Enabled bool `json:"enabled"` + } + ) + + id, err := strconv.Atoi(indexerID) + if err != nil { + h.encoder.Error(w, err) + return + } + + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + h.encoder.Error(w, err) + return + } + + if err := h.service.ToggleEnabled(ctx, id, data.Enabled); err != nil { + h.encoder.Error(w, err) + return + } + + h.encoder.NoContent(w) +} diff --git a/internal/indexer/service.go b/internal/indexer/service.go index 829ea69..f39244a 100644 --- a/internal/indexer/service.go +++ b/internal/indexer/service.go @@ -36,6 +36,7 @@ type Service interface { GetTorznabIndexers() []domain.IndexerDefinition Start() error TestApi(ctx context.Context, req domain.IndexerTestApiRequest) error + ToggleEnabled(ctx context.Context, indexerID int, enabled bool) error } type service struct { @@ -77,8 +78,7 @@ func NewService(log logger.Logger, config *domain.Config, repo domain.IndexerRep func (s *service) Store(ctx context.Context, indexer domain.Indexer) (*domain.Indexer, error) { // if indexer is rss or torznab do additional cleanup for identifier - switch indexer.Implementation { - case "torznab", "newznab", "rss": + if isImplFeed(indexer.Implementation) { // make lowercase cleanName := strings.ToLower(indexer.Name) @@ -114,7 +114,7 @@ func (s *service) Update(ctx context.Context, indexer domain.Indexer) (*domain.I return nil, err } - if indexer.Implementation == "torznab" || indexer.Implementation == "rss" { + if isImplFeed(indexer.Implementation) { if !indexer.Enabled { s.stopFeed(indexer.Identifier) } @@ -220,12 +220,9 @@ func (s *service) mapIndexers() (map[string]*domain.IndexerDefinition, error) { func (s *service) mapIndexer(indexer domain.Indexer) (*domain.IndexerDefinition, error) { definitionName := indexer.Identifier - if indexer.Implementation == "torznab" { - definitionName = "torznab" - } else if indexer.Implementation == "newznab" { - definitionName = "newznab" - } else if indexer.Implementation == "rss" { - definitionName = "rss" + + if isImplFeed(indexer.Implementation) { + definitionName = indexer.Implementation } d := s.getDefinitionByName(definitionName) @@ -332,7 +329,8 @@ func (s *service) Start() error { } for _, indexer := range indexerDefinitions { - if indexer.IRC != nil { + switch indexer.Implementation { + case string(domain.IndexerImplementationIRC): // add to irc server lookup table s.mapIRCServerDefinitionLookup(indexer.IRC.Server, indexer) @@ -342,15 +340,16 @@ func (s *service) Start() error { s.log.Error().Stack().Err(err).Msgf("indexer.start: could not init api client for: '%s'", indexer.Identifier) } } - } - // handle Torznab - if indexer.Implementation == "torznab" { - s.torznabIndexers[indexer.Identifier] = indexer - } else if indexer.Implementation == "newznab" { - s.newznabIndexers[indexer.Identifier] = indexer - } else if indexer.Implementation == "rss" { + // handle feeds + case string(domain.IndexerImplementationRSS): s.rssIndexers[indexer.Identifier] = indexer + + case string(domain.IndexerImplementationTorznab): + s.torznabIndexers[indexer.Identifier] = indexer + + case string(domain.IndexerImplementationNewznab): + s.newznabIndexers[indexer.Identifier] = indexer } } @@ -360,13 +359,16 @@ func (s *service) Start() error { } func (s *service) removeIndexer(indexer domain.Indexer) { - // remove Torznab - if indexer.Implementation == "torznab" { - delete(s.torznabIndexers, indexer.Identifier) - } else if indexer.Implementation == "newznab" { - delete(s.newznabIndexers, indexer.Identifier) - } else if indexer.Implementation == "rss" { + // handle feeds + switch indexer.Implementation { + case string(domain.IndexerImplementationRSS): delete(s.rssIndexers, indexer.Identifier) + + case string(domain.IndexerImplementationTorznab): + delete(s.torznabIndexers, indexer.Identifier) + + case string(domain.IndexerImplementationNewznab): + delete(s.newznabIndexers, indexer.Identifier) } // remove mapped definition @@ -383,7 +385,8 @@ func (s *service) addIndexer(indexer domain.Indexer) error { return errors.New("addindexer: could not find definition") } - if indexerDefinition.IRC != nil { + switch indexer.Implementation { + case string(domain.IndexerImplementationIRC): // add to irc server lookup table s.mapIRCServerDefinitionLookup(indexerDefinition.IRC.Server, indexerDefinition) @@ -393,15 +396,16 @@ func (s *service) addIndexer(indexer domain.Indexer) error { s.log.Error().Stack().Err(err).Msgf("indexer.start: could not init api client for: '%s'", indexer.Identifier) } } - } - // handle Torznab and RSS - if indexerDefinition.Implementation == "torznab" { - s.torznabIndexers[indexer.Identifier] = indexerDefinition - } else if indexer.Implementation == "newznab" { - s.newznabIndexers[indexer.Identifier] = indexerDefinition - } else if indexerDefinition.Implementation == "rss" { + // handle feeds + case string(domain.IndexerImplementationRSS): s.rssIndexers[indexer.Identifier] = indexerDefinition + + case string(domain.IndexerImplementationTorznab): + s.torznabIndexers[indexer.Identifier] = indexerDefinition + + case string(domain.IndexerImplementationNewznab): + s.newznabIndexers[indexer.Identifier] = indexerDefinition } s.mappedDefinitions[indexer.Identifier] = indexerDefinition @@ -419,7 +423,8 @@ func (s *service) updateIndexer(indexer domain.Indexer) error { return errors.New("update indexer: could not find definition") } - if indexerDefinition.IRC != nil { + switch indexer.Implementation { + case string(domain.IndexerImplementationIRC): // add to irc server lookup table s.mapIRCServerDefinitionLookup(indexerDefinition.IRC.Server, indexerDefinition) @@ -429,15 +434,16 @@ func (s *service) updateIndexer(indexer domain.Indexer) error { s.log.Error().Stack().Err(err).Msgf("indexer.start: could not init api client for: '%s'", indexer.Identifier) } } - } - // handle Torznab - if indexerDefinition.Implementation == "torznab" { - s.torznabIndexers[indexer.Identifier] = indexerDefinition - } else if indexer.Implementation == "newznab" { - s.newznabIndexers[indexer.Identifier] = indexerDefinition - } else if indexerDefinition.Implementation == "rss" { + // handle feeds + case string(domain.IndexerImplementationRSS): s.rssIndexers[indexer.Identifier] = indexerDefinition + + case string(domain.IndexerImplementationTorznab): + s.torznabIndexers[indexer.Identifier] = indexerDefinition + + case string(domain.IndexerImplementationNewznab): + s.newznabIndexers[indexer.Identifier] = indexerDefinition } s.mappedDefinitions[indexer.Identifier] = indexerDefinition @@ -672,3 +678,40 @@ func (s *service) TestApi(ctx context.Context, req domain.IndexerTestApiRequest) return nil } + +func (s *service) ToggleEnabled(ctx context.Context, indexerID int, enabled bool) error { + indexer, err := s.FindByID(ctx, indexerID) + if err != nil { + return err + } + + if err := s.repo.ToggleEnabled(ctx, int(indexer.ID), enabled); err != nil { + s.log.Error().Err(err).Msg("could not update indexer enabled") + return err + } + + // update indexerInstances + if err := s.updateIndexer(*indexer); err != nil { + s.log.Error().Err(err).Msgf("failed to add indexer: %s", indexer.Name) + return err + } + + if isImplFeed(indexer.Implementation) { + if !indexer.Enabled { + s.stopFeed(indexer.Identifier) + } + } + + s.log.Debug().Msgf("indexer.toggle_enabled: update indexer '%d' to '%v'", indexerID, enabled) + + return nil +} + +func isImplFeed(implementation string) bool { + switch implementation { + case "torznab", "newznab", "rss": + return true + default: + return false + } +} diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index f571b84..2434a9a 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -240,13 +240,16 @@ export const APIClient = { create: (indexer: Indexer) => appClient.Post("api/indexer", { body: indexer }), - update: (indexer: Indexer) => appClient.Put("api/indexer", { + update: (indexer: Indexer) => appClient.Put(`api/indexer/${indexer.id}`, { body: indexer }), delete: (id: number) => appClient.Delete(`api/indexer/${id}`), testApi: (req: IndexerTestApiReq) => appClient.Post(`api/indexer/${req.id}/api/test`, { body: req - }) + }), + toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/indexer/${id}/enabled`, { + body: { enabled } + }), }, irc: { getNetworks: () => appClient.Get("api/irc"), diff --git a/web/src/screens/settings/Indexer.tsx b/web/src/screens/settings/Indexer.tsx index 62fde0b..1e848e1 100644 --- a/web/src/screens/settings/Indexer.tsx +++ b/web/src/screens/settings/Indexer.tsx @@ -4,9 +4,11 @@ */ import { useState, useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Switch } from "@headlessui/react"; +import Toast from "@components/notifications/Toast"; import { IndexerAddForm, IndexerUpdateForm } from "@forms"; import { useToggle } from "@hooks/hooks"; import { classNames } from "@utils"; @@ -114,8 +116,23 @@ interface ListItemProps { const ListItem = ({ indexer }: ListItemProps) => { const [updateIsOpen, toggleUpdate] = useToggle(false); + const queryClient = useQueryClient(); + + const updateMutation = useMutation({ + mutationFn: (enabled: boolean) => APIClient.indexers.toggleEnable(indexer.id, enabled), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: indexerKeys.lists() }); + toast.custom((t) => ); + } + }); + + const onToggleMutation = (newState: boolean) => { + // backend is rejecting when ending the whole object + updateMutation.mutate(newState); + }; + return ( -
  • +
  • { />
    e.stopPropagation()} checked={indexer.enabled ?? false} - onChange={toggleUpdate} + onChange={onToggleMutation} className={classNames( indexer.enabled ? "bg-blue-500" : "bg-gray-200 dark:bg-gray-600", "relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" @@ -225,8 +243,8 @@ function IndexerSettings() { Implementation {sortedIndexers.getSortIndicator("implementation")}
  • - {sortedIndexers.items.map((indexer, idx) => ( - + {sortedIndexers.items.map((indexer) => ( + ))} diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index edce180..28e0ae9 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -263,7 +263,7 @@ const ListItem = ({ network, expanded }: ListItemProps) => { />
    e.stopPropagation()} + onClick={(e) => e.stopPropagation()} checked={network.enabled} onChange={onToggleMutation} className={classNames( @@ -478,7 +478,7 @@ const ListItemDropdown = ({ const restart = (id: number) => restartMutation.mutate(id); return ( - { e.preventDefault(); @@ -787,4 +787,3 @@ const IRCLogsDropdown = () => { ); }; -