Standardizing of search_results (overhaul of Mopidy search process, can improve other processes this way)

This commit is contained in:
James Barnsley
2020-10-13 21:37:55 +13:00
parent 81399d6d72
commit 63a2419b86
17 changed files with 768 additions and 1722 deletions

View File

@ -91,13 +91,21 @@ const SearchResults = ({
};
const mapStateToProps = (state, ownProps) => {
const searchResultsSelector = makeSearchResultsSelector();
const { query: { term }, type } = ownProps;
const {
ui: {
uri_schemes_priority = [],
search_results_sort: sort = 'name',
search_results_sort_reverse: sort_reverse = false,
},
} = state;
const searchResultsSelector = makeSearchResultsSelector(term, type);
return {
uri_schemes_priority: state.ui.uri_schemes_priority || [],
results: searchResultsSelector(state, ownProps),
sort: state.ui.search_results_sort,
sort_reverse: state.ui.search_results_sort_reverse,
results: searchResultsSelector(state),
uri_schemes_priority,
sort,
sort_reverse,
};
};

View File

@ -116,6 +116,7 @@ services:
title: Mopidy
local: Local
adding_uris: 'Adding %{count} URI(s)'
searching: 'Searching %{provider} %{type}'
searching_providers: 'Searching %{count} Mopidy providers'
loading_albums: 'Loading %{count} local albums'
spotify:

View File

@ -5,15 +5,6 @@ import { uriSource } from '../../util/helpers';
const spotifyActions = require('../../services/spotify/actions');
const mopidyActions = require('../../services/mopidy/actions');
export function startSearch(search_type, query, only_mopidy = false) {
return {
type: 'SEARCH_STARTED',
search_type,
query,
only_mopidy,
};
}
export function handleException(message, data = {}, description = null, show_notification = true) {
if (!message) {
if (data.message) {
@ -92,6 +83,26 @@ export function updateColdStore(items) {
};
}
/**
* Search results
*/
export function startSearch(query) {
return {
type: 'START_SEARCH',
query,
};
}
export function searchResultsLoaded(query, resultType, results) {
return {
type: 'SEARCH_RESULTS_LOADED',
query,
resultType,
results,
};
}
/**
* Record getters

View File

@ -264,33 +264,89 @@ const CoreMiddleware = (function () {
next(action);
break;
case 'SEARCH_STARTED':
if (ui.allow_reporting) {
ReactGA.event({ category: 'Search', action: 'Started', label: `${action.type}: ${action.query}` });
case 'START_SEARCH': {
const { query } = action;
const {
ui: {
allow_reporting,
uri_schemes_search_enabled = [],
search_settings,
},
} = store.getState();
if (allow_reporting) {
ReactGA.event({
category: 'Search',
action: 'Started',
label: `${query.type}: ${query.term}`,
});
}
console.info(`Searching for ${query.type} matching "${query.term}"`);
// Trigger reducer immediately; this will hose out any previous results
next(action);
// backends that can handle more than just track results
// make sure they are available and respect our settings
const uri_schemes = ui.search_uri_schemes || mopidy.uri_schemes;
const available_full_uri_schemes = ['local:', 'file:', 'gmusic:'];
const full_uri_schemes = available_full_uri_schemes.filter(
(full_uri_scheme) => uri_schemes.indexOf(full_uri_scheme) > -1
);
// initiate spotify searching
if (!action.only_mopidy) {
if (!ui.search_settings || ui.search_settings.spotify) {
store.dispatch(spotifyActions.getSearchResults(action.query));
if (uri_schemes_search_enabled.includes('spotify:')) {
if (!search_settings || search_settings.spotify) {
store.dispatch(spotifyActions.getSearchResults(query));
}
}
// backend searching (mopidy)
store.dispatch(
mopidyActions.getSearchResults(action.search_type, action.query, 100, full_uri_schemes),
);
store.dispatch(mopidyActions.getSearchResults(
query,
100,
uri_schemes_search_enabled.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 : {}),
};
console.log({
term,
type,
prevTerm,
prevType,
allResults,
});
// Merge our new results with the existing (if any)
search_results[resultType] = [
...(search_results[resultType] || []),
...results,
];
next({
...action,
search_results,
});
break;
}
case 'RESTART':
location.reload();

View File

@ -167,9 +167,9 @@ export default function reducer(core = {}, action) {
/**
* Genres
* */
/**
* Genres
* */
case 'SPOTIFY_GENRES_LOADED':
return { ...core, genres: action.genres };
@ -183,92 +183,28 @@ export default function reducer(core = {}, action) {
},
};
/**
* Search results
* */
case 'SEARCH_STARTED':
/**
* Search results
* */
case 'START_SEARCH':
return {
...core,
search_results: {
artists_uris: [],
albums_uris: [],
playlists_uris: [],
query: action.query,
artists: [],
albums: [],
playlists: [],
tracks: [],
},
};
case 'SEARCH_RESULTS_LOADED':
// artists
if (core.search_results && core.search_results.artists_uris) {
var { artists_uris } = core.search_results;
} else {
var artists_uris = [];
}
if (action.artists_uris) artists_uris = [...artists_uris, ...action.artists_uris];
// more tracks
if (typeof (action.artists_more) !== 'undefined') var { artists_more } = action;
else if (core.search_results && core.search_results.artists_more) var { artists_more } = core.search_results;
else var artists_more = null;
// albums
if (core.search_results && core.search_results.albums_uris) {
var { albums_uris } = core.search_results;
} else {
var albums_uris = [];
}
if (action.albums_uris) albums_uris = [...albums_uris, ...action.albums_uris];
// more tracks
if (typeof (action.albums_more) !== 'undefined') var { albums_more } = action;
else if (core.search_results && core.search_results.albums_more) var { albums_more } = core.search_results;
else var albums_more = null;
// playlists
if (core.search_results && core.search_results.playlists_uris) {
var { playlists_uris } = core.search_results;
} else {
var playlists_uris = [];
}
if (action.playlists_uris) playlists_uris = [...playlists_uris, ...action.playlists_uris];
// more tracks
if (typeof (action.playlists_more) !== 'undefined') var { playlists_more } = action;
else if (core.search_results && core.search_results.playlists_more) var { playlists_more } = core.search_results;
else var playlists_more = null;
// tracks
if (core.search_results && core.search_results.tracks) {
var { tracks } = core.search_results;
} else {
var tracks = [];
}
if (action.tracks) tracks = [...tracks, ...formatTracks(action.tracks)];
// more tracks
if (typeof (action.tracks_more) !== 'undefined') var { tracks_more } = action;
else if (core.search_results && core.search_results.tracks_more) var { tracks_more } = core.search_results;
else var tracks_more = null;
case 'SEARCH_RESULTS_LOADED': {
const { search_results } = action;
return {
...core,
search_results: {
artists_more,
artists_uris: removeDuplicates(artists_uris),
albums_more,
albums_uris: removeDuplicates(albums_uris),
playlists_more,
playlists_uris: removeDuplicates(playlists_uris),
tracks,
tracks_more,
},
search_results,
};
}
default:
return core;

View File

@ -546,11 +546,12 @@ export function clearSearchResults() {
};
}
export function getSearchResults(type, term, limit = 100) {
export function getSearchResults(query, limit = 100, uri_schemes) {
return {
type: 'MOPIDY_GET_SEARCH_RESULTS',
query: { type, term },
query,
limit,
uri_schemes,
};
}

View File

@ -2,12 +2,13 @@
import ReactGA from 'react-ga';
import Mopidy from 'mopidy';
import { sha256 } from 'js-sha256';
import { sampleSize } from 'lodash';
import { sampleSize, uniqBy } from 'lodash';
import { i18n } from '../../locale';
import {
generateGuid,
uriSource,
setFavicon,
titleCase,
} from '../../util/helpers';
import {
digestMopidyImages,
@ -239,14 +240,140 @@ const MopidyMiddleware = (function () {
});
};
/**
* Process our search queries queue. We process one item in the queue and then
* reiterate if there are any items remaining.
*
* @param {Object} store
* @param {Array} queries
*/
const processSearchQueue = (store, queue) => {
const {
type,
term,
requestType,
uri_scheme,
method = 'library.search',
data,
} = queue.shift();
const processor = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS;
if (processor && processor.status === 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS',
i18n(
'services.mopidy.searching',
{
provider: titleCase(uri_scheme.replace(':', '')),
type: requestType,
},
),
{
remaining: queue.length,
},
));
/**
* Process our results. Each type has a different method of formatting and destructuring.
*/
const processResults = {
albums: (response) => {
const result = response[0];
const albums = {};
if (result.albums) {
result.albums.forEach((album) => {
if (album.uri) {
albums[album.uri] = {
...albums[album.uri],
...formatAlbum(album),
};
}
});
}
if (result.tracks) {
result.tracks.forEach((track) => {
if (track.album && track.album.uri) {
albums[track.album.uri] = {
...albums[track.album.uri],
...formatAlbum(track.album),
};
}
});
}
return indexToArray(albums);
},
artists: (response) => {
const result = response[0];
const artists = {};
if (result.artists) {
result.artists.forEach((artist) => {
if (artist.uri) {
artists[artist.uri] = {
...artists[artist.uri],
...formatArtist(artist),
};
}
});
}
if (result.tracks) {
result.tracks.forEach((track) => {
if (track.artists) {
track.artists.forEach((artist) => {
if (artist && artist.uri) {
artists[artist.uri] = {
...artists[artist.uri],
...formatArtist(artist),
};
}
});
}
});
}
return indexToArray(artists);
},
playlists: () => {},
tracks: (response) => {
const { tracks = [] } = response[0];
return tracks;
},
};
request(
store,
method,
data,
).then(
(response) => {
if (response.length > 0) {
store.dispatch(coreActions.searchResultsLoaded(
{ term, type },
requestType,
processResults[requestType](response),
));
}
if (queue.length) {
processSearchQueue(store, queue);
} else {
store.dispatch(uiActions.processFinished(
'MOPIDY_GET_SEARCH_RESULTS',
));
}
},
);
};
/**
* Middleware
*
* This behaves like an action interceptor. We listen for specific actions
* and handle special functionality. If the action is not in our switch, then
* it just proceeds to the next middleware, or default functionality
* */
* Middleware
*
* This behaves like an action interceptor. We listen for specific actions
* and handle special functionality. If the action is not in our switch, then
* it just proceeds to the next middleware, or default functionality
* */
return (store) => (next) => (action) => {
switch (action.type) {
case 'MOPIDY_CONNECT':
@ -992,559 +1119,61 @@ const MopidyMiddleware = (function () {
* */
case 'MOPIDY_GET_SEARCH_RESULTS':
case 'MOPIDY_GET_SEARCH_RESULTS': {
const {
uri_schemes = [],
query = {},
} = action;
const types = query.type === 'all'
? ['artists', 'albums', 'tracks']
: [query.type];
// Flush out our previous results
store.dispatch({ type: 'MOPIDY_CLEAR_SEARCH_RESULTS' });
const queue = [];
uri_schemes.forEach(
(uri_scheme) => types.forEach(
(type) => {
const item = {
type: query.type,
term: query.term,
requestType: type,
uri_scheme,
};
switch (type) {
case 'tracks':
item.data = { query: { track_name: [query.term] }, uris: [uri_scheme] };
break;
case 'artists':
item.data = { query: { artist: [query.term] }, uris: [uri_scheme] };
break;
case 'albums':
item.data = { query: { album: [query.term] }, uris: [uri_scheme] };
break;
case 'playlistsXX': // Once we have the result handler, uncomment this
item.method = 'playlists.asList';
item.data = {};
break;
default:
break;
}
queue.push(item);
},
),
);
var uri_schemes_to_ignore = ['spotify:'];
var uri_schemes = Object.assign([], store.getState().ui.uri_schemes_search_enabled);
for (var i = 0; i < uri_schemes.length; i++) {
if (uri_schemes_to_ignore.includes(uri_schemes[i])) {
uri_schemes.splice(i, 1);
}
}
var uri_schemes_total = uri_schemes.length;
var uri_scheme = uri_schemes.shift();
if (uri_schemes_total <= 0) {
store.dispatch(uiActions.createNotification({ content: 'No sources selected', level: 'warning' }));
} else {
if (queue.length) {
store.dispatch(uiActions.startProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
i18n('services.mopidy.searching_providers', { count: uri_schemes_total }),
'MOPIDY_GET_SEARCH_RESULTS',
i18n('services.mopidy.searching'),
{
query: action.query,
limit: action.limit,
total: uri_schemes_total,
remaining: uri_schemes.length,
uri_scheme,
uri_schemes,
total: queue.length,
remaining: queue.length,
},
));
processSearchQueue(store, queue);
}
break;
case 'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR':
var last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
// Cancelling
if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
// No more schemes, so we're done!
} if (!action.data.uri_scheme) {
store.dispatch(uiActions.processFinished('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
// Construct our next batch's task
var next_uri_schemes = Object.assign([], action.data.uri_schemes);
var next_uri_scheme = next_uri_schemes.shift();
// Update UI for this round
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')}`,
{
remaining: action.data.uri_schemes.length,
},
));
switch (action.data.query.type) {
case 'albums':
var last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status === 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')}`,
));
var continue_process = () => {
store.dispatch(uiActions.runProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
{
query: action.data.query,
limit: action.data.limit,
uri_scheme: next_uri_scheme,
uri_schemes: next_uri_schemes,
},
));
};
request(store, 'library.search', { query: { album: [action.data.query.term] }, uris: [action.data.uri_scheme] })
.then(
(response) => {
if (response.length > 0) {
let albums = [];
// Merge our proper album response container
if (response[0].albums) {
albums = [...response[0].albums, ...albums];
}
// Pull the Album objects from our track responses
if (response[0].tracks) {
for (let i = 0; i < response[0].tracks.length; i++) {
if (response[0].tracks[i].album !== undefined && response[0].tracks[i].album.uri !== undefined) {
albums.push(response[0].tracks[i].album);
}
}
}
store.dispatch(coreActions.itemsLoaded(albums));
// and plug in their URIs
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
results: albums_uris,
context: 'albums',
});
}
continue_process();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
continue_process();
},
);
break;
case 'artists':
var last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status === 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')}`,
));
var continue_process = () => {
store.dispatch(uiActions.runProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
{
query: action.data.query,
limit: action.data.limit,
uri_scheme: next_uri_scheme,
uri_schemes: next_uri_schemes,
},
));
};
request(store, 'library.search', { query: { artist: [action.data.query.term] }, uris: [action.data.uri_scheme] })
.then(
(response) => {
if (response.length > 0) {
let artists_uris = [];
// Pull actual artist objects
if (response[0].artists) {
for (var i = 0; i < response[0].artists.length; i++) {
artists_uris.push(response[0].artists.uri);
}
}
// Digest track artists into actual artist results
if (response[0].tracks) {
for (var i = 0; i < response[0].tracks.length; i++) {
if (response[0].tracks[i].artists) {
for (let j = 0; j < response[0].tracks[i].artists.length; j++) {
const artist = response[0].tracks[i].artists[j];
if (artist.uri) {
artists_uris.push(artist.uri);
}
}
}
}
}
artists_uris = removeDuplicates(artists_uris);
store.dispatch(coreActions.loadItems(artists_uris));
// and plug in their URIs
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
context: 'artists',
results: artists_uris,
});
}
continue_process();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
continue_process();
},
);
break;
case 'playlists':
var last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status === 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
'Searching playlists',
));
var continue_process = () => {
store.dispatch(uiActions.processFinished('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
};
request(store, 'playlists.asList')
.then(
(response) => {
if (response.length > 0) {
const playlists_uris = [];
for (var i = 0; i < response.length; i++) {
const playlist = response[i];
if (playlist.name.includes(action.data.query) && action.data.uri_schemes.includes(`${uriSource(playlist.uri)}:`)) {
playlists_uris.push(playlist.uri);
}
}
// load each playlist
for (var i = 0; i < playlists_uris.length; i++) {
store.dispatch(mopidyActions.getPlaylist(playlists_uris[i]));
}
// and plug in their URIs
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
context: 'playlists',
results: playlists_uris,
});
}
continue_process();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
continue_process();
},
);
break;
case 'tracks':
var last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status === 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')}`,
));
var continue_process = () => {
store.dispatch(uiActions.runProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
{
query: action.data.query,
limit: action.data.limit,
uri_scheme: next_uri_scheme,
uri_schemes: next_uri_schemes,
},
));
};
request(store, 'library.search', { query: { any: [action.data.query.term] }, uris: [action.data.uri_scheme] })
.then(
(response) => {
if (response.length > 0 && response[0].tracks !== undefined) {
const { tracks } = response[0];
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
context: 'tracks',
results: formatTracks(tracks),
});
}
continue_process();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
continue_process();
},
);
break;
case 'all':
default:
var process_tracks = () => {
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')} tracks`,
{
remaining: (action.data.uri_schemes.length) + 1,
},
));
request(store, 'library.search', { query: { any: [action.data.query.term] }, uris: [action.data.uri_scheme] })
.then(
(response) => {
if (response.length > 0 && response[0].tracks !== undefined) {
const { tracks } = response[0];
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
context: 'tracks',
results: formatTracks(tracks),
});
}
process_albums();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
process_albums();
},
);
};
var process_albums = () => {
// Quick check to see if we should be cancelling
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')} albums`,
{
remaining: (action.data.uri_schemes.length) + 0.75,
},
));
request(store, 'library.search', { query: { album: [action.data.query.term] }, uris: [action.data.uri_scheme] })
.then(
(response) => {
if (response.length > 0) {
let albums = [];
// Merge actual album responses first
if (response[0].albums) {
albums = [...response[0].albums, ...albums];
}
// Then digest tracks albums
if (response[0].tracks) {
for (let i = 0; i < response[0].tracks.length; i++) {
if (response[0].tracks[i].album !== undefined && response[0].tracks[i].album.uri !== undefined) {
albums.push(response[0].tracks[i].album);
}
}
}
let albums_uris = arrayOf('uri', albums);
albums_uris = removeDuplicates(albums_uris);
store.dispatch(coreActions.itemsLoaded(formatAlbums(albums)));
// and plug in their URIs
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
context: 'albums',
results: albums_uris,
});
}
process_artists();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
process_artists();
},
);
};
var process_artists = () => {
// Quick check to see if we should be cancelling
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')} artists`,
{
remaining: (action.data.uri_schemes.length) + 0.5,
},
));
request(store, 'library.search', { query: { artist: [action.data.query.term] }, uris: [action.data.uri_scheme] })
.then(
(response) => {
if (response.length > 0) {
let artists_uris = [];
// Pull our actual artists objects
if (response[0].artists) {
for (var i = 0; i < response[0].artists.length; i++) {
artists_uris.push(response[0].artists.uri);
}
}
// Digest tracks artists
if (response[0].tracks) {
for (var i = 0; i < response[0].tracks.length; i++) {
if (response[0].tracks[i].artists) {
for (let j = 0; j < response[0].tracks[i].artists.length; j++) {
const artist = response[0].tracks[i].artists[j];
if (artist.uri) {
artists_uris.push(artist.uri);
}
}
}
}
}
artists_uris = removeDuplicates(artists_uris);
store.dispatch(coreActions.loadItems(artists_uris));
// and plug in their URIs
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
context: 'artists',
results: artists_uris,
});
}
process_playlists();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
process_playlists();
},
);
};
var process_playlists = () => {
// Quick check to see if we should be cancelling
const last_run = store.getState().ui.processes.MOPIDY_GET_SEARCH_RESULTS_PROCESSOR;
if (last_run && last_run.status == 'cancelling') {
store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS_PROCESSOR'));
return;
}
if (action.data.uri_scheme == 'm3u:') {
store.dispatch(uiActions.updateProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
`Searching ${action.data.uri_scheme.replace(':', '')} playlists`,
{
remaining: (action.data.uri_schemes.length) + 0.25,
},
));
request(store, 'playlists.asList')
.then(
(response) => {
if (response.length > 0) {
let playlists_uris = [];
for (var i = 0; i < response.length; i++) {
const playlist = response[i];
if (playlist.name.includes(action.data.query.term) && action.data.uri_schemes.includes(`${uriSource(playlist.uri)}:`)) {
playlists_uris.push(playlist.uri);
}
}
playlists_uris = playlists_uris;
// load each playlist
for (var i = 0; i < playlists_uris.length; i++) {
store.dispatch(mopidyActions.getPlaylist(playlists_uris[i]));
}
// and plug in their URIs
store.dispatch({
type: 'MOPIDY_SEARCH_RESULTS_LOADED',
query: action.data.query,
context: 'playlists',
results: playlists_uris,
});
}
finished();
},
(error) => {
store.dispatch(coreActions.handleException(
`Mopidy: ${error.message ? error.message : 'Search failed'}`,
error,
));
finished();
},
);
} else {
finished();
}
};
var finished = () => {
// We're finally done searching for types on this provider
// On to the next scheme!
store.dispatch(uiActions.runProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
{
query: action.data.query,
limit: action.data.limit,
uri_scheme: next_uri_scheme,
uri_schemes: next_uri_schemes,
remaining: action.data.uri_schemes.length,
},
));
};
// Kick things off with the tracks
process_tracks();
}
break;
}
case 'MOPIDY_GET_PLAYLIST':
request(store, 'playlists.lookup', { uri: action.uri })

