diff --git a/backend/download/serializers.py b/backend/download/serializers.py index c8afde33..1479edad 100644 --- a/backend/download/serializers.py +++ b/backend/download/serializers.py @@ -47,6 +47,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") class DownloadListQueueDeleteQuerySerializer(serializers.Serializer): diff --git a/backend/download/views.py b/backend/download/views.py index 80ed95b5..71729a5c 100644 --- a/backend/download/views.py +++ b/backend/download/views.py @@ -73,6 +73,10 @@ class DownloadApiListView(ApiBaseView): {"term": {"vid_type": {"value": vid_type_filter}}} ) + search_query = validated_data.get("q") + if search_query: + must_list.append({"match_phrase_prefix": {"title": search_query}}) + self.data["query"] = {"bool": {"must": must_list}} self.get_document_list(request) diff --git a/frontend/src/api/loader/loadDownloadQueue.ts b/frontend/src/api/loader/loadDownloadQueue.ts index d4979f75..2a2f4087 100644 --- a/frontend/src/api/loader/loadDownloadQueue.ts +++ b/frontend/src/api/loader/loadDownloadQueue.ts @@ -6,12 +6,14 @@ const loadDownloadQueue = async ( channelId: string | null, vid_type: string | null, showIgnored: boolean, + search: string, ) => { const searchParams = new URLSearchParams(); if (page) searchParams.append('page', page.toString()); if (channelId) searchParams.append('channel', channelId); if (vid_type) searchParams.append('vid_type', vid_type); + if (search) searchParams.append('q', encodeURIComponent(search)); searchParams.append('filter', showIgnored ? 'ignore' : 'pending'); const endpoint = `/api/download/${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; diff --git a/frontend/src/pages/Download.tsx b/frontend/src/pages/Download.tsx index c141648e..9d361459 100644 --- a/frontend/src/pages/Download.tsx +++ b/frontend/src/pages/Download.tsx @@ -4,6 +4,7 @@ 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'; @@ -64,6 +65,8 @@ const Download = () => { 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); const [rescanPending, setRescanPending] = useState(false); @@ -118,6 +121,7 @@ const Download = () => { channelFilterFromUrl, vidTypeFilterFromUrl, showIgnored, + searchInput, ); const { data: channelResponseData } = videosResponse ?? {}; const videoCount = channelResponseData?.paginate?.total_hits; @@ -136,7 +140,7 @@ const Download = () => { useEffect(() => { setRefresh(true); - }, [channelFilterFromUrl, vidTypeFilterFromUrl, currentPage, showIgnored]); + }, [channelFilterFromUrl, vidTypeFilterFromUrl, currentPage, showIgnored, searchInput]); useEffect(() => { (async () => { @@ -156,6 +160,11 @@ const Download = () => { setRefresh(true); }; + const handleShowSearchInput = () => { + if (showSearchInput) setSearchInput(''); + setShowSearchInput(!showSearchInput); + }; + return ( <>