add error state filtering

This commit is contained in:
Simon 2025-07-10 12:28:49 +07:00
parent ffd3bab948
commit 29be11cf75
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
4 changed files with 99 additions and 75 deletions

View File

@ -48,6 +48,7 @@ class DownloadListQuerySerializer(
channel = serializers.CharField(required=False, help_text="channel ID")
page = serializers.IntegerField(required=False)
q = serializers.CharField(required=False, help_text="Search Query")
error = serializers.BooleanField(required=False, allow_null=True)
class DownloadListQueueDeleteQuerySerializer(serializers.Serializer):

View File

@ -77,6 +77,12 @@ class DownloadApiListView(ApiBaseView):
if search_query:
must_list.append({"match_phrase_prefix": {"title": search_query}})
if validated_data.get("error") is not None:
operator = "must" if validated_data["error"] else "must_not"
must_list.append(
{"bool": {operator: [{"exists": {"field": "message"}}]}}
)
self.data["query"] = {"bool": {"must": must_list}}
self.get_document_list(request)

View File

@ -5,6 +5,7 @@ const loadDownloadQueue = async (
page: number,
channelId: string | null,
vid_type: string | null,
errorFilterFromUrl: string | null,
showIgnored: boolean,
search: string,
) => {
@ -14,6 +15,7 @@ const loadDownloadQueue = async (
if (channelId) searchParams.append('channel', channelId);
if (vid_type) searchParams.append('vid_type', vid_type);
if (search) searchParams.append('q', encodeURIComponent(search));
if (errorFilterFromUrl !== null) searchParams.append('error', errorFilterFromUrl);
searchParams.append('filter', showIgnored ? 'ignore' : 'pending');
const endpoint = `/api/download/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;

View File

@ -4,7 +4,6 @@ import iconAdd from '/img/icon-add.svg';
import iconSubstract from '/img/icon-substract.svg';
import iconGridView from '/img/icon-gridview.svg';
import iconListView from '/img/icon-listview.svg';
import iconSearch from '/img/icon-search.svg';
import { Fragment, useEffect, useState } from 'react';
import { useOutletContext, useSearchParams } from 'react-router-dom';
import { ConfigType } from './Home';
@ -59,13 +58,13 @@ const Download = () => {
const channelFilterFromUrl = searchParams.get('channel');
const ignoredOnlyParam = searchParams.get('ignored');
const vidTypeFilterFromUrl = searchParams.get('vid-type');
const errorFilterFromUrl = searchParams.get('error');
const [refresh, setRefresh] = useState(false);
const [showHiddenForm, setShowHiddenForm] = useState(false);
const [addAsAutoStart, setAddAsAutoStart] = useState(false);
const [addAsFlat, setAddAsFlat] = useState(false);
const [showQueueActions, setShowQueueActions] = useState(false);
const [showSearchInput, setShowSearchInput] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [downloadPending, setDownloadPending] = useState(false);
@ -120,6 +119,7 @@ const Download = () => {
currentPage,
channelFilterFromUrl,
vidTypeFilterFromUrl,
errorFilterFromUrl,
showIgnored,
searchInput,
);
@ -140,7 +140,14 @@ const Download = () => {
useEffect(() => {
setRefresh(true);
}, [channelFilterFromUrl, vidTypeFilterFromUrl, currentPage, showIgnored, searchInput]);
}, [
channelFilterFromUrl,
vidTypeFilterFromUrl,
errorFilterFromUrl,
currentPage,
showIgnored,
searchInput,
]);
useEffect(() => {
(async () => {
@ -160,11 +167,6 @@ const Download = () => {
setRefresh(true);
};
const handleShowSearchInput = () => {
if (showSearchInput) setSearchInput('');
setShowSearchInput(!showSearchInput);
};
return (
<>
<title>TA | Downloads</title>
@ -318,74 +320,8 @@ const Download = () => {
</div>
</div>
<div className="view-icons">
{showSearchInput && (
<input
type="text"
placeholder="Search..."
value={searchInput}
onChange={e => setSearchInput(e.target.value)}
autoFocus
/>
)}
<img
src={iconSearch}
alt="search-icon"
title="Search"
onClick={handleShowSearchInput}
/>
<select
name="vid_type_filter"
id="vid_type_filter"
value={vidTypeFilterFromUrl || 'all'}
onChange={async event => {
const value = event.currentTarget.value;
const params = searchParams;
if (value !== 'all') {
params.set('vid-type', value);
} else {
params.delete('vid-type');
}
setSearchParams(params);
}}
>
<option value="all">all types</option>
<option value="videos">Videos</option>
<option value="streams">Streams</option>
<option value="shorts">Shorts</option>
</select>
{channelAggsList && channelAggsList.length > 1 && (
<select
name="channel_filter"
id="channel_filter"
value={channelFilterFromUrl || 'all'}
onChange={async event => {
const value = event.currentTarget.value;
const params = searchParams;
if (value !== 'all') {
params.set('channel', value);
} else {
params.delete('channel');
}
setSearchParams(params);
}}
>
<option value="all">all channels</option>
{channelAggsList.map(channel => {
const [name, id] = channel.key;
const count = channel.doc_count;
return (
<option key={id} value={id}>
{name} ({count})
</option>
);
})}
</select>
)}
<Button onClick={() => setShowQueueActions(!showQueueActions)}>
{showQueueActions ? 'Hide Actions' : 'Show Actions'}
{showQueueActions ? 'Hide Advanced' : 'Show Advanced'}
</Button>
{isGridView && (
@ -449,6 +385,85 @@ const Download = () => {
</h3>
{showQueueActions && (
<div className="settings-group">
<h3>Search & Filter</h3>
<select
name="vid_type_filter"
id="vid_type_filter"
value={vidTypeFilterFromUrl || 'all'}
onChange={async event => {
const value = event.currentTarget.value;
const params = searchParams;
if (value !== 'all') {
params.set('vid-type', value);
} else {
params.delete('vid-type');
}
setSearchParams(params);
}}
>
<option value="all">all types</option>
<option value="videos">Videos</option>
<option value="streams">Streams</option>
<option value="shorts">Shorts</option>
</select>
{channelAggsList && channelAggsList.length > 1 && (
<select
name="channel_filter"
id="channel_filter"
value={channelFilterFromUrl || 'all'}
onChange={async event => {
const value = event.currentTarget.value;
const params = searchParams;
if (value !== 'all') {
params.set('channel', value);
} else {
params.delete('channel');
}
setSearchParams(params);
}}
>
<option value="all">all channels</option>
{channelAggsList.map(channel => {
const [name, id] = channel.key;
const count = channel.doc_count;
return (
<option key={id} value={id}>
{name} ({count})
</option>
);
})}
</select>
)}
<select
name="error_filter"
id="error_filter"
value={errorFilterFromUrl || 'all'}
onChange={async event => {
const value = event.currentTarget.value;
const params = searchParams;
if (value !== 'all') {
params.set('error', value);
} else {
params.delete('error');
}
setSearchParams(params);
}}
>
<option value="all">all error state</option>
<option value="true">has error</option>
<option value="false">has no error</option>
</select>
<input
type="text"
placeholder="Search..."
value={searchInput}
onChange={e => setSearchInput(e.target.value)}
/>
{searchInput && <Button onClick={() => setSearchInput('')}>Clear</Button>}
<h3>Bulk actions</h3>
<p>
Applied filtered by status <i>'{showIgnoredFilter}'</i>