View File

@ -563,13 +563,7 @@ export function getMore(url, core_action = null, custom_action = null, extra_dat
};
}
export function clearSearchResults() {
return {
type: 'SPOTIFY_CLEAR_SEARCH_RESULTS',
};
}
export function getSearchResults(type, term, limit = 50, offset = 0) {
export function getSearchResults({ type, term }, limit = 50, offset = 0) {
return (dispatch, getState) => {
dispatch(uiActions.startProcess('SPOTIFY_GET_SEARCH_RESULTS_PROCESSOR', 'Searching Spotify'));
@ -588,54 +582,39 @@ export function getSearchResults(type, term, limit = 50, offset = 0) {
.then(
(response) => {
if (response.tracks !== undefined) {
dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'tracks',
query: { type, term },
results: formatTracks(response.tracks.items),
more: response.tracks.next,
});
dispatch(coreActions.searchResultsLoaded(
{ term, type },
'tracks',
formatTracks(response.tracks.items),
));
}
if (response.artists !== undefined) {
dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'artists',
query: { type, term },
results: arrayOf('uri', response.artists.items),
more: response.artists.next,
});
// TODO: Loading items into index causes massive performance issue
// Not the formatter, not coldstorage (async) and not building new index
dispatch(coreActions.itemsLoaded(formatArtists(response.artists.items)));
dispatch(coreActions.searchResultsLoaded(
{ term, type },
'artists',
formatArtists(response.artists.items),
));
}
if (response.albums !== undefined) {
dispatch(coreActions.itemsLoaded(formatAlbums(response.albums.items)));
dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'albums',
query: { type, term },
results: arrayOf('uri', response.albums.items),
more: response.albums.next,
});
dispatch(coreActions.searchResultsLoaded(
{ term, type },
'albums',
formatAlbums(response.albums.items),
));
}
if (response.playlists !== undefined) {
const playlists = response.playlists.items.map((item) => ({
...formatPlaylist(item),
can_edit: (getState().spotify.me && item.owner.id === getState().spotify.me.id),
tracks_total: item.tracks.total,
}));
dispatch(coreActions.itemsLoaded(playlists));
dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'playlists',
query: { type, term },
results: arrayOf('uri', playlists),
more: response.playlists.next,
});
dispatch(coreActions.searchResultsLoaded(
{ term, type },
'playlists',
playlists,
));
}
dispatch(uiActions.processFinished('SPOTIFY_GET_SEARCH_RESULTS_PROCESSOR'));

