From 217a42e45dad16b2446cc367aef7c51a0848ac24 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 10:18:03 +0200 Subject: [PATCH 1/8] feat: Introduce Web App Settings --- src/jukebox/components/misc.py | 12 +++++++ src/webapp/src/App.js | 21 +++++++----- src/webapp/src/commands/index.js | 7 ++++ src/webapp/src/context/appsettings/context.js | 7 ++++ src/webapp/src/context/appsettings/index.js | 33 +++++++++++++++++++ 5 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 src/webapp/src/context/appsettings/context.js create mode 100644 src/webapp/src/context/appsettings/index.js diff --git a/src/jukebox/components/misc.py b/src/jukebox/components/misc.py index 9995509aa..28a983b0b 100644 --- a/src/jukebox/components/misc.py +++ b/src/jukebox/components/misc.py @@ -8,8 +8,10 @@ import jukebox.plugs as plugin import jukebox.utils from jukebox.daemon import get_jukebox_daemon +import jukebox.cfghandler logger = logging.getLogger('jb.misc') +cfg = jukebox.cfghandler.get_handler('jukebox') @plugin.register @@ -105,3 +107,13 @@ def empty_rpc_call(msg: str = ''): """ if msg: logger.warning(msg) + + +@plugin.register +def get_app_settings(): + """Return settings for web app stored in jukebox.yaml""" + show_covers = cfg.setndefault('webapp', 'show_covers', value=True) + + return { + 'show_covers': show_covers + } diff --git a/src/webapp/src/App.js b/src/webapp/src/App.js index 99272db64..a51529381 100644 --- a/src/webapp/src/App.js +++ b/src/webapp/src/App.js @@ -2,6 +2,7 @@ import React, { Suspense } from 'react'; import Grid from '@mui/material/Grid'; +import AppSettingsProvider from './context/appsettings'; import PubSubProvider from './context/pubsub'; import PlayerProvider from './context/player'; import Router from './router'; @@ -10,15 +11,17 @@ function App() { return ( - - - + + + + + ); diff --git a/src/webapp/src/commands/index.js b/src/webapp/src/commands/index.js index 8c844d8da..2d9ce7323 100644 --- a/src/webapp/src/commands/index.js +++ b/src/webapp/src/commands/index.js @@ -120,6 +120,7 @@ const commands = { _package: 'volume', plugin: 'ctrl', method: 'set_volume', + argKeys: ['volume'], }, getVolume: { _package: 'volume', @@ -250,6 +251,12 @@ const commands = { argKeys: ['option'], }, + // Misc + getAppSettings: { + _package: 'misc', + plugin: 'get_app_settings' + }, + // Synchronisation 'sync_rfidcards_all': { _package: 'sync_rfidcards', diff --git a/src/webapp/src/context/appsettings/context.js b/src/webapp/src/context/appsettings/context.js new file mode 100644 index 000000000..f2650d210 --- /dev/null +++ b/src/webapp/src/context/appsettings/context.js @@ -0,0 +1,7 @@ +import { createContext } from 'react'; + +const AppSettingsContext = createContext({ + showCovers: true, +}); + +export default AppSettingsContext; diff --git a/src/webapp/src/context/appsettings/index.js b/src/webapp/src/context/appsettings/index.js new file mode 100644 index 000000000..1fa34914d --- /dev/null +++ b/src/webapp/src/context/appsettings/index.js @@ -0,0 +1,33 @@ +import React, { useEffect, useState } from 'react'; + +import AppSettingsContext from './context'; +import request from '../../utils/request'; + +const AppSettingsProvider = ({ children }) => { + const [settings, setSettings] = useState({}); + + useEffect(() => { + const loadAppSettings = async () => { + const { result, error } = await request('getAppSettings'); + if(result) setSettings(result); + if(error) { + console.error('Error loading AppSettings'); + } + } + + loadAppSettings(); + }, []); + + const context = { + setSettings, + settings, + }; + + return( + + { children } + + ) +}; + +export default AppSettingsProvider; From 4c4230baccd5232828b28a5a8ef19b5b8bee7e29 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 10:18:49 +0200 Subject: [PATCH 2/8] feat: Allow to enable/disable Cover Art in Web App --- .../default-settings/jukebox.default.yaml | 4 ++++ .../albums/album-list/album-list-item.js | 21 ++++++++++++++----- src/webapp/src/components/Player/index.js | 9 +++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/resources/default-settings/jukebox.default.yaml b/resources/default-settings/jukebox.default.yaml index b8e429333..9bb214f3d 100644 --- a/resources/default-settings/jukebox.default.yaml +++ b/resources/default-settings/jukebox.default.yaml @@ -153,3 +153,7 @@ sync_rfidcards: config_file: ../../shared/settings/sync_rfidcards.yaml webapp: coverart_cache_path: ../../src/webapp/build/cover-cache + # Load cover arts in Webapp. Change to false in case you have performance issue + # when handling a lot of music + # Defaults to true + show_covers: true diff --git a/src/webapp/src/components/Library/lists/albums/album-list/album-list-item.js b/src/webapp/src/components/Library/lists/albums/album-list/album-list-item.js index 2c6d99180..71f6ba315 100644 --- a/src/webapp/src/components/Library/lists/albums/album-list/album-list-item.js +++ b/src/webapp/src/components/Library/lists/albums/album-list/album-list-item.js @@ -1,4 +1,4 @@ -import React, { forwardRef, useEffect, useState } from 'react'; +import React, { forwardRef, useContext, useEffect, useState } from 'react'; import { Link, useLocation, @@ -15,6 +15,7 @@ import { import noCover from '../../../../../assets/noCover.jpg'; +import AppSettingsContext from '../../../../../context/appsettings/context'; import request from '../../../../../utils/request'; const AlbumListItem = ({ albumartist, album, isButton = true }) => { @@ -22,6 +23,14 @@ const AlbumListItem = ({ albumartist, album, isButton = true }) => { const { search: urlSearch } = useLocation(); const [coverImage, setCoverImage] = useState(noCover); + const { + settings, + } = useContext(AppSettingsContext); + + const { + show_covers, + } = settings; + useEffect(() => { const getCoverArt = async () => { const { result } = await request('getAlbumCoverArt', { @@ -35,7 +44,7 @@ const AlbumListItem = ({ albumartist, album, isButton = true }) => { }; } - if (albumartist && album) { + if (albumartist && album && show_covers) { getCoverArt(); } }, [albumartist, album]); @@ -61,9 +70,11 @@ const AlbumListItem = ({ albumartist, album, isButton = true }) => { key={album} > - - - + {show_covers && + + + + } { const [coverImage, setCoverImage] = useState(undefined); const [backgroundImage, setBackgroundImage] = useState('none'); + const { + settings, + } = useContext(AppSettingsContext); + + const { show_covers } = settings; + useEffect(() => { const getCoverArt = async () => { const { result } = await request('getSingleCoverArt', { song_url: file }); @@ -30,7 +37,7 @@ const Player = () => { }; } - if (file) { + if (file && show_covers) { getCoverArt(); } }, [file]); From 0c9718aa2f64976b2ce4fb5417b1589e99b36452 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:21:17 +0200 Subject: [PATCH 3/8] fix: handle Falsy mimetype and data even when APIC tag has been found my mutagen --- src/jukebox/components/playermpd/coverart_cache_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jukebox/components/playermpd/coverart_cache_manager.py b/src/jukebox/components/playermpd/coverart_cache_manager.py index bb2346497..dbf95883b 100644 --- a/src/jukebox/components/playermpd/coverart_cache_manager.py +++ b/src/jukebox/components/playermpd/coverart_cache_manager.py @@ -67,9 +67,9 @@ def _extract_album_art(self, mp3_file_path: str) -> tuple: for tag in audio_file.tags.values(): if isinstance(tag, APIC): - mime_type = tag.mime - file_extension = 'jpg' if mime_type == 'image/jpeg' else mime_type.split('/')[-1] - return (file_extension, tag.data) + if tag.mime and tag.data: + file_extension = 'jpg' if tag.mime == 'image/jpeg' else tag.mime.split('/')[-1] + return (file_extension, tag.data) return (NO_COVER_ART_EXTENSION, b'') From 614b56ae618610f8256a4abee04fc5af50045883 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:51:38 +0200 Subject: [PATCH 4/8] feat: Try to load cover from filesystem when not found in audio file --- .../playermpd/coverart_cache_manager.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/jukebox/components/playermpd/coverart_cache_manager.py b/src/jukebox/components/playermpd/coverart_cache_manager.py index dbf95883b..7cad1a513 100644 --- a/src/jukebox/components/playermpd/coverart_cache_manager.py +++ b/src/jukebox/components/playermpd/coverart_cache_manager.py @@ -47,7 +47,10 @@ def save_to_cache(self, mp3_file_path: str): def _save_to_cache(self, mp3_file_path: str): base_filename = Path(mp3_file_path).stem cache_key = self.generate_cache_key(base_filename) + file_extension, data = self._extract_album_art(mp3_file_path) + if file_extension == NO_COVER_ART_EXTENSION: # Check if cover has been added as separate file in folder + file_extension, data = self._get_from_filesystem(mp3_file_path) cache_filename = f"{cache_key}.{file_extension}" full_path = self.cache_folder_path / cache_filename # Works due to Pathlib @@ -73,6 +76,20 @@ def _extract_album_art(self, mp3_file_path: str) -> tuple: return (NO_COVER_ART_EXTENSION, b'') + def _get_from_filesystem(self, mp3_file_path: str) -> tuple: + path = Path(mp3_file_path) + directory = path.parent + cover_files = list(directory.glob('Cover.*')) + list(directory.glob('cover.*')) + + for file in cover_files: + if file.suffix.lower() in ['.jpg', '.jpeg', '.png']: + with file.open('rb') as img_file: + data = img_file.read() + file_extension = file.suffix[1:] # Get extension without dot + return (file_extension, data) + + return (NO_COVER_ART_EXTENSION, b'') + def process_write_requests(self): while True: mp3_file_path = self.write_queue.get() From 609eaaff9e54d3b40436027db36f8b2c995b8a5d Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 13:26:15 +0200 Subject: [PATCH 5/8] fix: flake8 linting error --- src/jukebox/components/playermpd/coverart_cache_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jukebox/components/playermpd/coverart_cache_manager.py b/src/jukebox/components/playermpd/coverart_cache_manager.py index 7cad1a513..f292a2bbe 100644 --- a/src/jukebox/components/playermpd/coverart_cache_manager.py +++ b/src/jukebox/components/playermpd/coverart_cache_manager.py @@ -49,7 +49,7 @@ def _save_to_cache(self, mp3_file_path: str): cache_key = self.generate_cache_key(base_filename) file_extension, data = self._extract_album_art(mp3_file_path) - if file_extension == NO_COVER_ART_EXTENSION: # Check if cover has been added as separate file in folder + if file_extension == NO_COVER_ART_EXTENSION: # Check if cover has been added as separate file in folder file_extension, data = self._get_from_filesystem(mp3_file_path) cache_filename = f"{cache_key}.{file_extension}" From 954c259903ec6939fddb40b9ae325f36442336a1 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 14:09:52 +0200 Subject: [PATCH 6/8] docs: Add documentation for Cover Art --- documentation/builders/README.md | 2 ++ documentation/builders/webapp/cover-art.md | 37 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 documentation/builders/webapp/cover-art.md diff --git a/documentation/builders/README.md b/documentation/builders/README.md index 512d26ed0..29733e92b 100644 --- a/documentation/builders/README.md +++ b/documentation/builders/README.md @@ -30,6 +30,8 @@ ## Web Application +* Application + * [Cover Art](./webapp/cover-art.md) * Music * [Playlists, Livestreams and Podcasts](./webapp/playlists-livestreams-podcasts.md) diff --git a/documentation/builders/webapp/cover-art.md b/documentation/builders/webapp/cover-art.md new file mode 100644 index 000000000..09bae3a30 --- /dev/null +++ b/documentation/builders/webapp/cover-art.md @@ -0,0 +1,37 @@ +# Cover Art + +## Enable/Disable Cover Art + +The Web App automatically searches for cover art for albums and songs. If it finds cover art, it displays it; if not, it shows a placeholder image. However, you may prefer to disable cover art (e.g. in situations where device performance is low; screen space is limited; etc). There are two ways to do this: + +1. **Web App Settings**: Go to the "Settings" tab. Under the "General" section, find and toggle the "Show Cover Art" option. +1. **Configuration File**: Open the `jukebox.yaml` file. Navigate to `webapp` -> `show_covers`. Set this value to `true` to enable or `false` to disable cover art display. If this option does not exist, it assumes `true` as a default. + +## Providing Additional Cover Art + +Cover art can be provided in two ways: 1) embedded within the audio file itself, or 2) as a separate image file in the same directory as the audio file. The software searches for cover art in the order listed. + +To add cover art using the file system, place a file named `cover.jpg` in the same folder as your audio file or album. Accepted image file types are `jpg` and `png`. + +### Example + +Suppose none of your files currently include embedded cover art, the example below demonstrates how to enable cover art for an entire folder, applying the same cover art to all files within that folder. + +> [!IMPORTANT] +> You cannot assign different cover arts to different tracks within the same folder. + +#### Example Folder Structure + +```text +└── audiofolders + ├── Simone Sommerland + │ ├── 01 Aramsamsam.mp3 + │ ├── 02 Das Rote Pferd.mp3 + │ ├── 03 Hoch am Himmel.mp3 + │ └── cover.jpg <- Cover Art file as JPG + └── Bibi und Tina + ├── 01 Bibi und Tina Song.mp3 + ├── 02 Alles geht.mp3 + ├── 03 Solange dein Herz spricht.mp3 + └── cover.png <- Cover Art file as PNG +``` From a534aa31ed1e8b9b9864389b7ce0396133e584f0 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 15:10:15 +0200 Subject: [PATCH 7/8] feat: Allow show_covers setting to be managed in Web App --- src/jukebox/components/misc.py | 6 ++ src/webapp/public/locales/de/translation.json | 6 ++ src/webapp/public/locales/en/translation.json | 6 ++ src/webapp/src/commands/index.js | 6 ++ .../src/components/Settings/general/index.js | 39 +++++++++++++ .../Settings/general/show-covers.js | 56 +++++++++++++++++++ src/webapp/src/components/Settings/index.js | 6 +- 7 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/webapp/src/components/Settings/general/index.js create mode 100644 src/webapp/src/components/Settings/general/show-covers.js diff --git a/src/jukebox/components/misc.py b/src/jukebox/components/misc.py index 28a983b0b..80efe6393 100644 --- a/src/jukebox/components/misc.py +++ b/src/jukebox/components/misc.py @@ -117,3 +117,9 @@ def get_app_settings(): return { 'show_covers': show_covers } + +@plugin.register +def set_app_settings(settings = {}): + """Set configuration settings for the web app.""" + for key, value in settings.items(): + cfg.setn('webapp', key, value=value) diff --git a/src/webapp/public/locales/de/translation.json b/src/webapp/public/locales/de/translation.json index d1a4391d6..7dbdcf695 100644 --- a/src/webapp/public/locales/de/translation.json +++ b/src/webapp/public/locales/de/translation.json @@ -219,6 +219,12 @@ "why": "Warum?", "control-label": "Auto Hotspot" }, + "general": { + "title": "Allgmeine Einstellungen", + "show_covers": { + "title": "Cover anzeigen" + } + }, "timers": { "option-label-timeslot": "{{value}} min", "option-label-off": "Aus", diff --git a/src/webapp/public/locales/en/translation.json b/src/webapp/public/locales/en/translation.json index 74fd9a696..7ff66ecc4 100644 --- a/src/webapp/public/locales/en/translation.json +++ b/src/webapp/public/locales/en/translation.json @@ -219,6 +219,12 @@ "why": "Why?", "control-label": "Auto Hotspot" }, + "general": { + "title": "General Settings", + "show_covers": { + "title": "Show Cover Art" + } + }, "timers": { "option-label-timeslot": "{{value}} min", "option-label-off": "Off", diff --git a/src/webapp/src/commands/index.js b/src/webapp/src/commands/index.js index 2d9ce7323..f6f772875 100644 --- a/src/webapp/src/commands/index.js +++ b/src/webapp/src/commands/index.js @@ -257,6 +257,12 @@ const commands = { plugin: 'get_app_settings' }, + setAppSettings: { + _package: 'misc', + plugin: 'set_app_settings', + argKeys: ['settings'], + }, + // Synchronisation 'sync_rfidcards_all': { _package: 'sync_rfidcards', diff --git a/src/webapp/src/components/Settings/general/index.js b/src/webapp/src/components/Settings/general/index.js new file mode 100644 index 000000000..790043778 --- /dev/null +++ b/src/webapp/src/components/Settings/general/index.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useTheme } from '@mui/material/styles'; + +import { + Card, + CardContent, + CardHeader, + Divider, + Grid, +} from '@mui/material'; +import ShowCovers from './show-covers'; + +const SettingsGeneral = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const spacer = { marginBottom: theme.spacing(2) } + + return ( + + + + + .MuiGrid-root:not(:last-child)': spacer }} + > + + + + + ); +}; + +export default SettingsGeneral; diff --git a/src/webapp/src/components/Settings/general/show-covers.js b/src/webapp/src/components/Settings/general/show-covers.js new file mode 100644 index 000000000..a3b31f4e0 --- /dev/null +++ b/src/webapp/src/components/Settings/general/show-covers.js @@ -0,0 +1,56 @@ +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + Box, + Grid, + Switch, + Typography, +} from '@mui/material'; + +import AppSettingsContext from '../../../context/appsettings/context'; +import request from '../../../utils/request'; + +const ShowCovers = () => { + const { t } = useTranslation(); + + const { + settings, + setSettings, + } = useContext(AppSettingsContext); + + const { + show_covers, + } = settings; + + const updateShowCoversSetting = async (show_covers) => { + await request('setAppSettings', { settings: { show_covers }}); + } + + const handleSwitch = (event) => { + setSettings({ show_covers: event.target.checked}); + updateShowCoversSetting(event.target.checked); + } + + return ( + + + + {t(`settings.general.show_covers.title`)} + + + + + + + ); +}; + +export default ShowCovers; diff --git a/src/webapp/src/components/Settings/index.js b/src/webapp/src/components/Settings/index.js index 1bc599fc1..75ce7840f 100644 --- a/src/webapp/src/components/Settings/index.js +++ b/src/webapp/src/components/Settings/index.js @@ -2,9 +2,10 @@ import React from 'react'; import { Grid } from '@mui/material'; +import SettingsAudio from './audio/index'; import SettingsAutoHotspot from './autohotspot'; +import SettingsGeneral from './general'; import SettingsSecondSwipe from './secondswipe'; -import SettingsAudio from './audio/index'; import SettingsStatus from './status/index'; import SettingsTimers from './timers/index'; import SystemControls from './systemcontrols'; @@ -28,6 +29,9 @@ const Settings = () => { + + + From 7ff4b2341b248bb7508adbec82e35f8cb980b5d2 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 21 Apr 2024 15:14:27 +0200 Subject: [PATCH 8/8] fix: again flake8 linting errors --- src/jukebox/components/misc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jukebox/components/misc.py b/src/jukebox/components/misc.py index 80efe6393..2cc260d79 100644 --- a/src/jukebox/components/misc.py +++ b/src/jukebox/components/misc.py @@ -118,8 +118,9 @@ def get_app_settings(): 'show_covers': show_covers } + @plugin.register -def set_app_settings(settings = {}): +def set_app_settings(settings={}): """Set configuration settings for the web app.""" for key, value in settings.items(): cfg.setn('webapp', key, value=value)