From b011292a8cbe3c60b575483f4ffd5eb1f1a874e2 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Sun, 8 Oct 2023 21:14:08 +1300 Subject: [PATCH] Search results stored by provider:type:term - This allows storing of per-provider, per-type, per-term results - We can restore old search results, and hide/show by provider without needing to re-query --- src/js/App.js | 2 +- src/js/components/SearchResults.js | 12 +++-- src/js/locale/en.yaml | 2 +- src/js/services/core/actions.js | 5 +- src/js/services/core/middleware.js | 53 ++------------------- src/js/services/core/reducer.js | 18 ++----- src/js/services/mopidy/actions.js | 4 +- src/js/services/mopidy/middleware.js | 37 ++++++++------- src/js/services/mopidy/reducer.js | 27 ----------- src/js/services/spotify/actions.js | 16 +++---- src/js/store/index.js | 1 + src/js/util/helpers.js | 5 ++ src/js/util/selectors.js | 15 +++--- src/js/views/Search.js | 70 ++++++++++++++++++++-------- src/scss/views/_search.scss | 2 +- 15 files changed, 115 insertions(+), 154 deletions(-) diff --git a/src/js/App.js b/src/js/App.js index d097b168f..38ca038ed 100755 --- a/src/js/App.js +++ b/src/js/App.js @@ -54,7 +54,7 @@ const Content = () => ( } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/js/components/SearchResults.js b/src/js/components/SearchResults.js index 01cd95fd2..d1ef8cb4d 100755 --- a/src/js/components/SearchResults.js +++ b/src/js/components/SearchResults.js @@ -16,12 +16,14 @@ const SearchResults = ({ type, all, }) => { - const { term } = useParams(); + const { term, providers = '' } = useParams(); const [sortField, sortReverse] = useSelector( (state) => getSortSelector(state, SORT_KEY, 'name'), ); - const searchResultsSelector = makeSearchResultsSelector(term, type); + const providersArray = providers.split(','); + const searchResultsSelector = makeSearchResultsSelector(providersArray, term, type); const rawResults = useSelector(searchResultsSelector); + console.debug(rawResults, providersArray, term, type) const encodedTerm = encodeURIComponent(term); let results = [...rawResults]; @@ -43,7 +45,7 @@ const SearchResults = ({

{!all && ( - + {' '} @@ -53,7 +55,7 @@ const SearchResults = ({ )} {all && ( - + )} @@ -66,7 +68,7 @@ const SearchResults = ({ {type === 'tracks' && ( i !== 'spotify:'), // Omit Spotify; handled above + providers.filter((i) => i !== 'spotify:'), // Omit Spotify; handled above )); break; } - case 'SEARCH_RESULTS_LOADED': { - const { - query: { - term, - type, - }, - resultType, - results, - } = action; - const { - core: { - search_results: { - query: { - term: prevTerm, - type: prevType, - } = {}, - ...allResults - } = {}, - } = {}, - } = store.getState(); - - // Add to our existing results, so long as the search term is the same - const search_results = { - query: { term, type }, - ...(term === prevTerm && type === prevType ? allResults : {}), - }; - - // Merge our new results with the existing (if any) - search_results[resultType] = [ - ...(search_results[resultType] || []), - ...results, - ]; - - next({ - ...action, - search_results, - }); - break; - } - case 'PLAYLIST_TRACKS_ADDED': { const { key, diff --git a/src/js/services/core/reducer.js b/src/js/services/core/reducer.js index 7b771b077..9f424d36c 100755 --- a/src/js/services/core/reducer.js +++ b/src/js/services/core/reducer.js @@ -176,23 +176,15 @@ export default function reducer(core = {}, action) { /** * Search results * */ - case 'START_SEARCH': - return { - ...core, - search_results: { - query: action.query, - artists: [], - albums: [], - playlists: [], - tracks: [], - }, - }; case 'SEARCH_RESULTS_LOADED': { - const { search_results } = action; + console.debug(action) return { ...core, - search_results, + search_results: { + ...core?.search_results || {}, + [action.key]: action.results, + }, }; } diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index 48c883157..bbd06dddb 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -591,12 +591,12 @@ export function clearSearchResults() { }; } -export function getSearchResults(query, limit = 100, uri_schemes) { +export function getSearchResults(query, providers, limit = 100) { return { type: 'MOPIDY_GET_SEARCH_RESULTS', query, + providers, limit, - uri_schemes, }; } diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 310019dfe..bc04c878e 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -8,6 +8,7 @@ import { uriSource, setFavicon, titleCase, + getSearchResultKey, } from '../../util/helpers'; import { digestMopidyImages, @@ -263,10 +264,11 @@ const MopidyMiddleware = (function () { type, term, requestType, - uri_scheme, + provider, method = 'library.search', data, } = queue.shift(); + const resultKey = getSearchResultKey({ provider, type, term }); const processKey = 'MOPIDY_GET_SEARCH_RESULTS'; const processor = store.getState().ui.processes[processKey]; @@ -281,7 +283,7 @@ const MopidyMiddleware = (function () { content: i18n( 'services.mopidy.searching', { - provider: titleCase(uri_scheme.replace(':', '')), + provider: titleCase(provider.replace(':', '')), type: requestType, }, ), @@ -348,7 +350,7 @@ const MopidyMiddleware = (function () { playlists: (response) => { const playlists = response.filter( (item) => { - if (!item.uri.includes(uri_scheme)) return false; + if (!item.uri.includes(provider)) return false; return item.name.toLowerCase().includes(term.toLowerCase()); }, ); @@ -371,8 +373,7 @@ const MopidyMiddleware = (function () { (response) => { if (response.length > 0) { store.dispatch(coreActions.searchResultsLoaded( - { term, type }, - requestType, + resultKey, processResults[requestType](response), )); } @@ -1221,35 +1222,35 @@ const MopidyMiddleware = (function () { case 'MOPIDY_GET_SEARCH_RESULTS': { const { - uri_schemes = [], - query = {}, + query: { term, type: queryType } = {}, + providers, } = action; - const types = query.type === 'all' + const types = queryType === 'all' ? ['artists', 'albums', 'tracks', 'playlists'] - : [query.type]; + : [queryType]; const queue = []; - uri_schemes.forEach( - (uri_scheme) => types.forEach( + providers.forEach( + (provider) => types.forEach( (type) => { const item = { - type: query.type, - term: query.term, + type, + term, + provider, requestType: type, - uri_scheme, data: { - uris: [uri_scheme], + uris: [provider], }, }; switch (type) { case 'tracks': - item.data.query = { any: [query.term] }; + item.data.query = { any: [term] }; break; case 'artists': - item.data.query = { artist: [query.term] }; + item.data.query = { artist: [term] }; break; case 'albums': - item.data.query = { album: [query.term] }; + item.data.query = { album: [term] }; break; case 'playlists': // Searching for playlists is not supported, so we get a simple diff --git a/src/js/services/mopidy/reducer.js b/src/js/services/mopidy/reducer.js index 9ab621311..ba6239ca9 100755 --- a/src/js/services/mopidy/reducer.js +++ b/src/js/services/mopidy/reducer.js @@ -103,33 +103,6 @@ export default function reducer(mopidy = {}, action) { directory: { ...mopidy.directory, ...action.directory }, }; - /** - * Searching - * */ - case 'MOPIDY_CLEAR_SEARCH_RESULTS': - return { ...mopidy, search_results: {} }; - - case 'MOPIDY_SEARCH_RESULTS_LOADED': - // Fetch or create our container - if (mopidy.search_results) { - var search_results = { ...mopidy.search_results }; - } else { - var search_results = {}; - } - - search_results = { - ...search_results, - query: action.query, - }; - - if (search_results[action.context]) { - search_results[action.context] = [...search_results[action.context], ...action.results]; - } else { - search_results[action.context] = action.results; - } - - return { ...mopidy, search_results }; - default: return mopidy; } diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index e34a8c711..0d47a00ed 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -6,6 +6,7 @@ import { getFromUri, uriType, upgradeSpotifyPlaylistUris, + getSearchResultKey, } from '../../util/helpers'; import { formatPlaylistGroup, @@ -613,7 +614,8 @@ export function getMore(endpoint, core_action = null, custom_action = null, extr }; } -export function getSearchResults({ type, term }, limit = 50, offset = 0) { +export function getSearchResults(query, limit = 50, offset = 0) { + const { type, term } = query; const processKey = 'SPOTIFY_GET_SEARCH_RESULTS'; return (dispatch, getState) => { const { @@ -641,24 +643,21 @@ export function getSearchResults({ type, term }, limit = 50, offset = 0) { (response) => { if (response.tracks !== undefined) { dispatch(coreActions.searchResultsLoaded( - { term, type }, - 'tracks', + getSearchResultKey({ provider: 'spotify', type: 'tracks', term }), formatTracks(response.tracks.items), )); } if (response.artists !== undefined) { dispatch(coreActions.searchResultsLoaded( - { term, type }, - 'artists', + getSearchResultKey({ provider: 'spotify', type: 'artists', term }), formatArtists(response.artists.items), )); } if (response.albums !== undefined) { dispatch(coreActions.searchResultsLoaded( - { term, type }, - 'albums', + getSearchResultKey({ provider: 'spotify', type: 'albums', term }), formatAlbums(response.albums.items), )); } @@ -669,8 +668,7 @@ export function getSearchResults({ type, term }, limit = 50, offset = 0) { can_edit: (meId === item.owner.id), })); dispatch(coreActions.searchResultsLoaded( - { term, type }, - 'playlists', + getSearchResultKey({ provider: 'spotify', type: 'playlists', term }), playlists, )); } diff --git a/src/js/store/index.js b/src/js/store/index.js index eb520b929..cd861b384 100755 --- a/src/js/store/index.js +++ b/src/js/store/index.js @@ -38,6 +38,7 @@ let initialState = { tracks: {}, items: {}, libraries: {}, + search_results: {}, }, ui: { language: 'en', diff --git a/src/js/util/helpers.js b/src/js/util/helpers.js index 20f7aa24f..860b106e4 100755 --- a/src/js/util/helpers.js +++ b/src/js/util/helpers.js @@ -603,6 +603,9 @@ const upgradeSpotifyPlaylistUri = function (uri) { return upgradeSpotifyPlaylistUris([uri])[0]; }; +const getSearchResultKey = ({ provider, type, term }) => + [provider.replace(':', ''), type, term].join(':'); + export { debounce, throttle, @@ -626,6 +629,7 @@ export { upgradeSpotifyPlaylistUris, upgradeSpotifyPlaylistUri, iconFromKeyword, + getSearchResultKey, }; export default { @@ -651,4 +655,5 @@ export default { upgradeSpotifyPlaylistUris, upgradeSpotifyPlaylistUri, iconFromKeyword, + getSearchResultKey, }; diff --git a/src/js/util/selectors.js b/src/js/util/selectors.js index 03782f690..a3f707cce 100755 --- a/src/js/util/selectors.js +++ b/src/js/util/selectors.js @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; import { indexToArray } from './arrays'; -import { isLoading } from './helpers'; +import { getSearchResultKey, isLoading } from './helpers'; import { i18n } from '../locale'; const getItem = (state, uri) => state.core.items[uri]; @@ -68,13 +68,14 @@ const makeLibrarySelector = (name, filtered = true) => createSelector( }, ); -const makeSearchResultsSelector = (term, type) => createSelector( +const makeSearchResultsSelector = (providers = [], term, type) => createSelector( [getSearchResults], - (searchResults) => { - if (!searchResults || searchResults.query.term !== term) return []; - return searchResults[type] || []; - }, -); + (searchResults) => providers.reduce( + (acc, curr) => {[ + ...acc, + ...searchResults[getSearchResultKey({ provider: curr, term, type })] || [], + ]}, []), + ); const makeProcessProgressSelector = (keys) => createSelector( [getProcesses], diff --git a/src/js/views/Search.js b/src/js/views/Search.js index a15ecea2d..cf50da9b5 100755 --- a/src/js/views/Search.js +++ b/src/js/views/Search.js @@ -18,13 +18,21 @@ import { getSortSelector } from '../util/selectors'; const SORT_KEY = 'search_results'; const Search = () => { - const { term, type = 'all' } = useParams(); + const { + term, + providers: providersString = 'all', + type = 'all', + } = useParams(); const dispatch = useDispatch(); const navigate = useNavigate(); const lastQuery = useSelector((state) => state.core?.search_results?.query); const [sortField, sortReverse] = useSelector( (state) => getSortSelector(state, SORT_KEY, 'name'), ); + const allProviders = useSelector((state) => state.mopidy?.uri_schemes || []); + const providers = providersString == 'all' + ? [...allProviders] + : providersString.split(',').filter((str) => allProviders.indexOf(str) > -1); useEffect(() => { dispatch(setWindowTitle('Search')); @@ -32,25 +40,38 @@ const Search = () => { }, []); useEffect(() => { - if (term && type && term !== lastQuery?.term) { + console.debug(providers, lastQuery?.providers) + if (term && type && (term !== lastQuery?.term || providers.length !== lastQuery?.providers?.length)) { + console.debug('STARTING SEARCH', { term, type, providers }) dispatch(setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) }))); - dispatch(startSearch({ term, type })); + dispatch(startSearch({ term, type, providers })); } - }, [term, type]) + }, [term, type, providersString]) - const onSubmit = (nextTerm) => { - const encodedTerm = encodeURIComponent(nextTerm); - navigate(`/search/${type}/${encodedTerm}`); + const onSubmit = (term) => { + updateSearchQuery(term, providers); + } + + const updateSearchQuery = (term, providers) => { + const encodedTerm = encodeURIComponent(term); + navigate(`/search/${type}/${providers.join(',')}/${encodedTerm || ''}`); } const onReset = () => navigate('/search'); - const onSortChange = (field) => { + const onProvidersChange = (providers) => { + console.debug(providers) + // ON BLUR then trigger search event + updateSearchQuery(term, providers) + dispatch(hideContextMenu()); + } + + const onSortChange = (value) => { let reverse = false; - if (field !== null && sortField === field) { + if (value !== null && sortField === value) { reverse = !sortReverse; } - dispatch(setSort(SORT_KEY, field, reverse)); + dispatch(setSort(SORT_KEY, value, reverse)); dispatch(hideContextMenu()); } @@ -62,16 +83,27 @@ const Search = () => { { value: 'duration', label: i18n('common.duration') }, ]; + const providerOptions = allProviders.map((value) => ({ value, label: value})) + const options = ( - + <> + + + ); return ( diff --git a/src/scss/views/_search.scss b/src/scss/views/_search.scss index 00e0e4ef2..06a266cda 100755 --- a/src/scss/views/_search.scss +++ b/src/scss/views/_search.scss @@ -5,7 +5,7 @@ position: absolute; top: 30px; left: 90px; - right: 150px; + right: 250px; input { @include feature_font();