diff --git a/internal/database/release.go b/internal/database/release.go index 6b17e82..519fdc7 100644 --- a/internal/database/release.go +++ b/internal/database/release.go @@ -18,6 +18,7 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/lib/pq" "github.com/rs/zerolog" + "github.com/rs/zerolog/log" ) type ReleaseRepo struct { @@ -585,6 +586,40 @@ func (repo *ReleaseRepo) Delete(ctx context.Context) error { return nil } +func (repo *ReleaseRepo) DeleteOlder(ctx context.Context, duration int) error { + tx, err := repo.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer tx.Rollback() + + olderThanTimestamp := time.Now().UTC().Add(-time.Duration(duration) * time.Hour) + log.Debug().Msgf("Deleting releases older than: %v", olderThanTimestamp) + + result, err := tx.ExecContext(ctx, `DELETE FROM "release" WHERE timestamp < $1`, olderThanTimestamp) + if err != nil { + return errors.Wrap(err, "error executing query") + } + + deletedRows, err := result.RowsAffected() + if err != nil { + return errors.Wrap(err, "error fetching rows affected") + } + log.Debug().Msgf("Deleted %d rows from release table", deletedRows) + + _, err = tx.ExecContext(ctx, `DELETE FROM release_action_status WHERE release_id NOT IN (SELECT id FROM "release")`) + if err != nil { + return errors.Wrap(err, "error executing query") + } + + if err := tx.Commit(); err != nil { + return errors.Wrap(err, "error commit transaction delete") + } + + return nil +} + func (repo *ReleaseRepo) CanDownloadShow(ctx context.Context, title string, season int, episode int) (bool, error) { // TODO support non season episode shows // if rls.Day > 0 { diff --git a/internal/domain/release.go b/internal/domain/release.go index 0ce9558..a698734 100644 --- a/internal/domain/release.go +++ b/internal/domain/release.go @@ -34,6 +34,7 @@ type ReleaseRepo interface { GetIndexerOptions(ctx context.Context) ([]string, error) Stats(ctx context.Context) (*ReleaseStats, error) Delete(ctx context.Context) error + DeleteOlder(ctx context.Context, duration int) error CanDownloadShow(ctx context.Context, title string, season int, episode int) (bool, error) GetActionStatus(ctx context.Context, req *GetReleaseActionStatusRequest) (*ReleaseActionStatus, error) diff --git a/internal/http/release.go b/internal/http/release.go index e45df80..aff9247 100644 --- a/internal/http/release.go +++ b/internal/http/release.go @@ -20,6 +20,7 @@ type releaseService interface { GetIndexerOptions(ctx context.Context) ([]string, error) Stats(ctx context.Context) (*domain.ReleaseStats, error) Delete(ctx context.Context) error + DeleteOlder(ctx context.Context, duration int) error Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error } @@ -41,6 +42,7 @@ func (h releaseHandler) Routes(r chi.Router) { r.Get("/stats", h.getStats) r.Get("/indexers", h.getIndexerOptions) r.Delete("/all", h.deleteReleases) + r.Delete("/older-than/{duration}", h.deleteOlder) r.Route("/{releaseId}", func(r chi.Router) { r.Post("/actions/{actionStatusId}/retry", h.retryAction) @@ -193,6 +195,28 @@ func (h releaseHandler) deleteReleases(w http.ResponseWriter, r *http.Request) { h.encoder.NoContent(w) } +func (h releaseHandler) deleteOlder(w http.ResponseWriter, r *http.Request) { + durationStr := chi.URLParam(r, "duration") + duration, err := strconv.Atoi(durationStr) + if err != nil { + h.encoder.StatusResponse(w, http.StatusBadRequest, map[string]interface{}{ + "code": "BAD_REQUEST_PARAMS", + "message": "Invalid duration", + }) + return + } + + if err := h.service.DeleteOlder(r.Context(), duration); err != nil { + h.encoder.StatusResponse(w, http.StatusInternalServerError, map[string]interface{}{ + "code": "INTERNAL_SERVER_ERROR", + "message": err.Error(), + }) + return + } + + h.encoder.NoContent(w) +} + func (h releaseHandler) retryAction(w http.ResponseWriter, r *http.Request) { var ( req *domain.ReleaseActionRetryReq diff --git a/internal/release/service.go b/internal/release/service.go index ad6fb01..e5fe91c 100644 --- a/internal/release/service.go +++ b/internal/release/service.go @@ -26,7 +26,7 @@ type Service interface { Store(ctx context.Context, release *domain.Release) error StoreReleaseActionStatus(ctx context.Context, actionStatus *domain.ReleaseActionStatus) error Delete(ctx context.Context) error - + DeleteOlder(ctx context.Context, duration int) error Process(release *domain.Release) ProcessMultiple(releases []*domain.Release) Retry(ctx context.Context, req *domain.ReleaseActionRetryReq) error @@ -95,6 +95,10 @@ func (s *service) Delete(ctx context.Context) error { return s.repo.Delete(ctx) } +func (s *service) DeleteOlder(ctx context.Context, duration int) error { + return s.repo.DeleteOlder(ctx, duration) +} + func (s *service) Process(release *domain.Release) { if release == nil { return diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index 98b28de..bc7f285 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -196,6 +196,7 @@ export const APIClient = { indexerOptions: () => appClient.Get("api/release/indexers"), stats: () => appClient.Get("api/release/stats"), delete: () => appClient.Delete("api/release/all"), + deleteOlder: (duration: number) => appClient.Delete(`api/release/older-than/${duration}`), replayAction: (releaseId: number, actionId: number) => appClient.Post(`api/release/${releaseId}/actions/${actionId}/retry`) }, updates: { diff --git a/web/src/screens/settings/Releases.tsx b/web/src/screens/settings/Releases.tsx index ec08584..e3690e9 100644 --- a/web/src/screens/settings/Releases.tsx +++ b/web/src/screens/settings/Releases.tsx @@ -3,84 +3,147 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -import { useRef } from "react"; +import React, { useRef, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "react-hot-toast"; import { APIClient } from "@api/APIClient"; import Toast from "@components/notifications/Toast"; +import { releaseKeys } from "@screens/releases/ReleaseTable"; import { useToggle } from "@hooks/hooks"; import { DeleteModal } from "@components/modals"; -import { releaseKeys } from "@screens/releases/ReleaseTable"; function ReleaseSettings() { - const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); - const queryClient = useQueryClient(); - - const deleteMutation = useMutation({ - mutationFn: APIClient.release.delete, - onSuccess: () => { - toast.custom((t) => ( - - )); - - // Invalidate filters just in case, most likely not necessary but can't hurt. - queryClient.invalidateQueries({ queryKey: releaseKeys.lists() }); - } - }); - - const deleteAction = () => deleteMutation.mutate(); - - const cancelModalButtonRef = useRef(null); - return ( -
- - +