View File

@ -327,83 +327,6 @@ const SpotifyMiddleware = (function () {
next(action);
break;
/**
* Searching
* More results are lazy-loaded on demand, based on the _more URL
* */
case 'SEARCH_STARTED':
store.dispatch({
type: 'SPOTIFY_CLEAR_SEARCH_RESULTS',
});
next(action);
break;
case 'SPOTIFY_SEARCH_RESULTS_LOADED_MORE_TRACKS':
store.dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'tracks',
results: formatTracks(action.data.tracks.items),
more: action.data.tracks.next,
});
break;
case 'SPOTIFY_SEARCH_RESULTS_LOADED_MORE_ARTISTS':
store.dispatch({
type: 'ARTISTS_LOADED',
artists: action.data.artists.items,
});
store.dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'artists',
results: arrayOf('uri', action.data.playlists.items),
more: action.data.playlists.next,
});
break;
case 'SPOTIFY_SEARCH_RESULTS_LOADED_MORE_ALBUMS':
store.dispatch({
type: 'ALBUMS_LOADED',
albums: action.data.albums.items,
});
store.dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'playlists',
results: arrayOf('uri', action.data.albums.items),
more: action.data.albums.next,
});
break;
case 'SPOTIFY_SEARCH_RESULTS_LOADED_MORE_PLAYLISTS':
var playlists = [];
for (var i = 0; i < action.data.playlists.items.length; i++) {
playlists.push({
...action.data.playlists.items[i],
tracks_total: action.data.playlists.items[i].tracks.total,
});
}
store.dispatch({
type: 'PLAYLISTS_LOADED',
playlists,
});
store.dispatch({
type: 'SPOTIFY_SEARCH_RESULTS_LOADED',
context: 'playlists',
results: arrayOf('uri', action.data.playlists.items),
more: action.data.playlists.next,
});
break;
case 'SPOTIFY_ME_LOADED':
var me = { ...formatUser(action.me) };

