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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,7 @@
// Release details
// These are automatically injected to built HTML
var build = "1601918816";
var build = "1602578170";
var version = "3.52.4";
// Construct the script tag

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),
});