diff --git a/IRIS_VERSION b/IRIS_VERSION index 2d09e4284..b24440d98 100755 --- a/IRIS_VERSION +++ b/IRIS_VERSION @@ -1 +1 @@ -3.68.0 +3.69.0 diff --git a/mopidy_iris/__init__.py b/mopidy_iris/__init__.py index c870ea08b..fd776f665 100755 --- a/mopidy_iris/__init__.py +++ b/mopidy_iris/__init__.py @@ -3,7 +3,7 @@ from mopidy import config, ext -__version__ = "3.68.0" +__version__ = "3.69.0" logger = logging.getLogger(__name__) diff --git a/mopidy_iris/system.py b/mopidy_iris/system.py index 868e30162..6b9378452 100755 --- a/mopidy_iris/system.py +++ b/mopidy_iris/system.py @@ -25,7 +25,7 @@ def __init__(self, path): class IrisSystemThread(Thread): - _USE_SUDO = True + _USE_SUDO = os.environ.get("IRIS_USE_SUDO", True) def __init__(self, action, ioloop, callback): Thread.__init__(self) diff --git a/mopidy_iris/system.sh b/mopidy_iris/system.sh index 6e8dee109..bd46b5693 100755 --- a/mopidy_iris/system.sh +++ b/mopidy_iris/system.sh @@ -28,7 +28,10 @@ elif [[ $1 = "restart" ]]; then elif [[ $1 = "local_scan" ]]; then START=$(date +%s) if $IS_CONTAINER; then - SCAN=$(mopidy --config /config/mopidy.conf local scan) + if [ -n "$IRIS_CONFIG_LOCATION" ]; then + SCAN=$(mopidy --config $IRIS_CONFIG_LOCATION local scan) + else + SCAN=$(mopidy --config /config/mopidy.conf local scan) else SCAN=$(sudo mopidyctl local scan) fi diff --git a/package.json b/package.json index 36deb5d4f..cca160be6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mopidy-iris", - "version": "3.68.0", + "version": "3.69.0", "description": "Mopidy HTTP interface", "repository": "https://github.com/jaedb/iris", "author": "James Barnsley ", diff --git a/setup.cfg b/setup.cfg index 6b2cbd149..0ab38d488 100755 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = Mopidy-Iris -version = 3.68.0 +version = 3.69.0 url = https://github.com/jaedb/iris author = James Barnsley author_email = james@barnsley.nz 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..a434fed07 100755 --- a/src/js/components/SearchResults.js +++ b/src/js/components/SearchResults.js @@ -9,6 +9,7 @@ import { Grid } from './Grid'; import { I18n } from '../locale'; import Button from './Button'; import { makeSearchResultsSelector, getSortSelector } from '../util/selectors'; +import useSearchQuery from '../util/useSearchQuery'; const SORT_KEY = 'search_results'; @@ -16,11 +17,12 @@ const SearchResults = ({ type, all, }) => { - const { term } = useParams(); + const { term, providers } = useSearchQuery(); + const encodedProviders = providers.join(',').replace(/:/g,''); const [sortField, sortReverse] = useSelector( (state) => getSortSelector(state, SORT_KEY, 'name'), ); - const searchResultsSelector = makeSearchResultsSelector(term, type); + const searchResultsSelector = makeSearchResultsSelector(providers, term, type); const rawResults = useSelector(searchResultsSelector); const encodedTerm = encodeURIComponent(term); let results = [...rawResults]; @@ -43,17 +45,17 @@ const SearchResults = ({

