diff --git a/internal/database/release.go b/internal/database/release.go index bd1da3f..3eca4f7 100644 --- a/internal/database/release.go +++ b/internal/database/release.go @@ -3,6 +3,7 @@ package database import ( "context" "database/sql" + "fmt" "strings" "github.com/autobrr/autobrr/internal/domain" @@ -116,6 +117,11 @@ func (repo *ReleaseRepo) Find(ctx context.Context, params domain.ReleaseQueryPar release.ActionStatus = statuses } + if err = tx.Commit(); err != nil { + repo.log.Error().Stack().Err(err).Msg("error finding releases") + return nil, 0, 0, err + } + return releases, nextCursor, total, nil } @@ -139,6 +145,10 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain queryBuilder = queryBuilder.Where(sq.Lt{"r.id": params.Cursor}) } + if params.Search != "" { + queryBuilder = queryBuilder.Where("r.torrent_name LIKE ?", fmt.Sprint("%", params.Search, "%")) + } + if params.Filters.Indexers != nil { filter := sq.And{} for _, v := range params.Filters.Indexers { @@ -154,6 +164,10 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain query, args, err := queryBuilder.ToSql() repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args) + if err != nil { + repo.log.Error().Stack().Err(err).Msg("error building query") + return nil, 0, 0, err + } res := make([]*domain.Release, 0) @@ -197,6 +211,82 @@ func (repo *ReleaseRepo) findReleases(ctx context.Context, tx *Tx, params domain return res, nextCursor, countItems, nil } +func (repo *ReleaseRepo) FindRecent(ctx context.Context) ([]*domain.Release, error) { + tx, err := repo.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return nil, err + } + defer tx.Rollback() + + releases, err := repo.findRecentReleases(ctx, tx) + if err != nil { + return nil, err + } + + for _, release := range releases { + statuses, err := repo.attachActionStatus(ctx, tx, release.ID) + if err != nil { + return releases, err + } + release.ActionStatus = statuses + } + + if err = tx.Commit(); err != nil { + repo.log.Error().Stack().Err(err).Msg("error finding releases") + return nil, err + } + + return releases, nil +} + +func (repo *ReleaseRepo) findRecentReleases(ctx context.Context, tx *Tx) ([]*domain.Release, error) { + queryBuilder := repo.db.squirrel. + Select("r.id", "r.filter_status", "r.rejections", "r.indexer", "r.filter", "r.protocol", "r.title", "r.torrent_name", "r.size", "r.timestamp"). + From("release r"). + OrderBy("r.timestamp DESC"). + Limit(10) + + query, args, err := queryBuilder.ToSql() + repo.log.Trace().Str("database", "release.find").Msgf("query: '%v', args: '%v'", query, args) + if err != nil { + repo.log.Error().Stack().Err(err).Msg("error building query") + return nil, err + } + + res := make([]*domain.Release, 0) + + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + repo.log.Error().Stack().Err(err).Msg("error fetching releases") + return res, nil + } + + defer rows.Close() + + if err := rows.Err(); err != nil { + repo.log.Error().Stack().Err(err) + return res, err + } + + for rows.Next() { + var rls domain.Release + + var indexer, filter sql.NullString + + if err := rows.Scan(&rls.ID, &rls.FilterStatus, pq.Array(&rls.Rejections), &indexer, &filter, &rls.Protocol, &rls.Title, &rls.TorrentName, &rls.Size, &rls.Timestamp); err != nil { + repo.log.Error().Stack().Err(err).Msg("release.find: error scanning data to struct") + return res, err + } + + rls.Indexer = indexer.String + rls.FilterName = filter.String + + res = append(res, &rls) + } + + return res, nil +} + func (repo *ReleaseRepo) GetIndexerOptions(ctx context.Context) ([]string, error) { query := `SELECT DISTINCT indexer FROM "release" UNION SELECT DISTINCT identifier indexer FROM indexer;` @@ -359,7 +449,6 @@ func (repo *ReleaseRepo) Delete(ctx context.Context) error { if err != nil { repo.log.Error().Stack().Err(err).Msg("error deleting all releases") return err - } return nil diff --git a/internal/domain/release.go b/internal/domain/release.go index dabfbea..9477873 100644 --- a/internal/domain/release.go +++ b/internal/domain/release.go @@ -25,6 +25,7 @@ import ( type ReleaseRepo interface { Store(ctx context.Context, release *Release) (*Release, error) Find(ctx context.Context, params ReleaseQueryParams) (res []*Release, nextCursor int64, count int64, err error) + FindRecent(ctx context.Context) ([]*Release, error) GetIndexerOptions(ctx context.Context) ([]string, error) GetActionStatusByReleaseID(ctx context.Context, releaseID int64) ([]ReleaseActionStatus, error) Stats(ctx context.Context) (*ReleaseStats, error) diff --git a/internal/http/encoder.go b/internal/http/encoder.go index 626d4ff..010b174 100644 --- a/internal/http/encoder.go +++ b/internal/http/encoder.go @@ -10,7 +10,7 @@ type encoder struct{} type errorResponse struct { Message string `json:"message"` - Status int `json:"status"` + Status int `json:"status,omitempty"` } func (e encoder) StatusResponse(ctx context.Context, w http.ResponseWriter, response interface{}, status int) { diff --git a/internal/http/release.go b/internal/http/release.go index 9e3b513..b742db7 100644 --- a/internal/http/release.go +++ b/internal/http/release.go @@ -12,6 +12,7 @@ import ( type releaseService interface { Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error) + FindRecent(ctx context.Context) (res []*domain.Release, err error) GetIndexerOptions(ctx context.Context) ([]string, error) Stats(ctx context.Context) (*domain.ReleaseStats, error) Delete(ctx context.Context) error @@ -31,6 +32,7 @@ func newReleaseHandler(encoder encoder, service releaseService) *releaseHandler func (h releaseHandler) Routes(r chi.Router) { r.Get("/", h.findReleases) + r.Get("/recent", h.findRecentReleases) r.Get("/stats", h.getStats) r.Get("/indexers", h.getIndexerOptions) r.Delete("/all", h.deleteReleases) @@ -86,6 +88,7 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) { indexer := vals["indexer"] pushStatus := r.URL.Query().Get("push_status") + search := r.URL.Query().Get("q") query := domain.ReleaseQueryParams{ Limit: uint64(limit), @@ -96,6 +99,7 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) { Indexers []string PushStatus string }{Indexers: indexer, PushStatus: pushStatus}, + Search: search, } releases, nextCursor, count, err := h.service.Find(r.Context(), query) @@ -117,6 +121,23 @@ func (h releaseHandler) findReleases(w http.ResponseWriter, r *http.Request) { h.encoder.StatusResponse(r.Context(), w, ret, http.StatusOK) } +func (h releaseHandler) findRecentReleases(w http.ResponseWriter, r *http.Request) { + + releases, err := h.service.FindRecent(r.Context()) + if err != nil { + h.encoder.StatusNotFound(r.Context(), w) + return + } + + ret := struct { + Data []*domain.Release `json:"data"` + }{ + Data: releases, + } + + h.encoder.StatusResponse(r.Context(), w, ret, http.StatusOK) +} + func (h releaseHandler) getIndexerOptions(w http.ResponseWriter, r *http.Request) { stats, err := h.service.GetIndexerOptions(r.Context()) if err != nil { diff --git a/internal/release/service.go b/internal/release/service.go index 9b5e43e..69e3330 100644 --- a/internal/release/service.go +++ b/internal/release/service.go @@ -13,6 +13,7 @@ import ( type Service interface { Find(ctx context.Context, query domain.ReleaseQueryParams) (res []*domain.Release, nextCursor int64, count int64, err error) + FindRecent(ctx context.Context) ([]*domain.Release, error) GetIndexerOptions(ctx context.Context) ([]string, error) Stats(ctx context.Context) (*domain.ReleaseStats, error) Store(ctx context.Context, release *domain.Release) error @@ -49,6 +50,10 @@ func (s *service) Find(ctx context.Context, query domain.ReleaseQueryParams) (re return s.repo.Find(ctx, query) } +func (s *service) FindRecent(ctx context.Context) (res []*domain.Release, err error) { + return s.repo.FindRecent(ctx) +} + func (s *service) GetIndexerOptions(ctx context.Context) ([]string, error) { return s.repo.GetIndexerOptions(ctx) } diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index f1338c4..acb7d9e 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -140,9 +140,10 @@ export const APIClient = { }, release: { find: (query?: string) => appClient.Get(`api/release${query}`), + findRecent: () => appClient.Get("api/release/recent"), findQuery: (offset?: number, limit?: number, filters?: Array) => { const params = new URLSearchParams(); - if (offset !== undefined) + if (offset !== undefined && offset > 0) params.append("offset", offset.toString()); if (limit !== undefined) @@ -156,6 +157,8 @@ export const APIClient = { params.append("indexer", filter.value); else if (filter.id === "action_status") params.append("push_status", filter.value); + else if (filter.id == "torrent_name") + params.append("q", filter.value); }); return appClient.Get(`api/release?${params.toString()}`); diff --git a/web/src/screens/dashboard/ActivityTable.tsx b/web/src/screens/dashboard/ActivityTable.tsx index e8fd0f9..22dd79a 100644 --- a/web/src/screens/dashboard/ActivityTable.tsx +++ b/web/src/screens/dashboard/ActivityTable.tsx @@ -13,7 +13,6 @@ import { EmptyListState } from "../../components/emptystates"; import * as Icons from "../../components/Icons"; import * as DataTable from "../../components/data-table"; -import { Fragment } from "react"; // This is a custom filter UI for selecting // a unique option from a list @@ -180,8 +179,8 @@ export const ActivityTable = () => { ], []); const { isLoading, data } = useQuery( - "dash_release", - () => APIClient.release.find("?limit=10"), + "dash_recent_releases", + () => APIClient.release.findRecent(), { refetchOnWindowFocus: false } ); diff --git a/web/src/screens/releases/Filters.tsx b/web/src/screens/releases/Filters.tsx index 6f5a425..8668436 100644 --- a/web/src/screens/releases/Filters.tsx +++ b/web/src/screens/releases/Filters.tsx @@ -10,6 +10,7 @@ import { APIClient } from "../../api/APIClient"; import { classNames } from "../../utils"; import { PushStatusOptions } from "../../domain/constants"; import { FilterProps } from "react-table"; +import { DebounceInput } from "react-debounce-input"; interface ListboxFilterProps { id: string; @@ -77,6 +78,7 @@ export const IndexerSelectColumnFilter = ({ return ( ) => { const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status"; return ( -
+
- );}; \ No newline at end of file + );}; + +export const SearchColumnFilter = ({ + column: { filterValue, setFilter, id } +}: FilterProps) => { + return ( +
+ { + setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely + }} + id="filter" + type="text" + autoComplete="off" + className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm border-none" + placeholder="Search releases..." + /> +
+ );}; diff --git a/web/src/screens/releases/ReleaseTable.tsx b/web/src/screens/releases/ReleaseTable.tsx index 924239f..7c3b0fe 100644 --- a/web/src/screens/releases/ReleaseTable.tsx +++ b/web/src/screens/releases/ReleaseTable.tsx @@ -22,7 +22,7 @@ import * as DataTable from "../../components/data-table"; import { IndexerSelectColumnFilter, - PushStatusSelectColumnFilter + PushStatusSelectColumnFilter, SearchColumnFilter } from "./Filters"; type TableState = { @@ -77,7 +77,8 @@ export const ReleaseTable = () => { { Header: "Release", accessor: "torrent_name", - Cell: DataTable.TitleCell + Cell: DataTable.TitleCell, + Filter: SearchColumnFilter }, { Header: "Actions", @@ -185,9 +186,7 @@ export const ReleaseTable = () => { {headerGroups.map((headerGroup) => headerGroup.headers.map((column) => ( column.Filter ? ( -
- <>{column.render("Filter")} -
+ {column.render("Filter")} ) : null )) )}