From 1657073b7610e597af88817d7d7a47e5e20d0745 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Sun, 19 Feb 2023 18:46:44 +1300 Subject: [PATCH 01/14] Hiding outputs button completely - When snapcast is not enabled AND - When no controls saved - Supercedes PR #858 --- src/js/components/Fields/OutputControl.js | 31 ++++++++--------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/js/components/Fields/OutputControl.js b/src/js/components/Fields/OutputControl.js index 065c8eea3..1bee0bb26 100755 --- a/src/js/components/Fields/OutputControl.js +++ b/src/js/components/Fields/OutputControl.js @@ -1,15 +1,12 @@ import React, { useState, useEffect } from 'react'; -import { connect, useSelector, useDispatch } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { find, groupBy, map } from 'lodash'; +import { useSelector, useDispatch } from 'react-redux'; +import { find, groupBy, map, isEmpty } from 'lodash'; import VolumeControl from './VolumeControl'; import MuteControl from './MuteControl'; import Icon from '../Icon'; import Thumbnail from '../Thumbnail'; import LinksSentence from '../LinksSentence'; import DropdownField from './DropdownField'; -import * as coreActions from '../../services/core/actions'; -import * as mopidyActions from '../../services/mopidy/actions'; import * as pusherActions from '../../services/pusher/actions'; import * as snapcastActions from '../../services/snapcast/actions'; import { sortItems, indexToArray } from '../../util/arrays'; @@ -166,18 +163,10 @@ const Group = ({ }; const Outputs = () => { - const snapcastEnabled = useSelector((state) => state.snapcast.enabled); const allGroups = indexToArray(useSelector((state) => state.snapcast.groups || {})); const allStreams = useSelector((state) => state.snapcast.streams || {}); const allServers = indexToArray(useSelector((state) => state.mopidy.servers || {})); const groupsByStream = groupBy(allGroups, 'stream_id'); - if (!snapcastEnabled) { - return ( -

- -

