Merge branch 'develop'

This commit is contained in:
James Barnsley
2023-10-13 21:49:11 +13:00
27 changed files with 176 additions and 186 deletions

View File

@ -1 +1 @@
3.68.0
3.69.0

View File

@ -3,7 +3,7 @@ import pathlib
from mopidy import config, ext
__version__ = "3.68.0"
__version__ = "3.69.0"
logger = logging.getLogger(__name__)

View File

@ -25,7 +25,7 @@ class IrisSystemPermissionError(IrisSystemError):
class IrisSystemThread(Thread):
_USE_SUDO = True
_USE_SUDO = os.environ.get("IRIS_USE_SUDO", True)
def __init__(self, action, ioloop, callback):
Thread.__init__(self)

View File

@ -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

View File

@ -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 <james@barnsley.nz>",

View File

@ -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

View File

@ -54,7 +54,7 @@ const Content = () => (
<Route path="settings/debug" element={<Debug />} />
<Route path="settings/*" element={<Settings />} />
<Route path="search" element={<Search />} />
<Route path="search/:type/:term" element={<Search />} />
<Route path="search/:type/:providers/:term" element={<Search />} />
<Route path="artist/:uri/*" element={<Artist />} />
<Route path="album/:uri/" element={<Album />} />
<Route path="album/:uri/:name" element={<Album />} />

View File

@ -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 = ({
<h4>
{!all && (
<span>
<URILink uri={`iris:search:all:${encodedTerm}`} uriType="search" unencoded>
<URILink uri={`iris:search:all:${encodedProviders}:${encodedTerm}`} uriType="search" unencoded>
<I18n path="search.title" />
</URILink>
{' '}
<span style={{ display: 'inline-block', width: 10 }} />
<Icon type="fontawesome" name="angle-right" />
{' '}
<span style={{ display: 'inline-block', width: 10 }} />
<I18n path={`search.${type}.title`} />
</span>
)}
{all && (
<URILink uri={`iris:search:${type}:${encodedTerm}`} uriType="search" unencoded>
<URILink uri={`iris:search:${type}:${encodedProviders}:${encodedTerm}`} uriType="search" unencoded>
<I18n path={`search.${type}.title`} />
</URILink>
)}
@ -66,7 +68,7 @@ const SearchResults = ({
{type === 'tracks' && (
<TrackList
source={{
uri: `iris:search:${type}:${encodedTerm}`,
uri: `iris:search:${type}:${encodedProviders}:${encodedTerm}`,
name: 'Search results',
type: 'search',
}}
@ -77,7 +79,7 @@ const SearchResults = ({
{/* <LazyLoadListener enabled={this.props.artists_more && spotify_search_enabled} loadMore={loadMore} /> */}
{resultsCount > results.length && (
<Button uri={`iris:search:${type}:${encodedTerm}`} uriType="search" unencoded>
<Button uri={`iris:search:${type}:${encodedProviders}:${encodedTerm}`} uriType="search" unencoded>
<I18n path={`search.${type}.more`} count={resultsCount} />
</Button>
)}

View File

@ -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:

View File

@ -337,7 +337,7 @@ search:
title_window: 'Search: %{term}'
context_actions:
sort: Sort
source: 'Sources (%{count})'
source: Sources
placeholder: Search
all:
title: All

View File

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

View File

@ -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':

View File

@ -176,24 +176,15 @@ export default function reducer(core = {}, action) {
/**
* Search results
* */
case 'START_SEARCH':
case 'SEARCH_RESULTS_LOADED': {
return {
...core,
search_results: {
query: action.query,
artists: [],
albums: [],
playlists: [],
tracks: [],
...core?.search_results || {},
[action.key]: action.results,
},
};
case 'SEARCH_RESULTS_LOADED': {
const { search_results } = action;
return {
...core,
search_results,
};
}
default:

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

@ -38,6 +38,7 @@ let initialState = {
tracks: {},
items: {},
libraries: {},
search_results: {},
},
ui: {
language: 'en',

View File

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

View File

@ -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],

View File

@ -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;

View File

@ -197,8 +197,6 @@ class FeaturedPlaylists extends React.Component {
},
];
console.debug({ source, view })
const options = (
<>
<FilterField

View File

@ -223,7 +223,6 @@ class Recommendations extends React.Component {
if (!uri) return;
const seeds = uri.replace('iris:discover:', '').split(',').map((seed) => decodeUri(seed));
console.debug({ uri, uriProp, seeds })
loadUris(seeds);
this.setState(
{ seeds },

View File

@ -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 = (
<DropdownField
icon="swap_vert"
name={i18n('common.sort')}
value={sortField}
options={sortOptions}
selected_icon={sortField ? (sortReverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
handleChange={onSortChange}
valueAsLabel
/>
<>
<DropdownField
icon="swap_vert"
name={i18n('common.sort')}
value={sortField}
options={sortOptions}
selected_icon={sortField ? (sortReverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
handleChange={onSortChange}
valueAsLabel
/>
<DropdownField
icon="cloud"
name={i18n('search.context_actions.source')}
value={providers}
options={providerOptions}
handleChange={onProvidersChange}
/>
</>
);
return (

View File

@ -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 {

View File

@ -123,7 +123,7 @@
.sub-views {
margin-left: 5px;
padding-top: 10px;
padding-top: 30px;
.option {
margin: 0 8px;

View File

@ -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;