Releases

- Release settings. Reset state. + Manage release history.

-
-
-
+
+
+
-

- Danger Zone -

-

This will clear all release history in your database.

-
-
- +

Danger zone

+

+ This will clear release history in your database +

+ +
+ +
- +
+ ); +} + +const getDurationLabel = (durationValue: number): string => { + const durationOptions: Record = { + 0: "all time", + 1: "1 hour", + 12: "12 hours", + 24: "1 day", + 168: "1 week", + 720: "1 month", + 2160: "3 months", + 4320: "6 months", + 8760: "1 year" + }; + + return durationOptions[durationValue] || "Invalid duration"; +}; + +function DeleteReleases() { + const queryClient = useQueryClient(); + const [duration, setDuration] = useState(""); + const [parsedDuration, setParsedDuration] = useState(0); + const cancelModalButtonRef = useRef(null); + const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); + + const deleteOlderMutation = useMutation({ + mutationFn: (duration: number) => APIClient.release.deleteOlder(duration), + onSuccess: () => { + if (parsedDuration === 0) { + toast.custom((t) => ( + + )); + } else { + toast.custom((t) => ( + + )); + } + + // Invalidate filters just in case, most likely not necessary but can't hurt. + queryClient.invalidateQueries({ queryKey: releaseKeys.lists() }); + } + }); + + const deleteOlderReleases = () => { + if (isNaN(parsedDuration) || parsedDuration < 0) { + toast.custom((t) => ); + return; + } + + deleteOlderMutation.mutate(parsedDuration); + }; + + return ( +
+ + + +
+ + +
+
); }