{!all && ( - + - {' '} + - {' '} + )} {all && ( - + )} @@ -66,7 +68,7 @@ const SearchResults = ({ {type === 'tracks' && ( */} {resultsCount > results.length && ( - )} diff --git a/src/js/components/URILink.js b/src/js/components/URILink.js index 59ca313e3..fd95a6baa 100755 --- a/src/js/components/URILink.js +++ b/src/js/components/URILink.js @@ -47,7 +47,7 @@ export default memo(({ case 'search': var exploded = uri.split(':'); - to = `/search/${exploded[2]}/${exploded[3]}`; + to = `/search/${exploded[2]}/${exploded[3]}/${exploded[4]}`; break; default: diff --git a/src/js/locale/en.yaml b/src/js/locale/en.yaml index 5fdc0ca88..77eb86efc 100755 --- a/src/js/locale/en.yaml +++ b/src/js/locale/en.yaml @@ -337,7 +337,7 @@ search: title_window: 'Search: %{term}' context_actions: sort: Sort - source: 'Sources (%{count})' + source: Sources placeholder: Search all: title: All diff --git a/src/js/services/core/actions.js b/src/js/services/core/actions.js index 914359a64..ff9e80369 100755 --- a/src/js/services/core/actions.js +++ b/src/js/services/core/actions.js @@ -100,11 +100,10 @@ export function startSearch(query) { }; } -export function searchResultsLoaded(query, resultType, results) { +export function searchResultsLoaded(key, results) { return { type: 'SEARCH_RESULTS_LOADED', - query, - resultType, + key, results, }; } diff --git a/src/js/services/core/middleware.js b/src/js/services/core/middleware.js index 30581adc4..358f5516a 100755 --- a/src/js/services/core/middleware.js +++ b/src/js/services/core/middleware.js @@ -177,79 +177,36 @@ const CoreMiddleware = (function () { case 'START_SEARCH': { const { query } = action; + const { term = '', providers = [], type = 'all' } = query; const { ui: { allow_reporting, }, - mopidy: { - uri_schemes = [], - } = {}, } = store.getState(); if (allow_reporting) { ReactGA.event({ category: 'Search', action: 'Started', - label: `${query.type}: ${query.term}`, + label: `${type}: ${term}`, }); } - console.info(`Searching for ${query.type} matching "${query.term}"`); + console.info(`Searching ${providers.length} providers for ${type} matching "${term}"`); // Trigger reducer immediately; this will hose out any previous results next(action); - if (uri_schemes.includes('spotify:')) { + if (providers.includes('spotify')) { store.dispatch(spotifyActions.getSearchResults(query)); } store.dispatch(mopidyActions.getSearchResults( query, - 100, - uri_schemes.filter((i) => 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, @@ -546,7 +503,6 @@ const CoreMiddleware = (function () { case 'LOAD_LIBRARY': store.dispatch(uiActions.startLoading(action.uri, action.uri)); - console.debug(action); const fetchLibrary = () => { switch (uriSource(action.uri)) { case 'spotify': diff --git a/src/js/services/core/reducer.js b/src/js/services/core/reducer.js index 7b771b077..997e4f620 100755 --- a/src/js/services/core/reducer.js +++ b/src/js/services/core/reducer.js @@ -176,23 +176,14 @@ 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; 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..e297c6d1e 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, @@ -31,6 +32,7 @@ import { indexToArray, } from '../../util/arrays'; import { getProvider, getSortSelector } from '../../util/selectors'; +import { iterate } from 'localforage'; const mopidyActions = require('./actions.js'); const coreActions = require('../core/actions.js'); @@ -263,31 +265,44 @@ const MopidyMiddleware = (function () { type, term, requestType, - uri_scheme, + provider, method = 'library.search', data, } = queue.shift(); const processKey = 'MOPIDY_GET_SEARCH_RESULTS'; const processor = store.getState().ui.processes[processKey]; - if (processor && processor.status === 'cancelling') { store.dispatch(uiActions.processCancelled('MOPIDY_GET_SEARCH_RESULTS')); return; } + const iterateNext = (store, queue) => { + if (queue.length) { + processSearchQueue(store, queue); + } else { + store.dispatch(uiActions.processFinished(processKey)); + } + } + store.dispatch(uiActions.updateProcess( processKey, { content: i18n( 'services.mopidy.searching', { - provider: titleCase(uri_scheme.replace(':', '')), + provider: titleCase(provider), type: requestType, }, ), remaining: queue.length, }, )); + + const resultKey = getSearchResultKey({ provider, type, term }); + if (resultKey in store.getState().core.search_results) { + iterateNext(store, queue); + return; + } // Each type has a different method of formatting and destructuring. const processResults = { @@ -348,7 +363,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,17 +386,11 @@ const MopidyMiddleware = (function () { (response) => { if (response.length > 0) { store.dispatch(coreActions.searchResultsLoaded( - { term, type }, - requestType, + resultKey, processResults[requestType](response), )); } - - if (queue.length) { - processSearchQueue(store, queue); - } else { - store.dispatch(uiActions.processFinished(processKey)); - } + iterateNext(store, queue); }, ); }; @@ -969,7 +978,6 @@ const MopidyMiddleware = (function () { } case 'MOPIDY_ENQUEUE_URIS': { - console.debug(action) if (!action.uris || action.uris.length <= 0) { this.props.uiActions.createNotification({ content: 'No URIs to enqueue', @@ -1221,35 +1229,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 @@ -1818,13 +1826,13 @@ const MopidyMiddleware = (function () { if (item.__model__ === 'Track') { tracks.push(formatTrack(item)); } else if (item.__model__ === 'Ref' && item.type === 'track') { - tracks.push(formatTrack({ ...item, loading: true })); + tracks.push(formatTrack(item)); trackUrisToLoad.push(item.uri); } else if (item.__model__ === 'Ref' && item.type === 'album') { - subdirectories.push(formatAlbum({ ...item, loading: true })); + subdirectories.push(formatAlbum(item)); subdirectoryImagesToLoad.push(item.uri); } else if (item.__model__ === 'Ref' && item.type === 'artist') { - subdirectories.push(formatArtist({ ...item, loading: true })); + subdirectories.push(formatArtist(item)); subdirectoryImagesToLoad.push(item.uri); } else if (item.__model__ === 'Ref' && item.type === 'playlist') { // Tidal moods and genres incorrectly marked as Playlist type @@ -1837,7 +1845,7 @@ const MopidyMiddleware = (function () { type: 'directory', }); } else { - subdirectories.push(formatPlaylist({ ...item, loading: true })); + subdirectories.push(formatPlaylist(item)); subdirectoryImagesToLoad.push(item.uri); playlistsToLoad.push(item.uri); } 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..cf1d916f1 100755 --- a/src/js/util/helpers.js +++ b/src/js/util/helpers.js @@ -603,6 +603,8 @@ const upgradeSpotifyPlaylistUri = function (uri) { return upgradeSpotifyPlaylistUris([uri])[0]; }; +const getSearchResultKey = ({ provider, type, term }) => [provider, type, term].join(':'); + export { debounce, throttle, @@ -626,6 +628,7 @@ export { upgradeSpotifyPlaylistUris, upgradeSpotifyPlaylistUri, iconFromKeyword, + getSearchResultKey, }; export default { @@ -651,4 +654,5 @@ export default { upgradeSpotifyPlaylistUris, upgradeSpotifyPlaylistUri, iconFromKeyword, + getSearchResultKey, }; diff --git a/src/js/util/selectors.js b/src/js/util/selectors.js index 03782f690..f6528a1a2 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/util/useSearchQuery.js b/src/js/util/useSearchQuery.js new file mode 100644 index 000000000..032df70a7 --- /dev/null +++ b/src/js/util/useSearchQuery.js @@ -0,0 +1,27 @@ +import { useParams } from 'react-router'; +import { useSelector } from 'react-redux'; + +const useSearchQuery = () => { + const { + term = '', + type = 'all', + providers: rawProviders = 'all', + } = useParams(); + const allProviders = useSelector( + ({ mopidy: { uri_schemes } }) => uri_schemes || [] + ).map((str) => str.replace(/:/g,'')); + const providers = rawProviders == 'all' + ? [...allProviders] + : rawProviders.split(',').filter((str) => allProviders.indexOf(str) > -1); + const providersString = providers.join(','); + + return { + term, + type, + providers, + allProviders, + providersString, + } +}; + +export default useSearchQuery; diff --git a/src/js/views/Discover/FeaturedPlaylists.js b/src/js/views/Discover/FeaturedPlaylists.js index 6f9f15d44..feeb431bd 100644 --- a/src/js/views/Discover/FeaturedPlaylists.js +++ b/src/js/views/Discover/FeaturedPlaylists.js @@ -197,8 +197,6 @@ class FeaturedPlaylists extends React.Component { }, ]; - console.debug({ source, view }) - const options = ( <> decodeUri(seed)); - console.debug({ uri, uriProp, seeds }) loadUris(seeds); this.setState( { seeds }, diff --git a/src/js/views/Search.js b/src/js/views/Search.js index a15ecea2d..fceed8052 100755 --- a/src/js/views/Search.js +++ b/src/js/views/Search.js @@ -14,43 +14,58 @@ import { } from '../services/ui/actions'; import { i18n } from '../locale'; import { getSortSelector } from '../util/selectors'; +import useSearchQuery from '../util/useSearchQuery'; const SORT_KEY = 'search_results'; const Search = () => { - const { term, type = 'all' } = useParams(); const dispatch = useDispatch(); const navigate = useNavigate(); - const lastQuery = useSelector((state) => state.core?.search_results?.query); + const { + term, + type, + providers, + allProviders, + providersString, + } = useSearchQuery(); const [sortField, sortReverse] = useSelector( (state) => getSortSelector(state, SORT_KEY, 'name'), ); useEffect(() => { dispatch(setWindowTitle('Search')); - $(document).find('.search-form input').focus(); + $(document).find('.search-form input').trigger('focus'); }, []); useEffect(() => { - if (term && type && term !== lastQuery?.term) { + if (term) { dispatch(setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) }))); - dispatch(startSearch({ term, type })); + dispatch(startSearch({ term, type, providers })); } - }, [term, type]) + }, [providersString, type, term]) - 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) => { + 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 +77,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/global/_core.scss b/src/scss/global/_core.scss index 0c8d14817..82e4ed1bb 100755 --- a/src/scss/global/_core.scss +++ b/src/scss/global/_core.scss @@ -741,7 +741,7 @@ footer { } h4 { - font-size: 1.3rem; + font-size: 1.4rem; } h5 { @@ -773,7 +773,7 @@ footer { } h4 { - font-size: 1.1rem; + font-size: 1.2rem; } h5 { diff --git a/src/scss/views/_artist.scss b/src/scss/views/_artist.scss index d771199e5..6a3238f48 100755 --- a/src/scss/views/_artist.scss +++ b/src/scss/views/_artist.scss @@ -123,7 +123,7 @@ .sub-views { margin-left: 5px; - padding-top: 10px; + padding-top: 30px; .option { margin: 0 8px; diff --git a/src/scss/views/_search.scss b/src/scss/views/_search.scss index 00e0e4ef2..51ea489c0 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(); @@ -81,6 +81,10 @@ } @include responsive($bp_medium) { + header { + margin-bottom: 0; + } + .search-form { top: 10px; left: 40px; @@ -97,7 +101,7 @@ } .search-result-sections { - padding: 10px 10px 0; + // padding: 10px 10px 0; section { width: auto;