View File

@ -184,36 +184,6 @@ export default function reducer(spotify = {}, action) {
library_tracks_more: action.data.next,
};
case 'SPOTIFY_CLEAR_SEARCH_RESULTS':
return { ...spotify, search_results: {} };
case 'SPOTIFY_SEARCH_RESULTS_LOADED':
// Fetch or create our container
if (spotify.search_results) {
var search_results = { ...spotify.search_results };
} else {
var search_results = {};
}
search_results = {
...search_results,
query: action.query,
};
if (search_results.results) {
search_results[action.context] = [...search_results[action.context], ...action.results];
} else {
search_results[action.context] = action.results;
}
if (action.more) {
search_results[`${action.context}_more`] = action.more;
} else {
search_results[`${action.context}_more`] = null;
}
return { ...spotify, search_results };
default:
return spotify;
}

View File

@ -162,6 +162,7 @@ const corePersistConfig = {
'albums',
'artists',
'playlists',
'search_results',
'users',
'tracks',
'libraries', // We manually hydrate this, so we can handle the rehydration of library items
@ -252,7 +253,6 @@ const store = createStore(
state,
applyMiddleware(
thunk,
localstorageMiddleware,
coreMiddleware,
uiMiddleware,
mopidyMiddleware,

View File

@ -7,6 +7,7 @@ const getItems = (state) => state.core.items;
const getLoadQueue = (state) => state.ui.load_queue;
const getLibrary = (state, uri) => state.core.libraries[uri];
const getLibraries = (state) => state.core.libraries;
const getSearchResults = (state) => state.core.search_results;
const makeItemSelector = (uri) => createSelector(
[getItems],
@ -42,20 +43,11 @@ const makeLibrarySelector = (uris) => createSelector(
},
);
const getMopidySearchResults = (state, props) => (
state.mopidy.search_results && state.mopidy.search_results[props.type]
);
const getSpotifySearchResults = (state, props) => (
state.spotify.search_results && state.spotify.search_results[props.type]
);
const makeSearchResultsSelector = () => createSelector(
[getMopidySearchResults, getSpotifySearchResults, getItems],
(mopidySearchResults, spotifySearchResults, items) => {
const uris = [
...mopidySearchResults || [],
...spotifySearchResults || [],
];
return indexToArray(items, uris);
const makeSearchResultsSelector = (term, type) => createSelector(
[getSearchResults],
(searchResults) => {
if (!searchResults || searchResults.query.term !== term) return [];
return searchResults[type] || [];
},
);

View File

@ -96,8 +96,6 @@ class Search extends React.Component {
}
onSourceClose = () => {
spotifyActions.clearSearchResults();
mopidyActions.clearSearchResults();
this.search();
};
@ -118,59 +116,44 @@ class Search extends React.Component {
clearSearch = () => {
const {
spotifyActions,
mopidyActions,
uiActions: {
setWindowTitle,
},
} = this.props;
spotifyActions.clearSearchResults();
mopidyActions.clearSearchResults();
setWindowTitle(i18n('search.title'));
this.setState({ term: '' });
}
search = () => {
const {
coreActions: {
startSearch,
},
uiActions: {
setWindowTitle,
},
uri_schemes_search_enabled,
mopidyActions,
spotifyActions,
mopidy_search_results: {
query: {
term: mopidyTerm,
type: mopidyType,
} = {},
},
spotify_search_results: {
query: {
term: spotifyTerm,
type: spotifyType,
} = {},
},
} = this.props;
const {
type,
term,
} = this.state;
console.info(`Searching for ${type} matching "${term}"`);
setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) }));
if (type && term) {
if (mopidyTerm !== term || mopidyType !== type) {
mopidyActions.clearSearchResults();
mopidyActions.getSearchResults(type, term);
}
/**
* TODO: Searches are being triggered when navigating to type subviews, despite already having
* results. This might be a consequence of having providers merging their results into one array
*/
if ((spotifyTerm !== term || spotifyType !== type) && uri_schemes_search_enabled.includes('spotify:')) {
spotifyActions.clearSearchResults();
if (type && term) {
startSearch({ type, term });
/*
mopidyActions.getSearchResults(type, term);
if (uri_schemes_search_enabled.includes('spotify:')) {
spotifyActions.getSearchResults(type, term);
}
}*/
}
}
@ -311,11 +294,9 @@ class Search extends React.Component {
const mapStateToProps = (state, ownProps) => ({
type: ownProps.match.params.type,
term: ownProps.match.params.term,
uri_schemes_search_enabled: (state.ui.uri_schemes_search_enabled ? state.ui.uri_schemes_search_enabled : []),
uri_schemes: (state.mopidy.uri_schemes ? state.mopidy.uri_schemes : []),
mopidy_search_results: (state.mopidy.search_results ? state.mopidy.search_results : {}),
spotify_search_results: (state.spotify.search_results ? state.spotify.search_results : {}),
sort: (state.ui.search_results_sort ? state.ui.search_results_sort : 'followers.total'),
uri_schemes_search_enabled: state.ui.uri_schemes_search_enabled || [],
uri_schemes: state.mopidy.uri_schemes || [],
sort: state.ui.search_results_sort || 'followers.total',
sort_reverse: (!!state.ui.search_results_sort_reverse),
});