- ); - } return ( @@ -199,12 +188,10 @@ const Outputs = () => { ); } -const Commands = () => { +const Commands = ({ commands }) => { const dispatch = useDispatch(); - const commandsObj = useSelector((state) => state.pusher.commands || {}); - if (!commandsObj) return null; - let items = indexToArray(commandsObj); + let items = indexToArray(commands); if (items.length <= 0) return null; items = sortItems(items, 'sort_order'); @@ -230,6 +217,8 @@ const Commands = () => { }; const OutputControl = ({ force_expanded }) => { + const snapcastEnabled = useSelector((state) => state.snapcast.enabled); + const commands = useSelector((state) => state.pusher.commands); const [expanded, setExpanded] = useState(false); useEffect(() => { @@ -238,9 +227,9 @@ const OutputControl = ({ force_expanded }) => { } }, [force_expanded]); + if (!snapcastEnabled && isEmpty(commands)) return null; + if (expanded) { - const outputs = ; - const commands = ; return ( {!force_expanded &&
setExpanded(false)} />} @@ -251,8 +240,8 @@ const OutputControl = ({ force_expanded }) => {
- {commands} - {outputs} + {!isEmpty(commands) && } + {snapcastEnabled && }
); From d88f9b56f570ce2740900643f63a1eb9e6cba5bc Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Sun, 5 Mar 2023 20:45:38 +1300 Subject: [PATCH 02/14] Route to manage current search state - By using route, it makes it more readily available for the varying component structure - I believe I handn't done this earlier to avoid refactoring, but it wasn't a complex task - Removing link borders, the browser-provided standard underline hover is now much more astheticly pleasing --- src/js/App.js | 2 +- src/js/components/SearchResults.js | 135 +++++------ src/js/views/Search.js | 354 +++++++---------------------- src/scss/components/_grid.scss | 4 + src/scss/components/_images.scss | 1 + src/scss/components/_sub-tabs.scss | 1 + src/scss/global/_core.scss | 4 +- src/scss/views/_search.scss | 2 +- 8 files changed, 147 insertions(+), 356 deletions(-) diff --git a/src/js/App.js b/src/js/App.js index 11842dd95..d097b168f 100755 --- a/src/js/App.js +++ b/src/js/App.js @@ -53,7 +53,7 @@ const Content = () => ( }/> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/js/components/SearchResults.js b/src/js/components/SearchResults.js index 663bcd5be..3043a2a32 100755 --- a/src/js/components/SearchResults.js +++ b/src/js/components/SearchResults.js @@ -1,5 +1,6 @@ import React from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; import { sortItems } from '../util/arrays'; import URILink from './URILink'; import Icon from './Icon'; @@ -11,38 +12,21 @@ import { makeSearchResultsSelector, getSortSelector } from '../util/selectors'; const SearchResults = ({ type, - query, - sortField, - sortReverse: sortReverseProp, - uri_schemes_priority, all, - results: rawResults, }) => { - const encodedTerm = encodeURIComponent(query.term); - let results = rawResults; - let sortReverse = sortReverseProp; - - if (!results) return null; - - let sort_map = null; - switch (sortField) { - case 'uri': - sort_map = uri_schemes_priority; - break; - case 'followers': - // Followers (aka popularlity works in reverse-numerical order) - // Ie "more popular" is a bigger number - sortReverse = !sortReverse; - break; - default: - break; - } + const { term } = useParams(); + const { sortField, sortReverse } = useSelector( + (state) => getSortSelector(state, 'search_results'), + ); + const searchResultsSelector = makeSearchResultsSelector(term, type); + const rawResults = useSelector(searchResultsSelector); + const encodedTerm = encodeURIComponent(term); + let results = [...rawResults]; results = sortItems( results, (type === 'tracks' && sortField === 'followers' ? 'popularity' : sortField), sortReverse, - sort_map, ); const resultsCount = results.length; @@ -50,7 +34,7 @@ const SearchResults = ({ results = results.slice(0, 6); } - if (results.length <= 0) return null; + if (all && !results.length) return null; return (
@@ -72,56 +56,59 @@ const SearchResults = ({ )} -
- {type === 'artists' && } - {type === 'albums' && } - {type === 'playlists' && } - {type === 'tracks' && ( - - )} - {/* */} + {results.length > 0 && ( +
+ {type === 'artists' && } + {type === 'albums' && } + {type === 'playlists' && } + {type === 'tracks' && ( + + )} + {/* */} - {resultsCount > results.length && ( - - )} -
+ {resultsCount > results.length && ( + + )} +
+ )}
); }; -const mapStateToProps = (state, ownProps) => { - const { - query: { - term, - }, - type, - } = ownProps; - const { - ui: { - uri_schemes_priority = [], - }, - } = state; - const searchResultsSelector = makeSearchResultsSelector(term, type); - const { sortField, sortReverse } = getSortSelector(state, 'search_results'); - - return { - results: searchResultsSelector(state), - uri_schemes_priority, - sortField, - sortReverse, - }; -}; - -const mapDispatchToProps = () => ({}); +const AllSearchResults = () => ( + <> +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+ + +); -export default connect(mapStateToProps, mapDispatchToProps)(SearchResults); +export { + SearchResults, + AllSearchResults, +} \ No newline at end of file diff --git a/src/js/views/Search.js b/src/js/views/Search.js index 76c429df3..c969cc4a5 100755 --- a/src/js/views/Search.js +++ b/src/js/views/Search.js @@ -1,294 +1,94 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; +import React, { useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; import Header from '../components/Header'; import Icon from '../components/Icon'; import DropdownField from '../components/Fields/DropdownField'; import SearchForm from '../components/Fields/SearchForm'; -import SearchResults from '../components/SearchResults'; -import * as coreActions from '../services/core/actions'; -import * as uiActions from '../services/ui/actions'; -import * as mopidyActions from '../services/mopidy/actions'; -import * as spotifyActions from '../services/spotify/actions'; -import { titleCase } from '../util/helpers'; -import { withRouter } from '../util'; +import { AllSearchResults, SearchResults } from '../components/SearchResults'; +import { startSearch } from '../services/core/actions'; +import { + set, + hideContextMenu, + setWindowTitle, +} from '../services/ui/actions'; import { i18n } from '../locale'; - -class Search extends React.Component { - constructor(props) { - super(props); - this.state = { term: props.term || '' }; - } - - componentDidMount = () => { - const { - uiActions: { - setWindowTitle, - }, - } = this.props; - - setWindowTitle('Search'); - - // Auto-focus on the input field +import { getSortSelector } from '../util/selectors'; + +const Search = () => { + const { term, type = 'all' } = useParams(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const lastQuery = useSelector((state) => state.core?.search_results?.query); + const { sortField, sortReverse } = useSelector( + (state) => getSortSelector(state, 'search_results'), + ); + + useEffect(() => { + dispatch(setWindowTitle('Search')); $(document).find('.search-form input').focus(); - this.digestUri(); - } + }, []); - componentDidUpdate = ({ term: prevTerm }) => { - const { term: termProp } = this.props; - if (prevTerm !== termProp) { - this.search(); + useEffect(() => { + if (term && type && term !== lastQuery?.term) { + dispatch(setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) }))); + dispatch(startSearch({ term, type })); } - } - - onSubmit = (term) => { - const { navigate, type } = this.props; - const encodedTerm = encodeURIComponent(term); + }, [term, type]) - this.setState( - { term }, - () => { - navigate(`/search/${type}/${encodedTerm}`); - }, - ); + const onSubmit = (nextTerm) => { + const encodedTerm = encodeURIComponent(nextTerm); + navigate(`/search/${type}/${encodedTerm}`); } - onReset = () => { - const { navigate } = this.props; - navigate('/search'); - } - - onSortChange = (value) => { - const { uiActions: { hideContextMenu } } = this.props; - this.setSort(value); - hideContextMenu(); - } + const onReset = () => navigate('/search'); - onSourceChange = (value) => { - const { - uiActions: { - set, - hideContextMenu, - }, - } = this.props; - set({ uri_schemes_search_enabled: value }); - hideContextMenu(); + const onSortChange = (value) => { + dispatch(set({ uri_schemes_search_enabled: value })); + dispatch(hideContextMenu()); } - onSourceClose = () => { - this.search(true); - }; - - digestUri = () => { - const { term } = this.props; - if (term) { - this.setState({ term }, this.search); - } else { - this.clearSearch(); - } - } - - clearSearch = () => { - const { - uiActions: { - setWindowTitle, - }, - } = this.props; - - setWindowTitle(i18n('search.title')); - this.setState({ term: '' }); - } - - search = (force = false) => { - const { - coreActions: { - startSearch, - }, - uiActions: { - setWindowTitle, - }, - search_results_query: { - type: existingType, - term: existingTerm, - }, - type, - } = this.props; - const { term } = this.state; - - setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) })); - - if ((type && term && (force || existingType !== type || existingTerm !== term))) { - startSearch({ type, term }); - } - } - - setSort = (value) => { - const { - sort, - sort_reverse, - uiActions: { - set, - }, - } = this.props; - - let reverse = false; - if (sort === value) reverse = !sort_reverse; - - const data = { - search_results_sort_reverse: reverse, - search_results_sort: value, - }; - set(data); - } - - render = () => { - const { term } = this.state; - const { - uri_schemes, - sort, - sort_reverse, - uri_schemes_search_enabled, - uiActions, - type, - } = this.props; - - const sort_options = [ - { value: 'followers', label: i18n('common.popularity') }, - { value: 'name', label: i18n('common.name') }, - { value: 'artist', label: i18n('common.artist') }, - { value: 'duration', label: i18n('common.duration') }, - ]; - - const provider_options = uri_schemes.map((item) => ({ - value: item, - label: titleCase(item.replace(':', '').replace('+', ' ')), - })); - - const options = ( - <> - - - - ); - - let searchResults; - - switch (type) { - case 'artists': - searchResults = ; - break; - case 'albums': - searchResults = ; - break; - case 'playlists': - searchResults = ; - break; - case 'tracks': - searchResults = - break; - default: - searchResults = ( - <> -
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
- - - ); - } - - return ( -
-
- -
- - - -
- {searchResults} -
+ const sortOptions = [ + { value: 'followers', label: i18n('common.popularity') }, + { value: 'name', label: i18n('common.name') }, + { value: 'artist', label: i18n('common.artist') }, + { value: 'duration', label: i18n('common.duration') }, + ]; + + const options = ( + + ); + + return ( +
+
+ +
+ + + +
+ {type != 'all' ? ( + + ) : ( + + )}
- ); - } +
+ ); } -const mapStateToProps = (state, ownProps) => { - const { - params: { - type, - term, - }, - navigation, - } = ownProps; - - const { - mopidy: { - uri_schemes = [], - }, - ui: { - uri_schemes_search_enabled = [], - search_results_sort: sort = 'followers.total', - search_results_sort_reverse, - }, - core: { - search_results: { - query: search_results_query = {}, - } = {}, - }, - } = state; - - return { - type: type || 'all', - term, - navigation, - uri_schemes, - uri_schemes_search_enabled, - sort, - sort_reverse: !!search_results_sort_reverse, - search_results_query, - }; -}; - -const mapDispatchToProps = (dispatch) => ({ - coreActions: bindActionCreators(coreActions, dispatch), - uiActions: bindActionCreators(uiActions, dispatch), - mopidyActions: bindActionCreators(mopidyActions, dispatch), - spotifyActions: bindActionCreators(spotifyActions, dispatch), -}); - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Search)); +export default Search; diff --git a/src/scss/components/_grid.scss b/src/scss/components/_grid.scss index ab544217a..b7d7d1f62 100755 --- a/src/scss/components/_grid.scss +++ b/src/scss/components/_grid.scss @@ -11,6 +11,10 @@ border-bottom: 0 !important; cursor: pointer; + a { + text-decoration: none !important; + } + &__wrapper { display: inline-block; } diff --git a/src/scss/components/_images.scss b/src/scss/components/_images.scss index 5046c3e46..00f179536 100755 --- a/src/scss/components/_images.scss +++ b/src/scss/components/_images.scss @@ -111,6 +111,7 @@ cursor: pointer; color: colour('white'); border: 0 !important; + text-decoration: none !important; margin: 0 5px; &:hover { diff --git a/src/scss/components/_sub-tabs.scss b/src/scss/components/_sub-tabs.scss index f9338a003..29d352823 100755 --- a/src/scss/components/_sub-tabs.scss +++ b/src/scss/components/_sub-tabs.scss @@ -18,6 +18,7 @@ display: block; box-sizing: border-box; border: none !important; + text-decoration: none !important; cursor: pointer; &__inner { diff --git a/src/scss/global/_core.scss b/src/scss/global/_core.scss index 58430cddf..95fe15cbb 100755 --- a/src/scss/global/_core.scss +++ b/src/scss/global/_core.scss @@ -195,10 +195,8 @@ main { cursor: pointer; &:not(.control):not(.action):not(.button) { - border-bottom: 1px solid transparent; - &:hover { - border-color: colour('mid_grey'); + text-decoration: underline; } } } diff --git a/src/scss/views/_search.scss b/src/scss/views/_search.scss index d8bfa5fc6..102be9f91 100755 --- a/src/scss/views/_search.scss +++ b/src/scss/views/_search.scss @@ -5,7 +5,7 @@ position: absolute; top: 30px; left: 90px; - right: 270px; + right: 170px; input { @include feature_font(); From d2ced56690b47cfa461e85295f595affde592874 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Sat, 1 Apr 2023 16:21:23 +1300 Subject: [PATCH 03/14] Context menu loading fixes - Padding to center loader - Correcting missing import for `Loader` --- src/js/components/ContextMenu/PlaylistSubmenu.js | 1 + src/scss/components/_context-menu.scss | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/components/ContextMenu/PlaylistSubmenu.js b/src/js/components/ContextMenu/PlaylistSubmenu.js index 10ba22c67..7aa6f43fa 100644 --- a/src/js/components/ContextMenu/PlaylistSubmenu.js +++ b/src/js/components/ContextMenu/PlaylistSubmenu.js @@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux'; import { compact } from 'lodash'; import { I18n } from '../../locale'; import Link from '../Link'; +import Loader from '../Loader'; import Icon from '../Icon'; import { encodeUri } from '../../util/format'; import { diff --git a/src/scss/components/_context-menu.scss b/src/scss/components/_context-menu.scss index 6aea7835b..d8a801e8d 100755 --- a/src/scss/components/_context-menu.scss +++ b/src/scss/components/_context-menu.scss @@ -106,7 +106,8 @@ } &--loader { - padding-top: 75%; + text-align: center; + margin-top: 50%; } } From 5ac3c6254aecfd84d7dbfd6ca0a37aee35b82498 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Sat, 1 Apr 2023 17:54:35 +1300 Subject: [PATCH 04/14] Using built-in link underline - Setting decoration-thickness for nicer control of appearance --- src/scss/components/_sub-tabs.scss | 4 ++-- src/scss/components/_sub-views.scss | 22 +--------------------- src/scss/global/_core.scss | 13 +++++-------- src/scss/global/_forms.scss | 4 ++++ src/scss/views/_artist.scss | 2 +- 5 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/scss/components/_sub-tabs.scss b/src/scss/components/_sub-tabs.scss index 29d352823..0ee8d748d 100755 --- a/src/scss/components/_sub-tabs.scss +++ b/src/scss/components/_sub-tabs.scss @@ -17,8 +17,6 @@ padding: 0 5px; display: block; box-sizing: border-box; - border: none !important; - text-decoration: none !important; cursor: pointer; &__inner { @@ -61,6 +59,8 @@ } &:hover { + text-decoration: none; + .menu-item__inner { background: lighten(colour('dark_grey'), 4%); diff --git a/src/scss/components/_sub-views.scss b/src/scss/components/_sub-views.scss index eb463cca4..2ac088a38 100755 --- a/src/scss/components/_sub-views.scss +++ b/src/scss/components/_sub-views.scss @@ -7,8 +7,6 @@ margin-right: 25px; font-size: 15px; font-weight: 500; - border-bottom: 0; - padding-bottom: 3px; cursor: pointer; @include theme('light') { @@ -21,25 +19,7 @@ &--active, &:hover { - border-bottom-width: 3px !important; - border-bottom-style: solid; - padding-bottom: 0px; - } - - &--active { - border-color: colour('white') !important; - - @include theme('light') { - border-color: colour('darkest_grey') !important; - } - } - - &:not(.sub-views__option--active):hover { - border-color: colour('soft_grey') !important; - - @include theme('light') { - border-color: colour('light_grey') !important; - } + text-decoration: underline 0.15em; } } diff --git a/src/scss/global/_core.scss b/src/scss/global/_core.scss index 95fe15cbb..03d478c8b 100755 --- a/src/scss/global/_core.scss +++ b/src/scss/global/_core.scss @@ -194,10 +194,8 @@ main { text-decoration: none; cursor: pointer; - &:not(.control):not(.action):not(.button) { - &:hover { - text-decoration: underline; - } + &:hover { + text-decoration: underline 0.15em; } } @@ -260,17 +258,16 @@ h2 { a { color: inherit; - text-decoration: none; + text-decoration: none 2px; &:hover { - border-bottom: 2px solid colour('white'); + text-decoration: underline 2px; } } &.grey-text { a:hover { color: colour('mid_grey') !important; - border-bottom: 2px solid colour('mid_grey'); } } } @@ -340,7 +337,7 @@ h5 { text-decoration: none; &:hover { - text-decoration: underline; + text-decoration: underline 0.15em; } } } diff --git a/src/scss/global/_forms.scss b/src/scss/global/_forms.scss index 1d01b908d..cd6885622 100755 --- a/src/scss/global/_forms.scss +++ b/src/scss/global/_forms.scss @@ -214,6 +214,10 @@ select { transform: translate(1px, 1px); } + &:hover { + text-decoration: none; + } + @include theme('light') { &--default { border-color: colour('darkest_grey'); diff --git a/src/scss/views/_artist.scss b/src/scss/views/_artist.scss index 97878906c..d771199e5 100755 --- a/src/scss/views/_artist.scss +++ b/src/scss/views/_artist.scss @@ -66,7 +66,7 @@ .biography-text { overflow-wrap: break-word; - white-space: pre-wrap; + white-space: pre-wrap; } } } From 60fc010ec83dbc92c047d5b83520cea0949924f3 Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Mon, 3 Apr 2023 10:50:02 +0200 Subject: [PATCH 05/14] Switch Dockerfile to use a released gst-plugins-rs and master mopidy-spotify branch --- Dockerfile | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index c883991f8..b4fa4c5e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,13 +32,11 @@ RUN apt update \ WORKDIR /usr/src/gst-plugins-rs # Clone source of gst-plugins-rs to workdir -ARG GST_PLUGINS_RS_TAG=main +ARG GST_PLUGINS_RS_TAG=0.10.5 RUN git clone -c advice.detachedHead=false \ --single-branch --depth 1 \ --branch ${GST_PLUGINS_RS_TAG} \ https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git ./ -# EXPERIMENTAL: For gstreamer-spotify set upgraded version number of dependency librespot to 0.4.2 -RUN sed -i 's/librespot = { version = "0.4", default-features = false }/librespot = { version = "0.4.2", default-features = false }/g' audio/spotify/Cargo.toml # Build GStreamer plugins written in Rust (optional with --no-default-features) ENV DEST_DIR /target/gst-plugins-rs @@ -99,9 +97,9 @@ RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - && \ # Install mopidy and (optional) DLNA-server dleyna from apt.mopidy.com # see https://docs.mopidy.com/en/latest/installation/debian/ -RUN mkdir -p /usr/local/share/keyrings \ - && wget -q -O /usr/local/share/keyrings/mopidy-archive-keyring.gpg https://apt.mopidy.com/mopidy.gpg \ - && wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list \ +RUN mkdir -p /etc/apt/keyrings \ + && wget -q -O /etc/apt/keyrings/mopidy-archive-keyring.gpg https://apt.mopidy.com/mopidy.gpg \ + && wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/bullseye.list \ && apt-get update \ && apt-get install -y \ mopidy \ @@ -133,7 +131,7 @@ RUN git clone --depth 1 --single-branch -b ${IRIS_VERSION} https://github.com/ja # Install mopidy-spotify-gstspotify (Hack, not released yet!) # (https://github.com/kingosticks/mopidy-spotify/tree/gstspotifysrc-hack) -RUN git clone --depth 1 -b gstspotifysrc-hack https://github.com/kingosticks/mopidy-spotify.git mopidy-spotify \ +RUN git clone --depth 1 https://github.com/mopidy/mopidy-spotify.git mopidy-spotify \ && cd mopidy-spotify \ && python3 setup.py install \ && cd .. \ From dcbfce4f8b7ad0e9510fa2583aaee474ea149dcf Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Mon, 3 Apr 2023 12:13:04 +0200 Subject: [PATCH 06/14] Add an Alpine Linux-based Docker image It gets all it's dependencies (Mopidy, Mopidy-Spotify) from the Alpine Linux repositories. For reference the image sizes: - Debian, 1.49GB - Alpine, 832MB --- Dockerfile.alpine | 81 ++++++++++++++++++++++++++++++++++++++++++++ docker/entrypoint.sh | 8 ++--- 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 Dockerfile.alpine diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 000000000..bc1db6ddc --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,81 @@ +# Use Alpine edge for now as some required dependencies are only in the testing repository +FROM alpine:edge + +# Switch to the root user while we do our changes +USER root +WORKDIR / + +# Install GStreamer and other required Debian packages +RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ + && apk update \ + && apk add \ + dumb-init \ + shadow \ + sudo \ + git \ + py3-pip \ + mopidy \ + py3-mopidy-spotify + +# Install Node, to build Iris JS application +RUN apk add nodejs npm + +# Upgrade Python package manager pip +# https://pypi.org/project/pip/ +RUN python3 -m pip install --upgrade pip + +# Clone Iris from the repository and install in development mode. +# This allows a binding at "/iris" to map to your local folder for development, rather than +# installing using pip. +# Note: ADD helps prevent RUN caching issues. When HEAD changes in repo, our cache will be invalidated! +ADD https://api.github.com/repos/jaedb/Iris/git/refs/heads/master version.json +ENV IRIS_VERSION=develop +RUN git clone --depth 1 --single-branch -b ${IRIS_VERSION} https://github.com/jaedb/Iris.git /iris \ + && cd /iris \ + && npm install \ + && npm run prod \ + && python3 setup.py develop \ + && mkdir -p /var/lib/mopidy/.config \ + && ln -s /config /var/lib/mopidy/.config/mopidy \ + # Allow mopidy user to run system commands (restart, local scan, etc) + && echo "mopidy ALL=NOPASSWD: /iris/mopidy_iris/system.sh" >> /etc/sudoers \ + # Enable container mode (disable restart option, etc.) + && echo "1" >> /IS_CONTAINER \ + # Copy Version file + && cp /iris/VERSION / + +# Install additional mopidy extensions and Python dependencies via pip +COPY docker/requirements.txt . +RUN python3 -m pip install -r requirements.txt + +# Cleanup +RUN rm -rf /root/.cache \ + && rm -rf /iris/node_modules + +# Start helper script. +COPY docker/entrypoint.sh /entrypoint.sh + +# Copy Default configuration for mopidy +COPY docker/mopidy/mopidy.example.conf /config/mopidy.conf + +# Copy the pulse-client configuratrion +COPY docker/mopidy/pulse-client.conf /etc/pulse/client.conf + +# Allows any user to run mopidy, but runs by default as a randomly generated UID/GID. +# RUN useradd -ms /bin/bash mopidy +ENV HOME=/var/lib/mopidy +RUN set -ex \ + && usermod -G audio,wheel mopidy \ + && mkdir /var/lib/mopidy/local \ + && chown mopidy:audio -R $HOME /entrypoint.sh /iris \ + && chmod go+rwx -R $HOME /entrypoint.sh /iris + +# Runs as mopidy user by default. +USER mopidy:audio + +VOLUME ["/var/lib/mopidy/local"] + +EXPOSE 6600 6680 1704 1705 5555/udp + +ENTRYPOINT ["/usr/bin/dumb-init", "/entrypoint.sh"] +CMD ["mopidy"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index aec103625..040367b93 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,14 +1,14 @@ -#!/bin/bash +#!/bin/sh if [ -z "$PULSE_COOKIE_DATA" ] then - echo -ne $(echo $PULSE_COOKIE_DATA | sed -e 's/../\\x&/g') >$HOME/pulse.cookie - export PULSE_COOKIE=$HOME/pulse.cookie + printf '%s' "$(echo "$PULSE_COOKIE_DATA" | sed -e 's/../\\x&/g')" >"$HOME"/pulse.cookie + export PULSE_COOKIE="$HOME"/pulse.cookie fi if [ ${PIP_PACKAGES:+x} ]; then echo "-- INSTALLING PIP PACKAGES $PIP_PACKAGES --" - python3 -m pip install --no-cache $PIP_PACKAGES + python3 -m pip install --no-cache "$PIP_PACKAGES" fi exec "$@" From 6fe2e9d5dee2840485a3a0dd4a7007043c871dd0 Mon Sep 17 00:00:00 2001 From: Bart Ribbers Date: Tue, 18 Apr 2023 15:32:43 +0200 Subject: [PATCH 07/14] feat(docker): also update the specified pip packages rather than just installing them Some pip packages may get updates and subsequent container runs wouldn't install them. Not so much of a problem normally but when for example yt-dlp is installed through pip to use with mopidy-youtube it's important to keep yt-dlp up-to-date to not have functionality be broken when Youtube updates their webpage again --- docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index aec103625..670c5450d 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -8,7 +8,7 @@ fi if [ ${PIP_PACKAGES:+x} ]; then echo "-- INSTALLING PIP PACKAGES $PIP_PACKAGES --" - python3 -m pip install --no-cache $PIP_PACKAGES + python3 -m pip install --no-cache --upgrade $PIP_PACKAGES fi exec "$@" From a8d622ffaa680e489b72a2e8d615eb584d8775d7 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Mon, 8 May 2023 19:27:39 +1200 Subject: [PATCH 08/14] Manifest update for installability - Scope needs to be within start path --- src/index.html | 2 +- src/manifest.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.html b/src/index.html index 6ca6d9164..1ed47b120 100755 --- a/src/index.html +++ b/src/index.html @@ -116,7 +116,7 @@