diff --git a/.flake8 b/.flake8 index 7f201b078..6f7bcd443 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,8 @@ [flake8] max-line-length = 127 ignore = + # continuation line over-indented for hanging indent + E126, # continuation line over-indented for visual indent E127, # continuation line under-indented for visual indent diff --git a/.githooks/pre-commit b/.githooks/pre-commit index b1b0c0348..8ffc7de0c 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -9,6 +9,9 @@ # Note: This only checks the modified files # - docs build of if any python file is staged # Note: This builds the entire documentation if a changed file goes into the documentation +# - Markdownlint if any markdown file is staged +# Note: This checks all markdown files as configured in .markdownlint-cli2.yaml + # # If there are problem with this script, commit may still be done with # git commit --no-verify @@ -40,6 +43,20 @@ fi code=$(( flake8_code + doc_code )) +# Pass all staged markdown files through markdownlint-cli2 +MD_FILES="$(git diff --diff-filter=d --staged --name-only -- **/*.md)" +markdownlint_code=0 +if [[ -n $MD_FILES ]]; then + echo -e "\n**************************************************************" + echo "Modified Markdown files. Running markdownlint-cli2 ... " + echo -e "**************************************************************\n" + ./run_markdownlint.sh + markdownlint_code=$? + echo "Markdownlint-cli2 return code: $markdownlint_code" +fi + +code=$(( flake8_code + doc_code + markdownlint_code)) + if [[ code -gt 0 ]]; then echo -e "\n**************************************************************" echo -e "ERROR(s) during pre-commit checks. Aborting commit!" diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md index 508cf50b9..afcb9bd0c 100644 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -33,7 +33,6 @@ Please post here the output of 'tail -n 500 /var/log/syslog' or 'journalctl -u m i.e. `find logfiles at https://paste.ubuntu.com/p/cRS7qM8ZmP/` --> - ## Software ### Base image and version @@ -59,7 +58,6 @@ the following command will help with that i.e. `scripts/installscripts/buster-install-default.sh` --> - ## Hardware ### RaspberryPi version diff --git a/.github/workflows/bundle_webapp_and_release_v3.yml b/.github/workflows/bundle_webapp_and_release_v3.yml index 13bffe472..a64d6e288 100644 --- a/.github/workflows/bundle_webapp_and_release_v3.yml +++ b/.github/workflows/bundle_webapp_and_release_v3.yml @@ -101,7 +101,7 @@ jobs: tar -czvf ${{ steps.vars.outputs.webapp_bundle_name }} build - name: Artifact Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ steps.vars.outputs.webapp_bundle_name }} path: ${{ steps.build-webapp.outputs.webapp-root-path }}/${{ steps.vars.outputs.webapp_bundle_name }} @@ -119,7 +119,7 @@ jobs: steps: - name: Artifact Download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ needs.build.outputs.webapp_bundle_name }} diff --git a/.github/workflows/codeql-analysis_v3.yml b/.github/workflows/codeql-analysis_v3.yml index 89693df2a..a284d7691 100644 --- a/.github/workflows/codeql-analysis_v3.yml +++ b/.github/workflows/codeql-analysis_v3.yml @@ -38,25 +38,26 @@ jobs: uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | # Install necessary packages + sudo apt-get update sudo apt-get install libasound2-dev pulseaudio python3 -m venv .venv source ".venv/bin/activate" python -m pip install --upgrade pip pip install -r requirements.txt - # Set the `CODEQL-PYTHON` environment variable to the Python executable + # Set the `CODEQL_EXTRACTOR_PYTHON_ANALYSIS_VERSION` environment variable to the Python executable # that includes the dependencies - echo "CODEQL_PYTHON=$(which python)" >> $GITHUB_ENV + echo "CODEQL_EXTRACTOR_PYTHON_ANALYSIS_VERSION=$(which python)" >> $GITHUB_ENV # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,7 +69,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -82,4 +83,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/markdown_v3.yml b/.github/workflows/markdown_v3.yml new file mode 100644 index 000000000..38284038e --- /dev/null +++ b/.github/workflows/markdown_v3.yml @@ -0,0 +1,28 @@ +name: Markdown Linting + +on: + push: + branches: + - 'future3/**' + paths: + - '**.md' + pull_request: + branches: + - 'future3/**' + paths: + - '**.md' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Linting markdown + uses: DavidAnson/markdownlint-cli2-action@v15 + with: + config: .markdownlint-cli2.yaml + #continue-on-error: true diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml index 2236a5a47..3bc2c1174 100644 --- a/.github/workflows/pythonpackage_future3.yml +++ b/.github/workflows/pythonpackage_future3.yml @@ -19,12 +19,12 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/test_docker_debian_codename_sub_v3.yml b/.github/workflows/test_docker_debian_codename_sub_v3.yml index a9ec217dc..e3ef02bbe 100644 --- a/.github/workflows/test_docker_debian_codename_sub_v3.yml +++ b/.github/workflows/test_docker_debian_codename_sub_v3.yml @@ -134,11 +134,11 @@ jobs: BASE_TEST_IMAGE=${{ steps.vars.outputs.image_tag_name_local_base }} - name: Artifact Upload Docker Image - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ steps.vars.outputs.image_file_name }} path: ${{ steps.vars.outputs.image_file_path }} - retention-days: 1 + retention-days: 2 # Run tests with build image @@ -159,7 +159,7 @@ jobs: uses: docker/setup-buildx-action@v3.0.0 - name: Artifact Download Docker Image - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ needs.build.outputs.image_file_name }} @@ -179,13 +179,14 @@ jobs: # cleanup after test execution cleanup: - # run only if tests didn't fail: keep the artifact to make job reruns possible - if: ${{ !failure() }} - needs: [build, test] - runs-on: ${{ inputs.runs_on }} - - steps: - - name: Artifact Delete Docker Image - uses: geekyeggo/delete-artifact@v2 - with: - name: ${{ needs.build.outputs.image_file_name }} + # run only if tests didn't fail: keep the artifact to make job reruns possible + if: ${{ !failure() }} + needs: [build, test] + runs-on: ${{ inputs.runs_on }} + + steps: + - name: Artifact Delete Docker Image + uses: geekyeggo/delete-artifact@v5 + with: + name: ${{ needs.build.outputs.image_file_name }} + failOnError: false diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 000000000..88c67e576 --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,57 @@ +# +# markdownlint-cli2 configuration, see https://github.com/DavidAnson/markdownlint-cli2?tab=readme-ov-file#configuration +# + +# rules, see https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md +config: + line-length: false + # ignore dollar signs + commands-show-output: false + no-trailing-punctuation: false + no-duplicate-heading: + siblings_only: true + # allow some tags we use for formatting + no-inline-html: + allowed_elements: [ "details", "summary" ] + +# Include a custom rule package +#customRules: +# - markdownlint-rule-titlecase + +# Fix no fixable errors +fix: false + +# Define a custom front matter pattern +#frontMatter: "[^]*<\/head>" + +# Define glob expressions to use (only valid at root) +globs: + - "**.md" + +# Define glob expressions to ignore +ignores: + - "documentation/developers/docstring/*" + - "src/**" + +# Use a plugin to recognize math +#markdownItPlugins: +# - +# - "@iktakahiro/markdown-it-katex" + +# Additional paths to resolve module locations from +#modulePaths: +# - "./modules" + +# Enable inline config comments +noInlineConfig: false + +# Disable progress on stdout (only valid at root) +noProgress: true + +# Use a specific formatter (only valid at root) +#outputFormatters: +# - +# - markdownlint-cli2-formatter-default + +# Show found files on stdout (only valid at root) +showFound: true diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 332baee88..a28c343f0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,14 +1,14 @@ +# Contributor Covenant Code of Conduct -Dear Phonieboxians, - -As the Phoniebox community is growing, somebody suggested a pull request with the below document. I was hesitant to include it right away, but at the same time I thought: it might be good to have some kind of document to formulate the foundation this project is built on. To tell you the truth, this document is not it. However, it is a start and I thought: why not open this in the spirit of open source, sharing and pull requests and see if and how you or you or you want to change or add parts of this very *standard and corporate* document. Like most of you, I also have a small kid and my time is scarce, I might find some time though to add a bit. - -All the best, Micz +> [!NOTE] +> Dear Phonieboxians, +> +> As the Phoniebox community is growing, somebody suggested a pull request with the below document. I was hesitant to include it right away, but at the same time I thought: it might be good to have some kind of document to formulate the foundation this project is built on. To tell you the truth, this document is not it. However, it is a start and I thought: why not open this in the spirit of open source, sharing and pull requests and see if and how you or you or you want to change or add parts of this very *standard and corporate* document. Like most of you, I also have a small kid and my time is scarce, I might find some time though to add a bit. +> +> All the best, Micz 2018-08-21 -# Contributor Covenant Code of Conduct - This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] ## Our Pledge diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4ac9cd16..feb47f2c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,15 +45,16 @@ as local, temporary scratch areas. Contributors have played a bigger role over time to keep Phoniebox on the edge of innovation :) -Our goal is to make it simple for you to contribute changes that improve functionality in your specific environment. -To achieve this, we have a set of guidelines that we kindly request contributors to adhere to. +Our goal is to make it simple for you to contribute changes that improve functionality in your specific environment. +To achieve this, we have a set of guidelines that we kindly request contributors to adhere to. These guidelines help us maintain a streamlined process and stay on top of incoming contributions. To report bug fixes and improvements, please follow the steps outlined below: + 1. For bug fixes and minor improvements, simply open a new issue or pull request (PR). 2. If you intend to port a feature from Version 2.x to future3 or wish to implement a new feature, we recommend reaching out to us beforehand. - - In such cases, please create an issue outlining your plans and intentions. - - We will ensure that there are no ongoing efforts on the same topic. + * In such cases, please create an issue outlining your plans and intentions. + * We will ensure that there are no ongoing efforts on the same topic. We eagerly await your contributions! You can review the current [feature list](documentation/developers/status.md) to check for available features and ongoing work. @@ -108,7 +109,7 @@ Run the checks below on the code. Fix those issues! Or you are running in delays We provide git hooks for those checks for convenience. To activate ~~~bash -cp .githooks/pre-commit` .git/hooks/. +cp .githooks/pre-commit .git/hooks/. ~~~ ### Python Code @@ -152,7 +153,7 @@ to detect in advance. If the code change results in a test failure, we will make our best effort to correct the error. If a fix cannot be determined and committed within 24 hours -of its discovery, the commit(s) responsible _may_ be reverted, at the +of its discovery, the commit(s) responsible *may* be reverted, at the discretion of the committer and Phonie maintainers. The original contributor will be notified of the revert. @@ -163,7 +164,7 @@ The original contributor will be notified of the revert. ## Guidelines -* Phoniebox runs on Raspberry Pi OS. +* Phoniebox runs on Raspberry Pi OS. * Minimum python version is currently **Python 3.9**. ## Additional Resources diff --git a/docker/jukebox.Dockerfile b/docker/Dockerfile.jukebox similarity index 67% rename from docker/jukebox.Dockerfile rename to docker/Dockerfile.jukebox index 0936f5d46..3d63a4a02 100644 --- a/docker/jukebox.Dockerfile +++ b/docker/Dockerfile.jukebox @@ -1,3 +1,4 @@ +FROM libzmq:local as libzmq FROM debian:bullseye-slim # These are only dependencies that are required to get as close to the @@ -6,8 +7,7 @@ RUN apt-get update && apt-get install -y \ libasound2-dev \ pulseaudio \ pulseaudio-utils \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* + --no-install-recommends ARG UID ARG USER @@ -21,7 +21,7 @@ RUN usermod -aG pulse ${USER} # Install all Jukebox dependencies RUN apt-get update && apt-get install -qq -y \ --allow-downgrades --allow-remove-essential --allow-change-held-packages \ - g++ at wget \ + build-essential at wget \ espeak mpc mpg123 git ffmpeg spi-tools netcat \ python3 python3-venv python3-dev python3-mutagen @@ -37,21 +37,14 @@ ENV VIRTUAL_ENV=${INSTALLATION_PATH}/.venv RUN python3 -m venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" - +# Install all Python dependencies RUN pip install --no-cache-dir -r ${INSTALLATION_PATH}/requirements.txt -ENV ZMQ_TMP_DIR libzmq -ENV ZMQ_VERSION 4.3.5 -ENV ZMQ_PREFIX /usr/local - -RUN [ "$(uname -m)" = "aarch64" ] && ARCH="arm64" || ARCH="$(uname -m)"; \ - wget https://github.com/pabera/libzmq/releases/download/v${ZMQ_VERSION}/libzmq5-${ARCH}-${ZMQ_VERSION}.tar.gz -O libzmq.tar.gz; \ - tar -xzf libzmq.tar.gz -C ${ZMQ_PREFIX}; \ - rm -f libzmq.tar.gz; - -RUN export ZMQ_PREFIX=${PREFIX} && export ZMQ_DRAFT_API=1 -RUN pip install -v --no-binary pyzmq pyzmq +# Install pyzmq Python dependency separately +ENV ZMQ_PREFIX /opt/libzmq +ENV ZMQ_DRAFT_API 1 +COPY --from=libzmq ${ZMQ_PREFIX} ${ZMQ_PREFIX} +RUN pip install -v pyzmq --no-binary pyzmq EXPOSE 5555 5556 - WORKDIR ${INSTALLATION_PATH}/src/jukebox diff --git a/docker/Dockerfile.libzmq b/docker/Dockerfile.libzmq new file mode 100644 index 000000000..78cf52368 --- /dev/null +++ b/docker/Dockerfile.libzmq @@ -0,0 +1,25 @@ +FROM debian:bullseye-slim + +# Install necessary build dependencies +RUN apt-get update && apt-get install -y \ + build-essential wget tar + +# Define environment variables for libzmq +ENV ZMQ_VERSION 4.3.5 +ENV ZMQ_PREFIX /opt/libzmq + +# Download, compile, and install libzmq +RUN mkdir -p ${ZMQ_PREFIX}; \ + wget https://github.com/zeromq/libzmq/releases/download/v${ZMQ_VERSION}/zeromq-${ZMQ_VERSION}.tar.gz -O libzmq.tar.gz; \ + tar -xzf libzmq.tar.gz; \ + cd zeromq-${ZMQ_VERSION}; \ + ./configure --prefix=${ZMQ_PREFIX} --enable-drafts; \ + make -j$(nproc) && make install + +# Cleanup unnecessary files +RUN rm -rf /zeromq-${ZMQ_VERSION} libzmq.tar.gz + +# Create final image with only the libzmq build fragments +FROM scratch +ENV ZMQ_PREFIX /opt/libzmq +COPY --from=0 ${ZMQ_PREFIX} ${ZMQ_PREFIX} diff --git a/docker/mpd.Dockerfile b/docker/Dockerfile.mpd similarity index 100% rename from docker/mpd.Dockerfile rename to docker/Dockerfile.mpd diff --git a/docker/webapp.Dockerfile b/docker/Dockerfile.webapp similarity index 100% rename from docker/webapp.Dockerfile rename to docker/Dockerfile.webapp diff --git a/docker/config/docker.mpd.conf b/docker/config/docker.mpd.conf index 4ec64890e..ad7713d0d 100644 --- a/docker/config/docker.mpd.conf +++ b/docker/config/docker.mpd.conf @@ -11,7 +11,7 @@ # be disabled and audio files will only be accepted over ipc socket (using # file:// protocol) or streaming files over an accepted protocol. # -music_directory "/home/pi/RPi-Jukebox-RFID/shared/audiofolders" +music_directory "~/RPi-Jukebox-RFID/shared/audiofolders" # # This setting sets the MPD internal playlist directory. The purpose of this # directory is storage for playlists created by MPD. The server will use @@ -67,7 +67,7 @@ sticker_file "~/.config/mpd/sticker.sql" # initialization. This setting is disabled by default and MPD is run as the # current user. # -user "root" +# user "root" # # This setting specifies the group that MPD will run as. If not specified # primary group of user specified with "user" setting will be used (if set). @@ -225,6 +225,10 @@ decoder { # gapless "no" } +decoder { + plugin "wildmidi" + enabled "no" +} # ############################################################################### @@ -239,12 +243,11 @@ decoder { # audio_output { type "alsa" - name "My ALSA Device" -# device "pulse" # optional - mixer_type "software" # optional -# mixer_device "default" # optional -# mixer_control "Master" # optional -# mixer_index "0" # optional + name "Global ALSA->Pulse stream" +# mixer_type "hardware" + mixer_control "Master" + mixer_device "pulse" + device "pulse" } # # An example of an OSS output: @@ -311,9 +314,9 @@ audio_output { # Please see README.Debian if you want mpd to play through the pulseaudio # daemon started as part of your graphical desktop session! # -# audio_output { - # type "pulse" - # name "My Pulse Output" +#audio_output { +# type "pulse" +# name "My Pulse Output" # server "remote_server" # optional # sink "remote_server_sink" # optional # } diff --git a/docker/config/docker.pulse.mpd.conf b/docker/config/docker.pulse.mpd.conf deleted file mode 100644 index ad7713d0d..000000000 --- a/docker/config/docker.pulse.mpd.conf +++ /dev/null @@ -1,412 +0,0 @@ -# An example configuration file for MPD. -# Read the user manual for documentation: http://www.musicpd.org/doc/user/ -# or /usr/share/doc/mpd/html/user.html - - -# Files and directories ####################################################### -# -# This setting controls the top directory which MPD will search to discover the -# available audio files and add them to the daemon's online database. This -# setting defaults to the XDG directory, otherwise the music directory will be -# be disabled and audio files will only be accepted over ipc socket (using -# file:// protocol) or streaming files over an accepted protocol. -# -music_directory "~/RPi-Jukebox-RFID/shared/audiofolders" -# -# This setting sets the MPD internal playlist directory. The purpose of this -# directory is storage for playlists created by MPD. The server will use -# playlist files not created by the server but only if they are in the MPD -# format. This setting defaults to playlist saving being disabled. -# -# playlists are inside the Phoniebox path: -playlist_directory "~/.config/mpd/playlists" -# -# This setting sets the location of the MPD database. This file is used to -# load the database at server start up and store the database while the -# server is not up. This setting defaults to disabled which will allow -# MPD to accept files over ipc socket (using file:// protocol) or streaming -# files over an accepted protocol. -# -db_file "~/.config/mpd/database" -# -# These settings are the locations for the daemon log files for the daemon. -# These logs are great for troubleshooting, depending on your log_level -# settings. -# -# The special value "syslog" makes MPD use the local syslog daemon. This -# setting defaults to logging to syslog, or to journal if mpd was started as -# a systemd service. -# -log_file "~/.config/mpd/log" -# -# This setting sets the location of the file which stores the process ID -# for use of mpd --kill and some init scripts. This setting is disabled by -# default and the pid file will not be stored. -# -pid_file "~/.config/mpd/pid" -# -# This setting sets the location of the file which contains information about -# most variables to get MPD back into the same general shape it was in before -# it was brought down. This setting is disabled by default and the server -# state will be reset on server start up. -# -state_file "~/.config/mpd/state" -# -# The location of the sticker database. This is a database which -# manages dynamic information attached to songs. -# -sticker_file "~/.config/mpd/sticker.sql" -# -############################################################################### - - -# General music daemon options ################################################ -# -# This setting specifies the user that MPD will run as. MPD should never run as -# root and you may use this setting to make MPD change its user ID after -# initialization. This setting is disabled by default and MPD is run as the -# current user. -# -# user "root" -# -# This setting specifies the group that MPD will run as. If not specified -# primary group of user specified with "user" setting will be used (if set). -# This is useful if MPD needs to be a member of group such as "audio" to -# have permission to use sound card. -# -#group "nogroup" -# -# This setting sets the address for the daemon to listen on. Careful attention -# should be paid if this is assigned to anything other then the default, any. -# This setting can deny access to control of the daemon. Choose any if you want -# to have mpd listen on every address. Not effective if systemd socket -# activation is in use. -# -# For network -bind_to_address "any" -# -# And for Unix Socket -#bind_to_address "/run/mpd/socket" -# -# This setting is the TCP port that is desired for the daemon to get assigned -# to. -# -port "6600" -# -# This setting controls the type of information which is logged. Available -# setting arguments are "default", "secure" or "verbose". The "verbose" setting -# argument is recommended for troubleshooting, though can quickly stretch -# available resources on limited hardware storage. -# -log_level "default" -# -# Setting "restore_paused" to "yes" puts MPD into pause mode instead -# of starting playback after startup. -# -#restore_paused "no" -# -# This setting enables MPD to create playlists in a format usable by other -# music players. -# -#save_absolute_paths_in_playlists "no" -# -# This setting defines a list of tag types that will be extracted during the -# audio file discovery process. The complete list of possible values can be -# found in the user manual. -#metadata_to_use "artist,album,title,track,name,genre,date,composer,performer,disc" -# -# This example just enables the "comment" tag without disabling all -# the other supported tags: -#metadata_to_use "+comment" -# -# This setting enables automatic update of MPD's database when files in -# music_directory are changed. -# -auto_update "yes" -# -# Limit the depth of the directories being watched, 0 means only watch -# the music directory itself. There is no limit by default. -# -auto_update_depth "10" -# -############################################################################### - - -# Symbolic link behavior ###################################################### -# -# If this setting is set to "yes", MPD will discover audio files by following -# symbolic links outside of the configured music_directory. -# -#follow_outside_symlinks "yes" -# -# If this setting is set to "yes", MPD will discover audio files by following -# symbolic links inside of the configured music_directory. -# -#follow_inside_symlinks "yes" -# -############################################################################### - - -# Zeroconf / Avahi Service Discovery ########################################## -# -# If this setting is set to "yes", service information will be published with -# Zeroconf / Avahi. -# -#zeroconf_enabled "yes" -# -# The argument to this setting will be the Zeroconf / Avahi unique name for -# this MPD server on the network. %h will be replaced with the hostname. -# -#zeroconf_name "Music Player @ %h" -# -############################################################################### - - -# Permissions ################################################################# -# -# If this setting is set, MPD will require password authorization. The password -# setting can be specified multiple times for different password profiles. -# -#password "password@read,add,control,admin" -# -# This setting specifies the permissions a user has who has not yet logged in. -# -#default_permissions "read,add,control,admin" -# -############################################################################### - - -# Database ####################################################################### -# - -#database { -# plugin "proxy" -# host "other.mpd.host" -# port "6600" -#} - -# Input ####################################################################### -# - -input { - plugin "curl" -# proxy "proxy.isp.com:8080" -# proxy_user "user" -# proxy_password "password" -} - -# QOBUZ input plugin -input { - enabled "no" - plugin "qobuz" -# app_id "ID" -# app_secret "SECRET" -# username "USERNAME" -# password "PASSWORD" -# format_id "N" -} - -# TIDAL input plugin -input { - enabled "no" - plugin "tidal" -# token "TOKEN" -# username "USERNAME" -# password "PASSWORD" -# audioquality "Q" -} - -# Decoder ##################################################################### -# - -decoder { - plugin "hybrid_dsd" - enabled "no" -# gapless "no" -} - -decoder { - plugin "wildmidi" - enabled "no" -} -# -############################################################################### - -# Audio Output ################################################################ -# -# MPD supports various audio output types, as well as playing through multiple -# audio outputs at the same time, through multiple audio_output settings -# blocks. Setting this block is optional, though the server will only attempt -# autodetection for one sound card. -# -# An example of an ALSA output: -# -audio_output { - type "alsa" - name "Global ALSA->Pulse stream" -# mixer_type "hardware" - mixer_control "Master" - mixer_device "pulse" - device "pulse" -} -# -# An example of an OSS output: -# -#audio_output { -# type "oss" -# name "My OSS Device" -# device "/dev/dsp" # optional -# mixer_type "hardware" # optional -# mixer_device "/dev/mixer" # optional -# mixer_control "PCM" # optional -#} -# -# An example of a shout output (for streaming to Icecast): -# -#audio_output { -# type "shout" -# encoder "vorbis" # optional -# name "My Shout Stream" -# host "localhost" -# port "8000" -# mount "/mpd.ogg" -# password "hackme" -# quality "5.0" -# bitrate "128" -# format "44100:16:1" -# protocol "icecast2" # optional -# user "source" # optional -# description "My Stream Description" # optional -# url "http://example.com" # optional -# genre "jazz" # optional -# public "no" # optional -# timeout "2" # optional -# mixer_type "software" # optional -#} -# -# An example of a recorder output: -# -#audio_output { -# type "recorder" -# name "My recorder" -# encoder "vorbis" # optional, vorbis or lame -# path "/var/lib/mpd/recorder/mpd.ogg" -## quality "5.0" # do not define if bitrate is defined -# bitrate "128" # do not define if quality is defined -# format "44100:16:1" -#} -# -# An example of a httpd output (built-in HTTP streaming server): -# -#audio_output { -# type "httpd" -# name "My HTTP Stream" -# encoder "vorbis" # optional, vorbis or lame -# port "8000" -# bind_to_address "0.0.0.0" # optional, IPv4 or IPv6 -# quality "5.0" # do not define if bitrate is defined -# bitrate "128" # do not define if quality is defined -# format "44100:16:1" -# max_clients "0" # optional 0=no limit -#} -# -# An example of a pulseaudio output (streaming to a remote pulseaudio server) -# Please see README.Debian if you want mpd to play through the pulseaudio -# daemon started as part of your graphical desktop session! -# -#audio_output { -# type "pulse" -# name "My Pulse Output" -# server "remote_server" # optional -# sink "remote_server_sink" # optional -# } -# -# An example of a winmm output (Windows multimedia API). -# -#audio_output { -# type "winmm" -# name "My WinMM output" -# device "Digital Audio (S/PDIF) (High Definition Audio Device)" # optional -# or -# device "0" # optional -# mixer_type "hardware" # optional -#} -# -# An example of an openal output. -# -#audio_output { -# type "openal" -# name "My OpenAL output" -# device "Digital Audio (S/PDIF) (High Definition Audio Device)" # optional -#} -# -## Example "pipe" output: -# -#audio_output { -# type "pipe" -# name "my pipe" -# command "aplay -f cd 2>/dev/null" -## Or if you're want to use AudioCompress -# command "AudioCompress -m | aplay -f cd 2>/dev/null" -## Or to send raw PCM stream through PCM: -# command "nc example.org 8765" -# format "44100:16:2" -#} -# -## An example of a null output (for no audio output): -# -#audio_output { -# type "null" -# name "My Null Output" -# mixer_type "none" # optional -#} -# -############################################################################### - - -# Normalization automatic volume adjustments ################################## -# -# This setting specifies the type of ReplayGain to use. This setting can have -# the argument "off", "album", "track" or "auto". "auto" is a special mode that -# chooses between "track" and "album" depending on the current state of -# random playback. If random playback is enabled then "track" mode is used. -# See for more details about ReplayGain. -# This setting is off by default. -# -#replaygain "album" -# -# This setting sets the pre-amp used for files that have ReplayGain tags. By -# default this setting is disabled. -# -#replaygain_preamp "0" -# -# This setting sets the pre-amp used for files that do NOT have ReplayGain tags. -# By default this setting is disabled. -# -#replaygain_missing_preamp "0" -# -# This setting enables or disables ReplayGain limiting. -# MPD calculates actual amplification based on the ReplayGain tags -# and replaygain_preamp / replaygain_missing_preamp setting. -# If replaygain_limit is enabled MPD will never amplify audio signal -# above its original level. If replaygain_limit is disabled such amplification -# might occur. By default this setting is enabled. -# -#replaygain_limit "yes" -# -# This setting enables on-the-fly normalization volume adjustment. This will -# result in the volume of all playing audio to be adjusted so the output has -# equal "loudness". This setting is disabled by default. -# -volume_normalization "yes" -# -############################################################################### - -# Character Encoding ########################################################## -# -# If file or directory names do not display correctly for your locale then you -# may need to modify this setting. -# -filesystem_charset "UTF-8" -# -############################################################################### diff --git a/docker/docker-compose.linux.yml b/docker/docker-compose.linux.yml index 9609dc18d..6285484f8 100755 --- a/docker/docker-compose.linux.yml +++ b/docker/docker-compose.linux.yml @@ -12,7 +12,7 @@ services: volumes: - ../shared/audiofolders:/home/pi/RPi-Jukebox-RFID/shared/audiofolders - ../shared/playlists:/home/pi/.config/mpd/playlists - - ./config/docker.pulse.mpd.conf:/home/pi/.config/mpd/mpd.conf + - ./config/docker.mpd.conf:/home/pi/.config/mpd/mpd.conf - $XDG_RUNTIME_DIR/pulse/native:/tmp/pulse-sock jukebox: @@ -25,5 +25,5 @@ services: - PULSE_SERVER=unix:/tmp/pulse-sock volumes: - ../shared:/home/pi/RPi-Jukebox-RFID/shared - - ./config/docker.pulse.mpd.conf:/home/pi/.config/mpd/mpd.conf + - ./config/docker.mpd.conf:/home/pi/.config/mpd/mpd.conf - $XDG_RUNTIME_DIR/pulse/native:/tmp/pulse-sock diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 03191dbd5..38a112551 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -8,7 +8,7 @@ services: - USER=root - HOME=/root context: ../ - dockerfile: ./docker/mpd.Dockerfile + dockerfile: ./docker/Dockerfile.mpd container_name: mpd image: phoniebox/mpd environment: @@ -17,7 +17,7 @@ services: volumes: - ../shared/audiofolders:/root/RPi-Jukebox-RFID/shared/audiofolders - ../shared/playlists:/root/.config/mpd/playlists - - ./config/docker.pulse.mpd.conf:/root/.config/mpd/mpd.conf + - ./config/docker.mpd.conf:/root/.config/mpd/mpd.conf jukebox: build: @@ -26,7 +26,7 @@ services: - USER=root - HOME=/root context: ../ - dockerfile: ./docker/jukebox.Dockerfile + dockerfile: ./docker/Dockerfile.jukebox container_name: jukebox image: phoniebox/jukebox depends_on: @@ -43,13 +43,13 @@ services: - ../src/jukebox:/root/RPi-Jukebox-RFID/src/jukebox - ../src/webapp/public/cover-cache:/root/RPi-Jukebox-RFID/src/webapp/build/cover-cache - ../shared:/root/RPi-Jukebox-RFID/shared - - ./config/docker.pulse.mpd.conf:/root/.config/mpd/mpd.conf + - ./config/docker.mpd.conf:/root/.config/mpd/mpd.conf command: python run_jukebox.py webapp: build: context: ../ - dockerfile: ./docker/webapp.Dockerfile + dockerfile: ./docker/Dockerfile.webapp container_name: webapp image: phoniebox/webapp depends_on: diff --git a/documentation/README.md b/documentation/README.md index bb11dd6f4..cbfb6276a 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -42,7 +42,7 @@ project check out the [documentation of Version 2](https://github.com/MiczFlor/R Version 3 has reached a mature state and will soon be the default version. However, some features may still be missing. Please check the [Feature Status](./developers/status.md), if YOUR feature is already implemented. -> [!NOTE] +> [!NOTE] > If version 3 has all the features you need, we recommend using Version 3. If there is a feature missing, please open an issue. diff --git a/documentation/builders/README.md b/documentation/builders/README.md index 6d9e67bff..512d26ed0 100644 --- a/documentation/builders/README.md +++ b/documentation/builders/README.md @@ -9,29 +9,32 @@ ## Features * Audio - * [Audio Output](./audio.md) - * [Bluetooth audio buttons](./bluetooth-audio-buttons.md) + * [Audio Output](./audio.md) + * [Bluetooth audio buttons](./bluetooth-audio-buttons.md) * [GPIO Recipes](./gpio.md) * [Card Database](./card-database.md) - * [RFID Cards synchronisation](./components/synchronisation/rfidcards.md) + * [RFID Cards synchronisation](./components/synchronisation/rfidcards.md) * [Auto Hotspot](./autohotspot.md) * File Management - * [Network share / Samba](./samba.md) + * [Network share / Samba](./samba.md) ## Hardware Components * [Power](./components/power/) - * [OnOff SHIM for safe power on/off](./components/power/onoff-shim.md) + * [OnOff SHIM for safe power on/off](./components/power/onoff-shim.md) + * [Battery Monitor based on a ADS1015](./components/power/batterymonitor.md) * [Soundcards](./components/soundcards/) - * [HiFiBerry Boards](./components/soundcards/hifiberry.md) + * [HiFiBerry Boards](./components/soundcards/hifiberry.md) * [RFID Readers](./../developers/rfid/README.md) +* [Event devices (USB and other buttons)](./event-devices.md) ## Web Application * Music - * [Playlists, Livestreams and Podcasts](./webapp/playlists-livestreams-podcasts.md) + * [Playlists, Livestreams and Podcasts](./webapp/playlists-livestreams-podcasts.md) ## Advanced + * [Troubleshooting](./troubleshooting.md) * [Concepts](./concepts.md) * [System](./system.md) diff --git a/documentation/builders/audio.md b/documentation/builders/audio.md index 93c46dd54..765c6a08e 100644 --- a/documentation/builders/audio.md +++ b/documentation/builders/audio.md @@ -24,6 +24,7 @@ to setup the configuration for the Jukebox Core App. Run the following steps in a console: + ```bash # Check available PulseAudio sinks $ pactl list sinks short @@ -45,6 +46,7 @@ $ paplay /usr/share/sounds/alsa/Front_Center.wav # This must also work when using an ALSA device $ aplay /usr/share/sounds/alsa/Front_Center.wav ``` + You can also try different PulseAudio sinks without setting the default sink. In this case the volume is the last used volume level for this sink: @@ -86,6 +88,7 @@ Pairing successful .... [PowerLocus Buddy]# exit ``` + If `bluetoothctl` has trouble to execute due to permission issue, try `sudo bluetoothctl`. Wait for a few seconds and then with `$ pactl list sinks short`, check wether the Bluetooth device shows up as an output. diff --git a/documentation/builders/autohotspot.md b/documentation/builders/autohotspot.md index 5a62a37ca..8efde605a 100644 --- a/documentation/builders/autohotspot.md +++ b/documentation/builders/autohotspot.md @@ -7,11 +7,12 @@ The Auto-Hotspot function enables the Jukebox to switch its connection between a ## How to connect -When the Jukebox cannot connect to a known WiFi, it will automatically create a hotspot. +When the Jukebox cannot connect to a known WiFi, it will automatically create a hotspot. You can connect to this hotspot using the password set during installation. Afterwards, you can access the Web App or connect via SSH as before, using the IP from the configuration. The default configuration is + ``` text * SSID : Phoniebox_Hotspot_ * Password : PlayItLoud! @@ -23,8 +24,7 @@ The default configuration is Auto-Hotspot can be enabled or disabled using the Web App or RPC Commands. -> [!NOTE] -> Disabling the Auto-Hotspot will run the WiFi check again and maintain the last connection state until reboot. +Disabling the Auto-Hotspot will run the WiFi check again and maintain the last connection state until reboot. > [!IMPORTANT] > If you disable this feature, you will lose access to the Jukebox if you are not near a known WiFi after reboot! @@ -34,11 +34,13 @@ Auto-Hotspot can be enabled or disabled using the Web App or RPC Commands. ### AutoHotspot functionality is not working Check the `autohotspot.service` status + ``` bash sudo systemctl status autohotspot.service ``` and logs + ``` bash sudo journalctl -u autohotspot.service -n 50 ``` @@ -52,12 +54,13 @@ Check your WiFi configuration. ### You need to add a new WiFi network to the Raspberry Pi #### Using the command line + Connect to the hotspot and open a terminal. Use the [raspi-config](https://www.raspberrypi.com/documentation/computers/configuration.html#wireless-lan) tool to add the new WiFi. ## Resources * [Raspberry Connect - Auto WiFi Hotspot Switch](https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection) * [Raspberry Pi - Configuring networking](https://www.raspberrypi.com/documentation/computers/configuration.html#using-the-command-line) -* [dhcpcd / wpa_supplicant]() - * [hostapd](http://w1.fi/hostapd/) - * [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) +* dhcpcd / wpa_supplicant + * [hostapd](http://w1.fi/hostapd/) + * [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) diff --git a/documentation/builders/components/power/batterymonitor.md b/documentation/builders/components/power/batterymonitor.md new file mode 100644 index 000000000..d57e14acb --- /dev/null +++ b/documentation/builders/components/power/batterymonitor.md @@ -0,0 +1,33 @@ +# Battery Monitor based on a ADS1015 + +> [!CAUTION] +> Lithium and other batteries are dangerous and must be treated with care. +> Rechargeable Lithium Ion batteries are potentially hazardous and can +> present a serious **FIRE HAZARD** if damaged, defective or improperly used. +> Do not use this circuit to a lithium ion battery without expertise and +> training in handling and use of batteries of this type. +> Use appropriate test equipment and safety protocols during development. +> There is no warranty, this may not work as expected or at all! + +The script in [src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/\_\_init\_\_.py](../../../../src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py) is intended to read out the voltage of a single Cell LiIon Battery using a [CY-ADS1015 Board](https://www.adafruit.com/product/1083): + +```text + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === +``` + +> [!WARNING] +> +> * the circuit is constantly draining the battery! (leak current up to: 2.1µA) +> * the time between sample needs to be a minimum 1sec with this high impedance voltage divider don't use the continuous conversion method! diff --git a/documentation/builders/components/power/onoff-shim.md b/documentation/builders/components/power/onoff-shim.md index b83ea6140..e9a2849c9 100644 --- a/documentation/builders/components/power/onoff-shim.md +++ b/documentation/builders/components/power/onoff-shim.md @@ -9,7 +9,7 @@ To install the software, open a terminal and type the following command to run t > [!NOTE] > The installation will ask you a few questions. You can safely answer with the default response. -``` +```bash curl https://get.pimoroni.com/onoffshim | bash ``` @@ -28,9 +28,8 @@ The OnOff SHIM comes with a 12-PIN header which needs soldering. If you want to | GPLCLK0 | 7 | 7 | GPIO4 | | GPIO17 | 11 | 11 | GPIO17 | -* More information can be found here: https://pinout.xyz/pinout/onoff_shim +* More information can be found here: ## Assembly options -![](https://cdn.review-images.pimoroni.com/upload-b6276a310ccfbeae93a2d13ec19ab83b-1617096824.jpg?width=640) - +![OnOffShim soldered on a Raspberry Pi](https://cdn.review-images.pimoroni.com/upload-b6276a310ccfbeae93a2d13ec19ab83b-1617096824.jpg?width=640) diff --git a/documentation/builders/components/soundcards/hifiberry.md b/documentation/builders/components/soundcards/hifiberry.md index 1f19fa96d..f663abb42 100644 --- a/documentation/builders/components/soundcards/hifiberry.md +++ b/documentation/builders/components/soundcards/hifiberry.md @@ -19,7 +19,6 @@ If you know you HifiBerry Board identifier, you can run the script as a 1-liner If you like to disable your HiFiberry Sound card and enable onboard sound, run the following command - ```bash ./setup_hifiberry.sh disable ``` diff --git a/documentation/builders/configuration.md b/documentation/builders/configuration.md index dd4c1fd53..c75f53683 100644 --- a/documentation/builders/configuration.md +++ b/documentation/builders/configuration.md @@ -27,9 +27,10 @@ $ ./run_jukebox.sh # Restart the service $ systemctl --user start jukebox-daemon ``` -To try different configurations, you can start the Jukebox with a custom config file. + +To try different configurations, you can start the Jukebox with a custom config file. This could be useful if you want your Jukebox to only allow a lower volume when started -at nighttime, signaling it's time to go to bed. :-) +at nighttime, signaling it's time to go to bed. :-) The path to the custom config file must be either absolute or relative to the folder `src/jukebox/`. ```bash diff --git a/documentation/builders/event-devices.md b/documentation/builders/event-devices.md new file mode 100644 index 000000000..95e871430 --- /dev/null +++ b/documentation/builders/event-devices.md @@ -0,0 +1,124 @@ +# Event devices + +## Background + +Event devices are generic input devices that are exposed in `/dev/input`. +This includes USB peripherals (Keyboards, Controllers, Joysticks or Mouse) as well as potentially bluetooth devices. + +A specific usecase for this could be, if a Zero Delay Arcade USB Encoder is used to wire arcade buttons instead of using GPIO pins. + +These device interface support various kinds of input events, such as press, movements and potentially also outputs (eg. rumble, led lights, ...). Currently only the usage of button presses as input is supported. + +This functionality was previously implemented under the name of [USB buttons](https://github.com/MiczFlor/RPi-Jukebox-RFID/blob/develop/components/controls/buttons_usb_encoder/README.md). + +The devices and their button mappings need to be mapped in the configuration file. + +## Configuration + +To configure event devices, first add the plugin as an entry to the module list of your main configuration file ``shared/settings/jukebox.yaml``: + +``` yaml +modules: + named: + event_devices: controls.event_devices +``` + +And add the following section with the plugin specific configuration: + +``` yaml +evdev: + enabled: true + config_file: ../../shared/settings/evdev.yaml +``` + +The actual configuration itself is stored in a separate file. In this case in ``../../shared/settings/evdev.yaml``. + +The configuration is structured akin to the configuration of the [GPIO devices](./gpio.md). + +In contrast to `gpio`, multiple devices (eg arcade controllser, keyboards, joysticks, mice, ...) are supported, each with their own `input_devices` (=buttons). `output_devices` or actions other than `on_press` are currently not yet supported. + +``` yaml +devices: # list of devices to listen for + {device nickname}: # config for a specific device + device_name: {device_name} # name of the device + exact: False/True # optional to require exact match. Otherwise it is sufficient that a part of the name matches + input_devices: # list of buttons to listen for for this device + {button nickname}: + type: Button + kwargs: + key_code: {key-code}: # evdev event id + actions: + on_press: # Currently only the on_press action is supported + {rpc_command_definition} # eg `alias: toggle` +``` + +The `{device nickname}` is only for your own orientation and can be choosen freely. +For each device you need to figure out the `{device_name}` and the `{event_id}` corresponding to key strokes, as indicated in the sections below. + +### Identifying the `{device_name}` + +The `{device_name}` can be identified using the following Python snippet: + +``` Python +import evdev +devices = [evdev.InputDevice(path) for path in evdev.list_devices()] +for device in devices: + print(device.path, device.name, device.phys) +``` + +The output could be in the style of: + +```text +/dev/input/event1 Dell Dell USB Keyboard usb-0000:00:12.1-2/input0 +/dev/input/event0 Dell USB Optical Mouse usb-0000:00:12.0-2/input0 +``` + +In this example, the `{device_name}` could be `DELL USB Optical Mouse`. +Note that if you use the option `exact: False`, it would be sufficient to add a substring such as `USB Keyboard`. + +### Identifying the `{key-code}` + +The key code for a button press can be determined using the following code snippet: + +``` Python +import evdev +device = evdev.InputDevice('/dev/input/event0') +device.capabilities(verbose=True)[('EV_KEY', evdev.ecodes.EV_KEY)] +``` + +With the `InputDevice` corresponding to the path from the output of the section `{device_name}` (eg. in the example `/dev/input/event0` +would correspond to `Dell Dell USB Keyboard`). + +If the naming is not clear, it is also possible to empirically check for the key code by listening for events: + +``` Python +from evdev import InputDevice, categorize, ecodes +dev = InputDevice('/dev/input/event1') +print(dev) +for event in dev.read_loop(): + if event.type == ecodes.EV_KEY: + print(categorize(event)) +``` + +The output could be of the form: + +```text +device /dev/input/event1, name "DragonRise Inc. Generic USB Joystick ", phys "usb-3f980000.usb-1.2/input0" +key event at 1672569673.124168, 297 (BTN_BASE4), down +key event at 1672569673.385170, 297 (BTN_BASE4), up +``` + +In this example output, the `{key-code}` would be `297` + +Alternatively, the device could also be setup without a mapping. +Afterwards, when pressing keys, the key codes can be found in the log files. Press various buttons on your device, +while watching the logs with `tail -f shared/logs/app.log`. +Look for entries like `No callback registered for button ...`. + +### Specifying the `{rpc_command_definition}` + +The RPC command follows the regular RPC command rules as defined in the [following documentation](./rpc-commands.md). + +## Full example config + +A complete configuration example for a USB Joystick controller can be found in the [examples](../../resources/default-settings/evdev.example.yaml). diff --git a/documentation/builders/gpio.md b/documentation/builders/gpio.md index 9d496c6b7..d0d82b206 100644 --- a/documentation/builders/gpio.md +++ b/documentation/builders/gpio.md @@ -112,7 +112,7 @@ A button to shutdown the Jukebox if it is pressed for more than 3 seconds. Note ```yml input_devices: - IncreaseVolume: + Shutdown: type: LongPressButton kwargs: pin: 3 diff --git a/documentation/builders/installation.md b/documentation/builders/installation.md index 7cd321b7a..2228ee47f 100644 --- a/documentation/builders/installation.md +++ b/documentation/builders/installation.md @@ -3,7 +3,7 @@ ## Install Raspberry Pi OS Lite > [!IMPORTANT] -> All Raspberry Pi models are supported. For sufficient performance, **we recommend Pi 2, 3 or Zero 2** (`ARMv7` models). Because Pi 1 or Zero 1 (`ARMv6` models) have limited resources, they are slower (during installation and start up procedure) and might require a bit more work! Pi 4 and 5 are an excess ;-) +> All Raspberry Pi models are supported. For sufficient performance, **we recommend Pi 2, 3 or Zero 2** (`ARMv7` models). Because Pi 1 or Zero 1 (`ARMv6` models) have limited resources, they are slower (during installation and start up procedure) and might require a bit more work! Pi 4 and 5 are an excess ;-) Before you can install the Phoniebox software, you need to prepare your Raspberry Pi. @@ -27,8 +27,8 @@ Before you can install the Phoniebox software, you need to prepare your Raspberr 8. Confirm the next warning about erasing the SD card with `Yes` 9. Wait for the imaging process to be finished (it'll take a few minutes) - ### Pre-boot preparation +
In case you forgot to customize the OS settings, follow these instructions after RPi OS has been written to the SD card. @@ -81,8 +81,9 @@ You will need a terminal, like PuTTY for Windows or the Terminal app for Mac to ### Pre-install preparation / workarounds #### Network management since Bookworm +
-With Bookworm, network management has changed. Now, "NetworkManager" is used instead of "dhcpcd". +With Bookworm, network management has changed. Now, "NetworkManager" is used instead of "dhcpcd". Both methods are supported during installation, but "NetworkManager" is recommended as it is simpler to set up and use. For Bullseye, this can also be activated, though it requires a manual process before running the installation. @@ -91,24 +92,28 @@ If the settings are changed, your network will reset, and WiFi will not be confi Therefore, make sure you use a wired connection or perform the following steps in a local terminal with a connected monitor and keyboard. Change network config + * run `sudo raspi-config` * select `6 - Advanced Options` * select `AA - Network Config` * select `NetworkManager` If you need Wifi, add the information now + * select `1 - System Options` * select `1 - Wireless LAN` * enter Wifi information +
#### Workaround for 64-bit Kernels (Pi 4 and newer) +
The installation process checks if a 32-bit OS is running, as 64-bit is currently not supported. This check also fails if the kernel is running in 64-bit mode. This is the default for Raspberry Pi models 4 and newer. -To be able to run the installation, you have to switch to the 32-bit mode by modifying the `config.txt` and add/change the line `arm_64bit=0`. +To be able to run the installation, you have to switch to the 32-bit mode by modifying the `config.txt` and add/change the line `arm_64bit=0`. Up to Bullseye, the `config.txt` file is located at `/boot/`. Since Bookworm, the location changed to `/boot/firmware/` ([see here](https://www.raspberrypi.com/documentation/computers/config_txt.html)). Reboot before you proceed. @@ -117,6 +122,7 @@ Reboot before you proceed. ## Install Phoniebox software Choose a version, run the corresponding install command in your SSH terminal and follow the instructions. + * [Stable Release](#stable-release) * [Pre-Release](#pre-release) * [Development](#development) @@ -127,6 +133,7 @@ After a successful installation, [configure your Phoniebox](configuration.md). > Depending on your hardware, this installation might last around 60 minutes (usually it's faster, 20-30 min). It updates OS packages, installs Phoniebox dependencies and applies settings. Be patient and don't let your computer go to sleep. It might disconnect your SSH connection causing the interruption of the installation process. Consider starting the installation in a terminal multiplexer like 'screen' or 'tmux' to avoid this. ### Stable Release + This will install the latest **stable release** from the *future3/main* branch. ```bash @@ -134,6 +141,7 @@ cd; bash <(wget -qO- https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID ``` ### Pre-Release + This will install the latest **pre-release** from the *future3/develop* branch. ```bash @@ -141,6 +149,7 @@ cd; GIT_BRANCH='future3/develop' bash <(wget -qO- https://raw.githubusercontent. ``` ### Development + You can also install a specific branch and/or a fork repository. Update the variables to refer to your desired location. (The URL must not necessarily be updated, unless you have actually updated the file being downloaded.) > [!IMPORTANT] @@ -155,9 +164,9 @@ cd; GIT_USER='MiczFlor' GIT_BRANCH='future3/develop' bash <(wget -qO- https://ra > If you install another branch or from a fork repository, the Web App needs to be built locally. This is part of the installation process. See the the developers [Web App](../developers/webapp.md) documentation for further details. ### Logs + To follow the installation closely, use this command in another terminal. ```bash cd; tail -f INSTALL-.log ``` - diff --git a/documentation/builders/samba.md b/documentation/builders/samba.md index ac9a93bbc..8e486a181 100644 --- a/documentation/builders/samba.md +++ b/documentation/builders/samba.md @@ -4,15 +4,14 @@ To conveniently copy files to your Phoniebox via network `samba` can be configur ## Connect -To access the share open your OS network environment and select your Phoniebox device. +To access the share open your OS network environment and select your Phoniebox device. Alternatively directly access it via url with the file explorer (e.g. Windows `\\`, MacOS `smb://`). See also + * [MacOS](https://support.apple.com/lt-lt/guide/mac-help/mchlp1140/mac) ## User name / Password As login credentials use the same username you used to run the installation with. The password is `raspberry`. You can change the password anytime using the command `sudo smbpasswd -a ""`. - - diff --git a/documentation/builders/troubleshooting.md b/documentation/builders/troubleshooting.md index 5b4061aa8..a18272afb 100644 --- a/documentation/builders/troubleshooting.md +++ b/documentation/builders/troubleshooting.md @@ -47,10 +47,10 @@ The default logging config does 2 things: 1. It writes 2 log files: -```bash -shared/logs/app.log : Complete Debug Messages -shared/logs/errors.log : Only Errors and Warnings -``` + ```bash + shared/logs/app.log : Complete Debug Messages + shared/logs/errors.log : Only Errors and Warnings + ``` 2. Prints logging messages to the console. If run as a service, only error messages are emitted to console to avoid spamming the system log files. diff --git a/documentation/builders/update.md b/documentation/builders/update.md index cb919492a..6a9004625 100644 --- a/documentation/builders/update.md +++ b/documentation/builders/update.md @@ -12,6 +12,100 @@ Restore your old files after the new installation was successful and check if ne $ diff shared/settings/jukebox.yaml resources/default-settings/jukebox.default.yaml ``` +## Manually upgrade to the latest version + +> [!CAUTION] +> This documentation is only recommended for users running on `future3/develop` branch. For optimal system updates, it is strongly recommended to utilize the upgrade feature when transitioning to the next version (The Upgrade Feature will come in the future [#2304](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/2304)). Manual updates may necessitate specific migration steps and, if overlooked, could result in system failure. Please use these steps with caution. + +If you only want to update a few recent commits, this following explanation outlines the steps to do so + +Typically, 4 steps need to be considered + +1. Backup Local Changes (Optional) +1. Pull the latest version from Github +1. Replace the Web App with the most recent build +1. Optional: Update the config files + +### Fetch the most recent version from Github + +First, SSH into your Phoniebox. + +```bash +cd ~/RPi-Jukebox-RFID/ +``` + +Second, get the latest version from Github. Depending on your proficiency with Git, you can also checkout a specific branch or version. +Be aware, in case you have made changes to the software, stash them to keep them safe. + +1. Backup Local Changes (Optional): + - Stash your local changes: + + ```bash + git stash push -m "Backup before pull" + ``` + + - Create a Backup Branch (and potentially delete it in case it already exists): + + ```bash + git branch -D backup-before-pull + git branch backup-before-pull + ``` + +1. Pull Latest Changes: + + ```bash + git pull + ``` + +1. Update Web App: + 1. Backup the current webapp build + + ```bash + cd ~/RPi-Jukebox-RFID/src/webapp + rm -rf build-backup + mv build build-backup + ``` + + 1. Go to the [Github Release page](https://github.com/MiczFlor/RPi-Jukebox-RFID/releases) find the latest `Pre-release` release (typically Alpha). + 1. Under "Assets", find the latest Web App release called "webapp-build-latest.tar.gz" and copy the URL. + 1. On your Phoniebox, download the file and extract the archive. Afterwards, delete the archive + + ```bash + wget {URL} + tar -xzf webapp-build-latest.tar.gz + rm -rf webapp-build-latest.tar.gz + ``` + +1. Reboot the Phoniebox: + + ```bash + sudo reboot + ``` + +1. Verify the version of your Phoniebox in the settings tab. + +Revert to Backup If Needed: + +- Checkout the backup branch: + + ```bash + git checkout backup-before-pull + ``` + +- Reapply stashed changes (if any): + + ```bash + git stash pop + ``` + +- Revert Web App: + + ```bash + cd ~/RPi-Jukebox-RFID/src/webapp + rm -rf build + mv build-backup build + ``` + ## Migration Path from Version 2 There is no update path coming from Version 2.x of the Jukebox. diff --git a/documentation/builders/webapp/playlists-livestreams-podcasts.md b/documentation/builders/webapp/playlists-livestreams-podcasts.md index 2b8be79a1..f45969e6e 100644 --- a/documentation/builders/webapp/playlists-livestreams-podcasts.md +++ b/documentation/builders/webapp/playlists-livestreams-podcasts.md @@ -3,11 +3,13 @@ By default, the Jukebox represents music based on its metadata like album name, artist or song name. The hierarchy and order of songs is determined by their original definition, e.g. order of songs within an album. If you prefer a specific list of songs to be played, you can use playlists (files ending with `*.m3u`). Jukebox also supports livestreams and podcasts (if connected to the internet) through playlists. ## Playlists + If you like the Jukebox to play songs in a pre-defined order, you can use .m3u playlists. A .m3u playlist is a plain text file that contains a list of file paths or URLs to multimedia files. Each entry in the playlist represents a single song, and they are listed in the order in which they should be played. ### Structure of a .m3u playlist + A .m3u playlist is a simple text document with each song file listed on a separate line. Each entry is optionally preceded by a comment line that starts with a '#' symbol. The actual file paths or URLs of the media files come after the comment. ### Creating a .m3u playlist @@ -18,7 +20,7 @@ A .m3u playlist is a simple text document with each song file listed on a separa 1. On the following lines, list the file paths or URLs of the media files you want to include in the playlist, one per line. They must refer to true files paths on your Jukebox. They can be relative or absolute paths. 1. Save the file with the .m3u extension, e.g. `my_playlist.m3u`. -``` +```text # Absolute /home//RPi-Jukebox-RFID/shared/audiofolders/Simone Sommerland/Die 30 besten Kindergartenlieder/08 - Pitsch, patsch, Pinguin.mp3 /home//RPi-Jukebox-RFID/shared/audiofolders/Simone Sommerland/Die 30 besten Spiel- Und Bewegungslieder/12 - Das rote Pferd.mp3 @@ -42,7 +44,7 @@ Based on the note above, we suggest to use m3u playlists like this, especially i #### Example folder structure -``` +```text └── audiofolders ├── wake-up-songs │ └── playlist.m3u @@ -74,9 +76,9 @@ In order to play radio livestreams on your Jukebox, you use playlists to registe You can now assign livestreams to cards [following the example](#assigning-a-m3u-playlist-to-a-card) of playlists. -#### Example folder structure and playlist names +#### Example folder structure and playlist names for livestreams -``` +```text └── audiofolders ├── wdr-kids │ └── wdr-kids-livestream.txt @@ -108,13 +110,13 @@ We will explain options 1 and 2 more closely. ### Using podcast.txt playlist in Jukebox 1. [Follow the steps above](#using-m3u-playlists-in-jukebox) to add a playlist to your Jukebox (make sure you have created individual folders). -1. When creating the playlist file, make sure it's called or at least ends with `podcasts.txt` instead of `.m3u`. (Examples: `awesome-podcast.txt`, `podcast.txt`). +1. When creating the playlist file, make sure it's called or at least ends with `podcast.txt` instead of `.m3u`. (Examples: `awesome-podcast.txt`, `podcast.txt`). 1. Add links to your individual podcast episodes just like you would with songs in .m3u playlists 1. As an alternative, you can provide a single RSS feed (XML). Jukebox will expand the file and refer to all episodes listed within this file. -#### Example folder structure and playlist names +#### Example folder structure and playlist names for podcasts -``` +```text └── audiofolders ├── die-maus │ └── die-maus-podcast.txt diff --git a/documentation/developers/README.md b/documentation/developers/README.md index 6addeaff1..ff21a8ceb 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -4,6 +4,7 @@ * [Development Environment](./development-environment.md) * [Python Development Notes](python.md) +* [Documentation (with Markdown)](documentatíon.md) ## Reference diff --git a/documentation/developers/docker.md b/documentation/developers/docker.md index 827178aca..6a0af80c7 100644 --- a/documentation/developers/docker.md +++ b/documentation/developers/docker.md @@ -20,14 +20,14 @@ need to adapt some of those commands to your needs. 2. Pull the Jukebox repository: ```bash - $ git clone https://github.com/MiczFlor/RPi-Jukebox-RFID.git + git clone https://github.com/MiczFlor/RPi-Jukebox-RFID.git ``` 3. Create a jukebox.yaml file * Copy the `./resources/default-settings/jukebox.default.yaml` to `./shared/settings` and rename the file to `jukebox.yaml`. ```bash - $ cp ./resources/default-settings/jukebox.default.yaml ./shared/settings/jukebox.yaml + cp ./resources/default-settings/jukebox.default.yaml ./shared/settings/jukebox.yaml ``` * Override/Merge the values from the following [Override file](../../docker/config/jukebox.overrides.yaml) in your `jukebox.yaml`. @@ -39,121 +39,168 @@ need to adapt some of those commands to your needs. ## Run development environment -In contrary to how everything is set up on the Raspberry Pi, it\'s good +In contrary to how everything is set up on the Raspberry Pi, it's good practice to isolate different components in different Docker images. They can be run individually or in combination. To do that, we use `docker-compose`. ### Mac +
+ +See details + 1. [Install Docker & Compose (Mac)](https://docs.docker.com/docker-for-mac/install/) -2. Install pulseaudio +1. Install pulseaudio 1. Use Homebrew to install - ``` - $ brew install pulseaudio - ``` - 2. Enable pulseaudio network capabilities. In an editor, open `/opt/homebrew/Cellar/pulseaudio/16.1/etc/pulse/default.pa` (you might need to adapt this path to your own system settings). Uncomment the following line. - ``` - load-module module-native-protocol-tcp - ``` - 3. Restart the pulseaudio service - ``` - $ brew services restart pulseaudio - ``` - 4. If you have trouble with your audio, try these resources to troubleshoot: [[1]](https://gist.github.com/seongyongkim/b7d630a03e74c7ab1c6b53473b592712), [[2]](https://devops.datenkollektiv.de/running-a-docker-soundbox-on-mac.html), [[3]](https://stackoverflow.com/a/50939994/1062438) -> [!NOTE] -> In order for Pulseaudio to work properly with Docker on your Mac, you need to start Pulseaudio in a specific way. Otherwise MPD will throw an exception. See [Pulseaudio issues on Mac](#pulseaudio-issue-on-mac) for more info. + ```bash + brew install pulseaudio + ``` -``` bash -// Build Images -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml build + 1. Enable pulseaudio network capabilities. In an editor, open `/opt/homebrew/Cellar/pulseaudio/16.1/etc/pulse/default.pa` (you might need to adapt this path to your own system settings). Uncomment the following line: -// Run Docker Environment -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml up + ```text + load-module module-native-protocol-tcp + ``` -// Shuts down Docker containers and Docker network -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml down -``` + 1. Restart the pulseaudio service + + ```bash + brew services restart pulseaudio + ``` + + 1. If you have trouble with your audio, try these resources to troubleshoot: [[1]](https://gist.github.com/seongyongkim/b7d630a03e74c7ab1c6b53473b592712), [[2]](https://devops.datenkollektiv.de/running-a-docker-soundbox-on-mac.html), [[3]](https://stackoverflow.com/a/50939994/1062438) + +1. Run `docker-compose` + + > [!NOTE] + > In order for Pulseaudio to work properly with Docker on your Mac, you need to start Pulseaudio in a specific way. Otherwise MPD will throw an exception. See [Pulseaudio issues on Mac](#pulseaudio-issue-on-mac) for more info. + + 1. Build libzmq for your host machine + + ```bash + docker build -f docker/Dockerfile.libzmq -t libzmq:local . + ``` + + 1. Build Images + + ```bash + docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml build + ``` + + 1. Run Docker Environment -> Runs the entire Phoniebox environment + + ```bash + docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml up + ``` + + * Shuts down Docker containers and Docker network + + ```bash + docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml down + ``` + +
### Windows +
+ +See details + 1. Install [Docker & Compose (Windows)](https://docs.docker.com/docker-for-windows/install/) -2. Download [pulseaudio](https://www.freedesktop.org/wiki/Software/PulseAudio/Ports/Windows/Support/) +1. Download [pulseaudio](https://www.freedesktop.org/wiki/Software/PulseAudio/Ports/Windows/Support/) -3. Uncompress somewhere in your user folder +1. Uncompress somewhere in your user folder -4. Edit `$INSTALL_DIR/etc/pulse/default.pa` +1. Edit `$INSTALL_DIR/etc/pulse/default.pa` -5. Add the following line +1. Add the following line ``` bash load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1 ``` -6. Edit `$INSTALL_DIR/etc/pulse//etc/pulse/daemon.conf`, find the +1. Edit `$INSTALL_DIR/etc/pulse/daemon.conf`, find the following line and change it to: ``` bash exit-idle-time = -1 ``` -7. Execute `$INSTALL_DIR/bin/pulseaudio.exe` +1. Execute `$INSTALL_DIR/bin/pulseaudio.exe` -8. Make sure Docker is running (e.g. start Docker Desktop) +1. Make sure Docker is running (e.g. start Docker Desktop) -9. Run `docker-compose` +1. Run `docker-compose` - ``` bash - // Build Images - $ docker-compose -f docker/docker-compose.yml build + 1. Build libzmq for your host machine - // Run Docker Environment - $ docker-compose -f docker/docker-compose.yml up + ```bash + docker build -f docker/Dockerfile.libzmq -t libzmq:local . + ``` - // Shuts down Docker containers and Docker network - $ docker-compose -f docker/docker-compose.yml down - ``` + 1. Build Images + + ```bash + docker-compose -f docker/docker-compose.yml build + ``` + + 1. Run Docker Environment -> Runs the entire Phoniebox environment + + ```bash + docker-compose -f docker/docker-compose.yml up + ``` + + * Shuts down Docker containers and Docker network + + ```bash + docker-compose -f docker/docker-compose.yml down + ``` + +
### Linux +
+ +See details + 1. Install Docker & Compose * [Docker](https://docs.docker.com/engine/install/debian/) * [Compose](https://docs.docker.com/compose/install/) -2. Make sure you don\'t use `sudo` to run your `docker-compose`. Check out +1. Make sure you don\'t use `sudo` to run your `docker-compose`. Check out Docker\'s [post-installation guide](https://docs.docker.com/engine/install/linux-postinstall/) for more information. -```bash -// Build Images -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml build +1. Run `docker-compose` -// Run Docker Environment -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml up + 1. Build libzmq for your host machine -// Shuts down Docker containers and Docker network -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml down -``` + ```bash + docker build -f docker/Dockerfile.libzmq -t libzmq:local . + ``` -Note: if you have `mpd` running on your system, you need to stop it -using: + 1. Build Images -``` bash -$ sudo systemctl stop mpd.socket -$ sudo mpd --kill -``` + ```bash + docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml build + ``` -Otherwise you might get the error message: + 1. Run Docker Environment -> Runs the entire Phoniebox environment -``` bash -$ docker-compose -f docker-compose.yml -f docker-compose.linux.yml up -Starting mpd ... -Starting mpd ... error -(...) -Error starting userland proxy: listen tcp4 0.0.0.0:6600: bind: address already in use -``` + ```bash + docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml up + ``` -Read these threads for details: [thread 1](https://unix.stackexchange.com/questions/456909/socket-already-in-use-but-is-not-listed-mpd) and [thread 2](https://stackoverflow.com/questions/5106674/error-address-already-in-use-while-binding-socket-with-address-but-the-port-num/5106755#5106755) + * Shuts down Docker containers and Docker network + + ```bash + docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml down + ``` + +
## Test & Develop @@ -169,7 +216,7 @@ restart your `jukebox` container. Update the below path with your specific host environment. ``` bash -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.[ENVIRONMENT].yml restart jukebox +docker-compose -f docker/docker-compose.yml -f docker/docker-compose.[ENVIRONMENT].yml restart jukebox ``` ## Known issues @@ -189,7 +236,7 @@ details. If you notice the following exception while running MPD in Docker, it refers to a incorrect setup of your Mac host Pulseaudio. -``` +```text mpd | ALSA lib pulse.c:242:(pulse_connect) PulseAudio: Unable to connect: Connection refused mpd | exception: Failed to read mixer for 'Global ALSA->Pulse stream': failed to attach to pulse: Connection refused ``` @@ -197,15 +244,20 @@ mpd | exception: Failed to read mixer for 'Global ALSA->Pulse stream': fail To fix the issue, try the following. 1. Stop your Pulseaudio service - ``` + + ```bash brew service stop pulseaudio ``` -2. Start Pulseaudio with this command - ``` + +1. Start Pulseaudio with this command + + ```bash pulseaudio --load=module-native-protocol-tcp --exit-idle-time=-1 --daemon ``` -3. Check if daemon is working - ``` + +1. Check if daemon is working + + ```bash pulseaudio --check -v ``` @@ -213,7 +265,26 @@ Everything else should have been set up properly as a [prerequisite](#mac) * [Source](https://gist.github.com/seongyongkim/b7d630a03e74c7ab1c6b53473b592712) +#### `mpd` issues on Linux +If you have `mpd` running on your system, you need to stop it using: + +``` bash +sudo systemctl stop mpd.socket +sudo mpd --kill +``` + +Otherwise you might get the error message: + +``` bash +docker-compose -f docker-compose.yml -f docker-compose.linux.yml up +Starting mpd ... +Starting mpd ... error +(...) +Error starting userland proxy: listen tcp4 0.0.0.0:6600: bind: address already in use +``` + +Read these threads for details: [thread 1](https://unix.stackexchange.com/questions/456909/socket-already-in-use-but-is-not-listed-mpd) and [thread 2](https://stackoverflow.com/questions/5106674/error-address-already-in-use-while-binding-socket-with-address-but-the-port-num/5106755#5106755) #### Other error messages @@ -265,12 +336,11 @@ jukebox | 319:server.py - jb.pub.server - host.timer.cputemp If you encounter the following error, refer to [Pulseaudio issues on Mac](#pulseaudio-issue-on-mac). -``` +``` bash jukebox | 21.12.2023 08:50:09 - 629:plugs.py - jb.plugin - MainThread - ERROR - Ignoring failed package load finalizer: 'volume.finalize()' jukebox | 21.12.2023 08:50:09 - 630:plugs.py - jb.plugin - MainThread - ERROR - Reason: NameError: name 'pulse_control' is not defined ``` - ## Appendix ### Individual Docker Image @@ -281,8 +351,8 @@ run `mpd` or `webapp`. The following command can be run on a Mac. ``` bash -$ docker build -f docker/jukebox.Dockerfile -t jukebox . -$ docker run -it --rm \ +docker build -f docker/Dockerfile.jukebox -t jukebox . +docker run -it --rm \ -v $(PWD)/src/jukebox:/home/pi/RPi-Jukebox-RFID/src/jukebox \ -v $(PWD)/shared/audiofolders:/home/pi/RPi-Jukebox-RFID/shared/audiofolders \ -v ~/.config/pulse:/root/.config/pulse \ @@ -291,6 +361,19 @@ $ docker run -it --rm \ --name jukebox jukebox ``` +## Testing ``evdev`` devices in Linux + +To test the [event device capabilities](../builders/event-devices.md) in docker, the device needs to be made available to the container. + +Mount the device into the container by configuring the appropriate device in a `devices` section of the `jukebox` service in the docker compose file. For example: + +```yaml + jukebox: + ... + devices: + - /dev/input/event3:/dev/input/event3 +``` + ### Resources #### Mac diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index aa7cefecc..c48c34a41 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -104,6 +104,12 @@ * [pbox\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.pbox_set_state) * [que\_set\_pbox](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_pbox) * [create\_outputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_outputs) +* [components.rfid.hardware.generic\_nfcpy.description](#components.rfid.hardware.generic_nfcpy.description) +* [components.rfid.hardware.generic\_nfcpy.generic\_nfcpy](#components.rfid.hardware.generic_nfcpy.generic_nfcpy) + * [ReaderClass](#components.rfid.hardware.generic_nfcpy.generic_nfcpy.ReaderClass) + * [cleanup](#components.rfid.hardware.generic_nfcpy.generic_nfcpy.ReaderClass.cleanup) + * [stop](#components.rfid.hardware.generic_nfcpy.generic_nfcpy.ReaderClass.stop) + * [read\_card](#components.rfid.hardware.generic_nfcpy.generic_nfcpy.ReaderClass.read_card) * [components.rfid.hardware.generic\_usb.description](#components.rfid.hardware.generic_usb.description) * [components.rfid.hardware.generic\_usb.generic\_usb](#components.rfid.hardware.generic_usb.generic_usb) * [components.rfid.hardware.rc522\_spi.description](#components.rfid.hardware.rc522_spi.description) @@ -1948,6 +1954,61 @@ Add all output devices to the GUI List of all added GUI objects + + +# components.rfid.hardware.generic\_nfcpy.description + +List of supported devices https://nfcpy.readthedocs.io/en/latest/overview.html + + + + +# components.rfid.hardware.generic\_nfcpy.generic\_nfcpy + + + +## ReaderClass Objects + +```python +class ReaderClass(ReaderBaseClass) +``` + +The reader class for nfcpy supported NFC card readers. + + + + +#### cleanup + +```python +def cleanup() +``` + +The cleanup function: free and release all resources used by this card reader (if any). + + + + +#### stop + +```python +def stop() +``` + +This function is called to tell the reader to exit its reading function. + + + + +#### read\_card + +```python +def read_card() -> str +``` + +Blocking or non-blocking function that waits for a new card to appear and return the card's UID as string + + # components.rfid.hardware.generic\_usb.description @@ -2659,7 +2720,7 @@ def stop_autohotspot() Stop auto hotspot functionality -Basically disabling the cronjob and running the script one last time manually +Stopping and disabling the timer and running the service one last time manually @@ -2671,9 +2732,9 @@ Basically disabling the cronjob and running the script one last time manually def start_autohotspot() ``` -start auto hotspot functionality +Start auto hotspot functionality -Basically enabling the cronjob and running the script one time manually +Enabling and starting the timer (timer will start the service) @@ -2966,35 +3027,7 @@ class battmon_ads1015(BatteryMonitorBase.BattmonBase) Battery Monitor based on a ADS1015 -> [!CAUTION] -> Lithium and other batteries are dangerous and must be treated with care. -> Rechargeable Lithium Ion batteries are potentially hazardous and can -> present a serious **FIRE HAZARD** if damaged, defective or improperly used. -> Do not use this circuit to a lithium ion battery without expertise and -> training in handling and use of batteries of this type. -> Use appropriate test equipment and safety protocols during development. -> There is no warranty, this may not work as expected or at all! - -This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: - - 3.3V - + - | - .----o----. - ___ | | SDA - .--------|___|---o----o---------o AIN0 o------ - | 2MΩ | | | | SCL - | .-. | | ADS1015 o------ - --- | | --- | | - Battery - 1.5MΩ| | ---100nF '----o----' - 2.9V-4.2V| '-' | | - | | | | - === === === === - -Attention: -* the circuit is constantly draining the battery! (leak current up to: 2.1µA) -* the time between sample needs to be a minimum 1sec with this high impedance voltage divider - don't use the continuous conversion method! +See [Battery Monitor documentation](../../builders/components/power/batterymonitor.md) diff --git a/documentation/developers/documentation.md b/documentation/developers/documentation.md new file mode 100644 index 000000000..51cd8cda2 --- /dev/null +++ b/documentation/developers/documentation.md @@ -0,0 +1,39 @@ +# Documentation with Markdown + +We use markdown for documentation. Please add/update documentation in `documentation`. + +## Linting + +To ensure a consistent documentation we lint markdown files. + +We use [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) for linting. + +`.markdownlint-cli2.yaml` configures linting consistently for the Github Action, the pre-commit hook, manual linting and the [markdownlint extension](https://github.com/DavidAnson/vscode-markdownlint) for Visual Studio Code. + +You can start a manual check, if you call `run_markdownlint.sh`. + +If markdown files are changed and the pre-commit hook is enabled, `run_markdownlint.sh` is triggered on commits. + +After creating a PR or pushing to the repo a Github Action triggers the linter, if markdown files are changed (see `.github/workflows/markdown_v3.yml`). + +### Disabling Rules + +> [!NOTE] +> Please use disabling rules with caution and always try to fix the violation first. + +A few rules are globally disabled in `.markdownlint-cli2.yaml` (see section `config`). + +If you want to disable a rule for a specific section of a markdown file you can use + +```markdown + +section where MD010 should be ignored + +``` + +### References + +* +* Rules: + * + * diff --git a/documentation/developers/rfid/README.md b/documentation/developers/rfid/README.md index 0b2df4db3..f0db4fd6a 100644 --- a/documentation/developers/rfid/README.md +++ b/documentation/developers/rfid/README.md @@ -11,4 +11,3 @@ * [Generic Readers without HID (NFCpy)](generic_nfcpy.md) * [Mock Reader](mock_reader.md) * [Template Reader](template_reader.md) - diff --git a/documentation/developers/rfid/generic_nfcpy.md b/documentation/developers/rfid/generic_nfcpy.md index 76de98d88..27c6ececb 100644 --- a/documentation/developers/rfid/generic_nfcpy.md +++ b/documentation/developers/rfid/generic_nfcpy.md @@ -11,13 +11,14 @@ driver, and thus cannot be used with the [genericusb](genericusb.md) module. Als > The setup will do this automatically, so make sure the device is connected > before running the [RFID reader configuration tool](../coreapps.md#RFID-Reader). -# Configuration +## Configuration -The installation script will scan for compatible devices and will assist in configuration. +The installation script will scan for compatible devices and will assist in configuration. By setting `rfid > readers > generic_nfcpy > config > device_path` in `shared/settings/rfid.yaml` you can override the device location. By specifying an explicit device location it is possible to use multiple readers compatible with NFCpy. Example configuration for a usb-device with vendor ID 072f and product ID 2200: + ```yaml rfid: readers: @@ -33,4 +34,4 @@ rfid: alias: pause ``` -For possible values see the `path` parameter in this [nfcpy documentation](https://nfcpy.readthedocs.io/en/latest/modules/clf.html#nfc.clf.ContactlessFrontend.open) \ No newline at end of file +For possible values see the `path` parameter in this [nfcpy documentation](https://nfcpy.readthedocs.io/en/latest/modules/clf.html#nfc.clf.ContactlessFrontend.open) diff --git a/documentation/developers/rfid/mfrc522_spi.md b/documentation/developers/rfid/mfrc522_spi.md index 8a04f729e..363df3836 100644 --- a/documentation/developers/rfid/mfrc522_spi.md +++ b/documentation/developers/rfid/mfrc522_spi.md @@ -57,7 +57,8 @@ If true all card read-outs will be logged, even when card is permanently on read The following pin-out is for the default SPI Bus 0 on Raspberry Pins. -*MFRC522 default wiring (spi_bus=0, spi_ce=0)* +### MFRC522 default wiring (spi_bus=0, spi_ce=0) + |Pin Board Name |Function |RPI GPIO |RPI Pin | |----------------|----------|----------|---------| |SDA |CE |GPIO8 |24 | diff --git a/documentation/developers/rfid/mock_reader.md b/documentation/developers/rfid/mock_reader.md index 4d5a3ea36..d6cac8cd0 100644 --- a/documentation/developers/rfid/mock_reader.md +++ b/documentation/developers/rfid/mock_reader.md @@ -6,7 +6,7 @@ machine - probably in a Python virtual environment. **place-capable**: yes -If you [mock the GPIO pins](../../../src/jukebox/components/gpio/gpioz/README.rst#use-mock-pins), this GUI will show the GPIO devices. +If you [mock the GPIO pins](../../builders/gpio.md#use-mock-pins), this GUI will show the GPIO devices. ![image](mock_reader.png) diff --git a/documentation/developers/rfid/pn532_i2c.md b/documentation/developers/rfid/pn532_i2c.md index d60cb2e54..33b7e3f51 100644 --- a/documentation/developers/rfid/pn532_i2c.md +++ b/documentation/developers/rfid/pn532_i2c.md @@ -29,7 +29,7 @@ You can usually pick up a board at ## Board Connections -*Default wiring* +### Default wiring | PN532 | RPI GPIO | RPI Pin | |-------|--------------|---------| @@ -45,9 +45,9 @@ PI's own voltage regulator. ## Jumpers -*Jumper settings for I2C protocol* +### Jumper settings for I2C protocol -Jumper | Position --------|---------- -SEL0 | ON -SEL1 | OFF +| Jumper | Position | +|--------|----------| +|SEL0 | ON | +|SEL1 | OFF | diff --git a/documentation/developers/rfid/template_reader.md b/documentation/developers/rfid/template_reader.md index 5b8458691..e02f56c69 100644 --- a/documentation/developers/rfid/template_reader.md +++ b/documentation/developers/rfid/template_reader.md @@ -1,9 +1,8 @@ # Template Reader -*Template for creating and integrating a new RFID Reader* - > [!NOTE] +> Template for creating and integrating a new RFID Reader. > For developers only This template provides the skeleton API for a new Reader. If you follow diff --git a/documentation/developers/webapp.md b/documentation/developers/webapp.md index 2e8504337..30b791816 100644 --- a/documentation/developers/webapp.md +++ b/documentation/developers/webapp.md @@ -19,7 +19,7 @@ sudo apt-get -y update && sudo apt-get -y install nodejs The Web App is a React application based on [Create React App](https://create-react-app.dev/). To start a development server, run the following command: -``` +```bash cd ~/RPi-Jukebox-RFID/src/webapp npm install # Just the first time or when dependencies change npm start @@ -37,7 +37,7 @@ cd ~/RPi-Jukebox-RFID/src/webapp; \ After a successfull build you might need to restart the web server. -``` +```bash sudo systemctl restart nginx.service ``` @@ -71,6 +71,7 @@ Use the [provided script](#build-the-web-app) to rebuild the Web App. It sets th If you need to run the commands manually, make sure to have enough memory available (min. 512 MB). The following commands might help. Set the swapsize to 512 MB (and deactivate swapfactor). Adapt accordingly if you have a SD Card with small capacity. + ```bash sudo dphys-swapfile swapoff sudo sed -i "s|.*CONF_SWAPSIZE=.*|CONF_SWAPSIZE=512|g" /etc/dphys-swapfile @@ -80,6 +81,7 @@ sudo dphys-swapfile swapon ``` Set Node's maximum amount of memory. Memory must be available. + ``` bash export NODE_OPTIONS=--max-old-space-size=512 npm run build @@ -105,7 +107,6 @@ Node tried to allocate more memory than available on the system. See [JavaScript heap out of memory](#javascript-heap-out-of-memory) - ### Client network socket disconnected ``` {.bash emphasize-lines="8,9"} @@ -122,12 +123,12 @@ npm ERR! network 'proxy' config is set properly. See: 'npm help config' #### Reason -The network connection is too slow or has issues. -This tends to happen on `armv6l` devices where building takes significantly more time due to limited resources. +The network connection is too slow or has issues. +This tends to happen on `armv6l` devices where building takes significantly more time due to limited resources. #### Solution -Try to use an ethernet connection. A reboot and/or running the script multiple times might also help ([Build produces EOF errors](#build-produces-eof-errors) might occur). +Try to use an ethernet connection. A reboot and/or running the script multiple times might also help ([Build produces EOF errors](#build-produces-eof-errors) might occur). If the error still persists, try to raise the timeout for npm package resolution. @@ -144,6 +145,7 @@ A previous run failed during installation and left a package corrupted. #### Solution Remove the mode packages and rerun again the script. + ``` {.bash emphasize-lines="8,9"} rm -rf node_modules ``` diff --git a/installation/routines/setup_jukebox_core.sh b/installation/routines/setup_jukebox_core.sh index f5ef2eec2..2e4fbad39 100644 --- a/installation/routines/setup_jukebox_core.sh +++ b/installation/routines/setup_jukebox_core.sh @@ -86,7 +86,7 @@ _jukebox_core_build_and_install_pyzmq() { fi ZMQ_PREFIX="${JUKEBOX_ZMQ_PREFIX}" ZMQ_DRAFT_API=1 \ - pip install -v --no-binary pyzmq 'pyzmq<26' + pip install -v 'pyzmq<26' --no-binary pyzmq else print_lc " Skipping. pyzmq already installed" fi diff --git a/requirements.txt b/requirements.txt index c172a7636..8ddfc881a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,11 +10,11 @@ wheel # Jukebox Core # For USB inputs (reader, buttons) and bluetooth buttons evdev +mutagen pyalsaaudio pulsectl python-mpd2 ruamel.yaml -python-slugify # For playlistgenerator requests # For the publisher event reactor loop: diff --git a/resources/default-settings/evdev.example.yaml b/resources/default-settings/evdev.example.yaml new file mode 100644 index 000000000..5fbcd57e8 --- /dev/null +++ b/resources/default-settings/evdev.example.yaml @@ -0,0 +1,59 @@ +devices: # A list of evdev devices each containing one or multiple input/output devices + joystick: # A nickname for a device + device_name: DragonRise Inc. Generic USB # Device name + exact: false # If true, the device name must match exactly, otherwise it is sufficient to contain the name + input_devices: + TogglePlayback: + type: Button + kwargs: + key_code: 299 + actions: + on_press: + alias: toggle + NextSong: + type: Button + kwargs: + key_code: 298 + actions: + on_press: + alias: next_song + PrevSong: + type: Button + kwargs: + key_code: 297 + actions: + on_press: + alias: prev_song + VolumeUp: + type: Button + kwargs: + key_code: 296 + actions: + on_press: + alias: change_volume + args: 5 + VolumeDown: + type: Button + kwargs: + key_code: 295 + actions: + on_press: + alias: change_volume + args: -5 + VolumeReset: + type: Button + kwargs: + key_code: 291 + actions: + on_press: + package: volume + plugin: ctrl + method: set_volume + args: [18] + Shutdown: + type: Button + kwargs: + key_code: 292 + actions: + on_press: + alias: shutdown \ No newline at end of file diff --git a/resources/default-settings/jukebox.default.yaml b/resources/default-settings/jukebox.default.yaml index c087cc024..b8e429333 100644 --- a/resources/default-settings/jukebox.default.yaml +++ b/resources/default-settings/jukebox.default.yaml @@ -87,6 +87,12 @@ playermpd: update_on_startup: true check_user_rights: true mpd_conf: ~/.config/mpd/mpd.conf + # Must be one of: 'none', 'stop', 'rewind': + end_of_playlist_next_action: none + # Must be one of: 'none', 'prev', 'rewind': + stopped_prev_action: prev + # Must be one of: 'none', 'next', 'rewind': + stopped_next_action: next rpc: tcp_port: 5555 websocket_port: 5556 diff --git a/run_markdownlint.sh b/run_markdownlint.sh new file mode 100755 index 000000000..0f890590e --- /dev/null +++ b/run_markdownlint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Runner script to ensure +# - independent from working directory + +# Change working directory to project root +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +PROJECT_ROOT="$SCRIPT_DIR" +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +# Run markdownlint-cli2 +./src/webapp/node_modules/.bin/markdownlint-cli2 --config .markdownlint-cli2.yaml "#node_modules" || { echo "ERROR: markdownlint-cli2 not found"; exit 1; } diff --git a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py index af193a37f..551759c14 100644 --- a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py +++ b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py @@ -37,36 +37,7 @@ class battmon_ads1015(BatteryMonitorBase.BattmonBase): """Battery Monitor based on a ADS1015 - > [!CAUTION] - > Lithium and other batteries are dangerous and must be treated with care. - > Rechargeable Lithium Ion batteries are potentially hazardous and can - > present a serious **FIRE HAZARD** if damaged, defective or improperly used. - > Do not use this circuit to a lithium ion battery without expertise and - > training in handling and use of batteries of this type. - > Use appropriate test equipment and safety protocols during development. - > There is no warranty, this may not work as expected or at all! - - This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: - - 3.3V - + - | - .----o----. - ___ | | SDA - .--------|___|---o----o---------o AIN0 o------ - | 2MΩ | | | | SCL - | .-. | | ADS1015 o------ - --- | | --- | | - Battery - 1.5MΩ| | ---100nF '----o----' - 2.9V-4.2V| '-' | | - | | | | - === === === === - - Attention: - * the circuit is constantly draining the battery! (leak current up to: 2.1µA) - * the time between sample needs to be a minimum 1sec with this high impedance voltage divider - don't use the continuous conversion method! - + See [Battery Monitor documentation](../../builders/components/power/batterymonitor.md) """ def __init__(self, cfg): diff --git a/src/jukebox/components/controls/bluetooth_audio_buttons/README.rst b/src/jukebox/components/controls/bluetooth_audio_buttons/README.rst deleted file mode 100644 index 87a7eab2b..000000000 --- a/src/jukebox/components/controls/bluetooth_audio_buttons/README.rst +++ /dev/null @@ -1,55 +0,0 @@ -When a bluetooth sound device (headphone, speakers) connects -attempt to automatically listen to it's buttons (play, next, ...) - -The bluetooth input device name is matched automatically from the -bluetooth sound card device name. During boot up, it is uncertain if the bluetooth device connects first, -or the Jukebox service is ready first. Therefore, -after service initialization, already connected bluetooth sound devices are scanned and an attempt is made -to find their input buttons. - -.. note:: If the automatic matching fails, there currently is no - manual configuration option. Open an issue ticket if you have problems with the automatic matching. - -Button key codes are standardized and by default the buttons -play, pause, next song, previous song are recognized. Volume up/down is handled independently -from this module by PulseAudio and the bluetooth audio transmission protocol. - -The module needs to be enabled in the main configuration file with: - -.. code-block:: yaml - - bluetooth_audio_buttons: - enable: true - - -Custom key bindings ---------------------- - -You may change or extend the actions assigned to a button in the configuration. If the configuration contains -a block 'mapping', the default button-action mapping is *completely* replaced with the new mapping. The definitions for -each key looks like ``key-code: {rpc_command_definition}``. -The RPC command follows the regular RPC command rules as defined in :ref:`userguide/rpc_commands:RPC Commands`. - -.. code-block:: yaml - - bluetooth_audio_buttons: - enable: true - mapping: - # Play & pause both map to toggle which is also the usual behaviour of headsets - 200: - alias: toggle - 201: - alias: toggle - # Re-map next song button, to set defined output volume (for some fun) - 163: - package: volume - plugin: ctrl - method: set_volume - args: [18] - # Re-map prev song button to shutdown - 165: - alias: shutdown - - -Key codes can be found in the log files. Press the various buttons on your headset, while watching the -logs with e.g. ``tail -f shared/logs/app.log``. Look for entries like ``No callback registered for button ...``. diff --git a/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py b/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py index 4d17f398e..f03a447cd 100644 --- a/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py +++ b/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py @@ -49,8 +49,8 @@ def activate(device_name: str, exact: bool = True, open_initial_delay: float = 0 # Do a bit of housekeeping: Delete dead threads listener = list(filter(lambda x: x.is_alive(), listener)) # Check that there is no running thread for this device already - for ll in listener: - if ll.device_request == device_name and ll.is_alive(): + for thread in listener: + if thread.device_request == device_name and thread.is_alive(): logger.debug(f"Button listener thread already active for '{device_name}'") return diff --git a/src/jukebox/components/controls/common/evdev_listener.py b/src/jukebox/components/controls/common/evdev_listener.py index a4279afda..a1f336758 100644 --- a/src/jukebox/components/controls/common/evdev_listener.py +++ b/src/jukebox/components/controls/common/evdev_listener.py @@ -160,7 +160,7 @@ def run(self): self._connect() except FileNotFoundError as e: # This error occurs, if opening the bluetooth input device fails - logger.debug(f"{e} (attempt: {idx+1}/{self.open_retry_cnt}). Retrying in {self.open_retry_delay}") + logger.debug(f"{e} (attempt: {idx + 1}/{self.open_retry_cnt}). Retrying in {self.open_retry_delay}") time.sleep(self.open_retry_delay) except AttributeError as e: # This error occurs, when the device can be found, but does not have the mandatory keys diff --git a/src/jukebox/components/controls/event_devices/__init__.py b/src/jukebox/components/controls/event_devices/__init__.py new file mode 100644 index 000000000..407a73f14 --- /dev/null +++ b/src/jukebox/components/controls/event_devices/__init__.py @@ -0,0 +1,221 @@ +""" +Plugin to register event_devices (ie USB controllers, keyboards etc) in a + generic manner. + +This effectively does: + + * parse the configured event devices from the evdev.yaml + * setup listen threads + +""" +from __future__ import annotations + +import logging +from typing import Callable +from typing import Tuple + +import jukebox.cfghandler +import jukebox.plugs as plugin +import jukebox.utils +from components.controls.common.evdev_listener import EvDevKeyListener + +logger = logging.getLogger("jb.EventDevice") +cfg_main = jukebox.cfghandler.get_handler("jukebox") +cfg_evdev = jukebox.cfghandler.get_handler("eventdevices") + +# Keep track of all active key event listener threads +# Removal of dead listener threads is done in lazy fashion: +# only on a new connect are dead threads removed +listener: list[EvDevKeyListener] = [] +# Running count of all created listener threads for unique thread naming IDs +listener_cnt = 0 + +#: Indicates that the module is enabled and loaded w/o errors +IS_ENABLED: bool = False +#: The path of the config file the event device configuration was loaded from +CONFIG_FILE: str + +# Constants +_TYPE_BUTTON = 'Button' +_ACTION_ON_PRESS = 'on_press' + +_SUPPORTED_TYPES = [_TYPE_BUTTON] +_SUPPORTED_ACTIONS = {_TYPE_BUTTON: _ACTION_ON_PRESS} + + +@plugin.register +def activate( + device_name: str, + button_callbacks: dict[int, Callable], + exact: bool = True, + mandatory_keys: set[int] | None = None, +): + """Activate an event device listener + + :param device_name: device name + :type device_name: str + :param button_callbacks: mapping of event + code to RPC + :type button_callbacks: dict[int, Callable] + :param exact: Should the device_name match exactly + (default, false) or be a substring of the name? + :type exact: bool, optional + :param mandatory_keys: Mandatory event ids the + device needs to support. Defaults to None + to require all ids from the button_callbacks + :type mandatory_keys: set[int] | None, optional + """ + global listener + global listener_cnt + logger.debug("activate event device: %s", device_name) + # Do a bit of housekeeping: Delete dead threads + listener = list(filter(lambda x: x.is_alive(), listener)) + # Check that there is no running thread for this device already + for thread in listener: + if thread.device_name_request == device_name and thread.is_alive(): + logger.debug( + "Event device listener thread already active for '%s'", + device_name, + ) + return + + listener_cnt += 1 + new_listener = EvDevKeyListener( + device_name_request=device_name, + exact_name=exact, + thread_name=f"EvDevKeyListener-{listener_cnt}", + ) + + listener.append(new_listener) + if button_callbacks is not None: + new_listener.button_callbacks = button_callbacks + if mandatory_keys is not None: + new_listener.mandatory_keys = mandatory_keys + else: + new_listener.mandatory_keys = set(button_callbacks.keys()) + new_listener.start() + + +@plugin.initialize +def initialize(): + """Initialize event device button listener from config + + Initializes event buttons from the main configuration file. + Please see the documentation `builders/event-devices.md` for a specification of the format. + """ + global IS_ENABLED + global CONFIG_FILE + IS_ENABLED = False + enable = cfg_main.setndefault('evdev', 'enable', value=False) + CONFIG_FILE = cfg_main.setndefault('evdev', 'config_file', value='../../shared/settings/evdev.yaml') + if not enable: + return + try: + jukebox.cfghandler.load_yaml(cfg_evdev, CONFIG_FILE) + except Exception as e: + logger.error(f"Disable Event Devices due to error loading evdev config file. {e.__class__.__name__}: {e}") + return + + IS_ENABLED = True + + with cfg_evdev: + for name, config in cfg_evdev.getn( + "devices", + default={}, + ).items(): + logger.debug("activate %s", name) + try: + device_name, exact, button_callbacks = parse_device_config(config) + except Exception as e: + logger.error(f"Error parsing event device config for '{name}'. {e.__class__.__name__}: {e}") + continue + + logger.debug( + f'Call activate with: "{device_name}" and exact: {exact}', + ) + activate( + device_name, + button_callbacks=button_callbacks, + exact=exact, + ) + + +def parse_device_config(config: dict) -> Tuple[str, bool, dict[int, Callable]]: + """Parse the device configuration from the config file + + :param config: The configuration of the device + :type config: dict + :return: The parsed device configuration + :rtype: Tuple[str, bool, dict[int, Callable]] + """ + device_name = config.get("device_name") + if device_name is None: + raise ValueError("'device_name' is required but missing") + exact = bool(config.get("exact", False)) + input_devices = config.get("input_devices", {}) + # Raise warning if not used config present + if 'output_devices' in config: + logger.warning( + "Output devices are not yet supported for event devices", + ) + + # Parse input devices and convert them to button mappings. + # Due to the current implementation of the Event Device Listener, + # only the 'on_press' action is supported. + button_mapping = _input_devices_to_key_mapping(input_devices) + button_callbacks: dict[int, Callable] = {} + for key, action_request in button_mapping.items(): + button_callbacks[key] = jukebox.utils.bind_rpc_command( + action_request, + dereference=False, + logger=logger, + ) + return device_name, exact, button_callbacks + + +def _input_devices_to_key_mapping(input_devices: dict) -> dict: + """Convert input devices to key mapping + + Currently this only supports 'button' input devices with the 'on_press' action. + + :param input_devices: The input devices + :type input_devices: dict + :return: The mapping of key_code to action + :rtype: dict + """ + mapping = {} + for nickname, device in input_devices.items(): + input_type = device.get('type') + if input_type not in _SUPPORTED_TYPES: + logger.warning( + f"Input '{nickname}' device type '{input_type}' is not supported", + ) + continue + + key_code = device.get('kwargs', {}).get('key_code') + if key_code is None: + logger.warning( + f"Input '{nickname}' has no key_code and cannot be mapped.", + ) + continue + + actions = device.get('actions') + + for action_name, action in actions.items(): + if action_name not in _SUPPORTED_ACTIONS[_TYPE_BUTTON]: + logger.warning( + f"Input '{nickname}' has unsupported action '{action_name}'.\n" + f"Currently supported actions: {_SUPPORTED_ACTIONS}", + ) + if action_name == _ACTION_ON_PRESS: + mapping[key_code] = action + + return mapping + + +@plugin.atexit +def atexit(**ignored_kwargs): + global listener + for ll in listener: + ll.stop() + return listener diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index 49f630224..86dbc60ab 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -87,7 +87,7 @@ import logging import time import functools -from slugify import slugify +from pathlib import Path import components.player import jukebox.cfghandler import jukebox.utils as utils @@ -156,6 +156,31 @@ def __init__(self): self.second_swipe_action = None self.decode_2nd_swipe_option() + self.end_of_playlist_next_action = utils.get_config_action(cfg, + 'playermpd', + 'end_of_playlist_next_action', + 'none', + {'rewind': self.rewind, + 'stop': self.stop, + 'none': lambda: None}, + logger) + self.stopped_prev_action = utils.get_config_action(cfg, + 'playermpd', + 'stopped_prev_action', + 'prev', + {'rewind': self.rewind, + 'prev': self._prev_in_stopped_state, + 'none': lambda: None}, + logger) + self.stopped_next_action = utils.get_config_action(cfg, + 'playermpd', + 'stopped_next_action', + 'next', + {'rewind': self.rewind, + 'next': self._next_in_stopped_state, + 'none': lambda: None}, + logger) + self.mpd_client = mpd.MPDClient() self.coverart_cache_manager = CoverartCacheManager() @@ -327,15 +352,48 @@ def pause(self, state: int = 1): @plugs.tag def prev(self): logger.debug("Prev") + if self.mpd_status['state'] == 'stop': + logger.debug('Player is stopped, calling stopped_prev_action') + return self.stopped_prev_action() + try: + with self.mpd_lock: + self.mpd_client.previous() + except mpd.base.CommandError: + # This shouldn't happen in reality, but we still catch + # this error to avoid crashing the player thread: + logger.warning('Failed to go to previous song, ignoring') + + def _prev_in_stopped_state(self): with self.mpd_lock: - self.mpd_client.previous() + self.mpd_client.play(max(0, int(self.mpd_status['pos']) - 1)) @plugs.tag def next(self): """Play next track in current playlist""" logger.debug("Next") + if self.mpd_status['state'] == 'stop': + logger.debug('Player is stopped, calling stopped_next_action') + return self.stopped_next_action() + playlist_len = int(self.mpd_status.get('playlistlength', -1)) + current_pos = int(self.mpd_status.get('pos', 0)) + if current_pos == playlist_len - 1: + logger.debug(f'next() called during last song ({current_pos}) of ' + f'playlist (len={playlist_len}), running end_of_playlist_next_action.') + return self.end_of_playlist_next_action() + try: + with self.mpd_lock: + self.mpd_client.next() + except mpd.base.CommandError: + # This shouldn't happen in reality, but we still catch + # this error to avoid crashing the player thread: + logger.warning('Failed to go to next song, ignoring') + + def _next_in_stopped_state(self): + pos = int(self.mpd_status['pos']) + 1 + if pos > int(self.mpd_status['playlistlength']) - 1: + return self.end_of_playlist_next_action() with self.mpd_lock: - self.mpd_client.next() + self.mpd_client.play(pos) @plugs.tag def seek(self, new_time): @@ -350,7 +408,7 @@ def rewind(self): Note: Will not re-read folder config, but leave settings untouched""" logger.debug("Rewind") with self.mpd_lock: - self.mpd_client.play(1) + self.mpd_client.play(0) @plugs.tag def replay(self): @@ -521,40 +579,10 @@ def play_card(self, folder: str, recursive: bool = False): @plugs.tag def get_single_coverart(self, song_url): - """ - Saves the album art image to a cache and returns the filename. - """ - base_filename = slugify(song_url) - - try: - metadata_list = self.mpd_client.listallinfo(song_url) - metadata = {} - if metadata_list: - metadata = metadata_list[0] - - if 'albumartist' in metadata and 'album' in metadata: - base_filename = slugify(f"{metadata['albumartist']}-{metadata['album']}") - - cache_filename = self.coverart_cache_manager.find_file_by_hash(base_filename) - - if cache_filename: - return cache_filename + mp3_file_path = Path(components.player.get_music_library_path(), song_url).expanduser() + cache_filename = self.coverart_cache_manager.get_cache_filename(mp3_file_path) - # Cache file does not exist - # Fetch cover art binary - album_art_data = self.mpd_client.readpicture(song_url) - - # Save to cache - cache_filename = self.coverart_cache_manager.save_to_cache(base_filename, album_art_data) - - return cache_filename - - except mpd.base.CommandError as e: - logger.error(f"{e.__class__.__qualname__}: {e} at uri {song_url}") - except Exception as e: - logger.error(f"{e.__class__.__qualname__}: {e} at uri {song_url}") - - return "" + return cache_filename @plugs.tag def get_album_coverart(self, albumartist: str, album: str): @@ -562,6 +590,14 @@ def get_album_coverart(self, albumartist: str, album: str): return self.get_single_coverart(song_list[0]['file']) + @plugs.tag + def flush_coverart_cache(self): + """ + Deletes the Cover Art Cache + """ + + return self.coverart_cache_manager.flush_cache() + @plugs.tag def get_folder_content(self, folder: str): """ diff --git a/src/jukebox/components/playermpd/coverart_cache_manager.py b/src/jukebox/components/playermpd/coverart_cache_manager.py index a7ae12eef..bb2346497 100644 --- a/src/jukebox/components/playermpd/coverart_cache_manager.py +++ b/src/jukebox/components/playermpd/coverart_cache_manager.py @@ -1,26 +1,90 @@ -import os +from mutagen.mp3 import MP3 +from mutagen.id3 import ID3, APIC +from pathlib import Path +import hashlib +import logging +from queue import Queue +from threading import Thread import jukebox.cfghandler +COVER_PREFIX = 'cover' +NO_COVER_ART_EXTENSION = 'no-art' +NO_CACHE = '' +CACHE_PENDING = 'CACHE_PENDING' + +logger = logging.getLogger('jb.CoverartCacheManager') cfg = jukebox.cfghandler.get_handler('jukebox') class CoverartCacheManager: def __init__(self): coverart_cache_path = cfg.setndefault('webapp', 'coverart_cache_path', value='../../src/webapp/build/cover-cache') - self.cache_folder_path = os.path.expanduser(coverart_cache_path) + self.cache_folder_path = Path(coverart_cache_path).expanduser() + self.write_queue = Queue() + self.worker_thread = Thread(target=self.process_write_requests) + self.worker_thread.daemon = True # Ensure the thread closes with the program + self.worker_thread.start() + + def generate_cache_key(self, base_filename: str) -> str: + return f"{COVER_PREFIX}-{hashlib.sha256(base_filename.encode()).hexdigest()}" + + def get_cache_filename(self, mp3_file_path: str) -> str: + base_filename = Path(mp3_file_path).stem + cache_key = self.generate_cache_key(base_filename) + + for path in self.cache_folder_path.iterdir(): + if path.stem == cache_key: + if path.suffix == f".{NO_COVER_ART_EXTENSION}": + return NO_CACHE + return path.name + + self.save_to_cache(mp3_file_path) + return CACHE_PENDING + + def save_to_cache(self, mp3_file_path: str): + self.write_queue.put(mp3_file_path) - def find_file_by_hash(self, hash_value): - for filename in os.listdir(self.cache_folder_path): - if filename.startswith(hash_value): - return filename - return None + 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) - def save_to_cache(self, base_filename, album_art_data): - mime_type = album_art_data['type'] - file_extension = 'jpg' if mime_type == 'image/jpeg' else mime_type.split('/')[-1] - cache_filename = f"{base_filename}.{file_extension}" + cache_filename = f"{cache_key}.{file_extension}" + full_path = self.cache_folder_path / cache_filename # Works due to Pathlib - with open(os.path.join(self.cache_folder_path, cache_filename), 'wb') as file: - file.write(album_art_data['binary']) + with full_path.open('wb') as file: + file.write(data) + logger.debug(f"Created file: {cache_filename}") return cache_filename + + def _extract_album_art(self, mp3_file_path: str) -> tuple: + try: + audio_file = MP3(mp3_file_path, ID3=ID3) + except Exception as e: + logger.error(f"Error reading MP3 file {mp3_file_path}: {e}") + return (NO_COVER_ART_EXTENSION, b'') + + 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) + + return (NO_COVER_ART_EXTENSION, b'') + + def process_write_requests(self): + while True: + mp3_file_path = self.write_queue.get() + try: + self._save_to_cache(mp3_file_path) + except Exception as e: + logger.error(f"Error processing write request: {e}") + self.write_queue.task_done() + + def flush_cache(self): + for path in self.cache_folder_path.iterdir(): + if path.is_file(): + path.unlink() + logger.debug(f"Deleted cached file: {path.name}") + logger.info("Cache flushed successfully.") diff --git a/src/jukebox/components/rpc_command_alias.py b/src/jukebox/components/rpc_command_alias.py index f6e238559..5a7820733 100644 --- a/src/jukebox/components/rpc_command_alias.py +++ b/src/jukebox/components/rpc_command_alias.py @@ -75,6 +75,10 @@ 'method': 'repeat', 'note': 'Repeat', 'ignore_card_removal_action': True}, + 'flush_coverart_cache': { + 'package': 'player', + 'plugin': 'ctrl', + 'method': 'flush_coverart_cache'}, # VOLUME 'set_volume': { diff --git a/src/jukebox/components/volume/__init__.py b/src/jukebox/components/volume/__init__.py index 9dd827e4a..b99e94616 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -424,7 +424,7 @@ def _publish_outputs(self, pulse_inst: pulsectl.Pulse): def _set_output(self, pulse_inst: pulsectl.Pulse, sink_index: int): error_state = 1 if not 0 <= sink_index < len(self._sink_list): - logger.error(f"Sink index '{sink_index}' out of range (0..{len(self._sink_list)-1}). " + logger.error(f"Sink index '{sink_index}' out of range (0..{len(self._sink_list) - 1}). " f"Did you configure your secondary output device?") else: # Before we switch the sink, check the new sinks volume levels... diff --git a/src/jukebox/jukebox/playlistgenerator.py b/src/jukebox/jukebox/playlistgenerator.py index b9f0223c6..e7f5b50d3 100755 --- a/src/jukebox/jukebox/playlistgenerator.py +++ b/src/jukebox/jukebox/playlistgenerator.py @@ -275,7 +275,12 @@ def get_directory_content(self, path='.'): logger.error(f" {e.__class__.__name__}: {e}") else: for m in content: - self.playlist.append({'type': TYPE_DECODE[m.filetype], 'name': m.name, 'path': m.path}) + self.playlist.append({ + 'type': TYPE_DECODE[m.filetype], + 'name': m.name, + 'path': m.path, + 'relpath': os.path.relpath(m.path, self._music_library_base_path) + }) def _parse_nonrecusive(self, path='.'): return [x.path for x in self._get_directory_content(path) if x.filetype != TYPE_DIR] @@ -294,7 +299,7 @@ def _parse_recursive(self, path='.'): return recursive_playlist def parse(self, path='.', recursive=False): - """Parse the folder ``path`` and create a playlist from it's content + """Parse the folder ``path`` and create a playlist from its content :param path: Path to folder **relative** to ``music_library_base_path`` :param recursive: Parse folder recursivley, or stay in top-level folder diff --git a/src/jukebox/jukebox/utils.py b/src/jukebox/jukebox/utils.py index dbd647490..4cc0270ae 100644 --- a/src/jukebox/jukebox/utils.py +++ b/src/jukebox/jukebox/utils.py @@ -183,6 +183,19 @@ def rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) -> str: return readable +def get_config_action(cfg, section, option, default, valid_actions_dict, logger): + """ + Looks up the given {section}.{option} config option and returns + the associated entry from valid_actions_dict, if valid. Falls back to the given + default otherwise. + """ + action = cfg.setndefault(section, option, value='').lower() + if action not in valid_actions_dict: + logger.error(f"Config {section}.{option} must be one of {valid_actions_dict.keys()}. Using default '{default}'") + action = default + return valid_actions_dict[action] + + def indent(doc, spaces=4): lines = doc.split('\n') for i in range(0, len(lines)): diff --git a/src/jukebox/jukebox/version.py b/src/jukebox/jukebox/version.py index d62ef3e93..a7f37c5e6 100644 --- a/src/jukebox/jukebox/version.py +++ b/src/jukebox/jukebox/version.py @@ -1,8 +1,8 @@ VERSION_MAJOR = 3 -VERSION_MINOR = 5 -VERSION_PATCH = 3 -VERSION_EXTRA = "" +VERSION_MINOR = 6 +VERSION_PATCH = 0 +VERSION_EXTRA = "alpha" # build a version string in compliance with the SemVer specification # https://semver.org/#semantic-versioning-specification-semver diff --git a/src/jukebox/run_configure_audio.py b/src/jukebox/run_configure_audio.py index 93f0a4c6a..191a25e5d 100644 --- a/src/jukebox/run_configure_audio.py +++ b/src/jukebox/run_configure_audio.py @@ -192,7 +192,7 @@ def query_sinks(pulse_config: PaConfigClass): # noqa: C901 if sink_is_equalizer(primary_signal_chain[sidx - 1]): pulse_config.enable_equalizer = False print(f"\n*** Equalizer already configured for '{pulse_config.primary}' with name\n" - f" '{primary_signal_chain[sidx-1].name}'. Shifting entry point...") + f" '{primary_signal_chain[sidx - 1].name}'. Shifting entry point...") pulse_config.primary = primary_signal_chain[sidx - 1].name sidx -= 1 except ValueError: diff --git a/src/webapp/package-lock.json b/src/webapp/package-lock.json index 814090555..218e67ab7 100644 --- a/src/webapp/package-lock.json +++ b/src/webapp/package-lock.json @@ -28,6 +28,9 @@ "react-scripts": "^5.0.1", "url": "^0.11.3", "uuid": "^9.0.1" + }, + "devDependencies": { + "markdownlint-cli2": "^0.12.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4042,6 +4045,18 @@ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", + "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -12284,6 +12299,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -12525,6 +12546,15 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -12663,11 +12693,165 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", + "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdownlint": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.33.0.tgz", + "integrity": "sha512-4lbtT14A3m0LPX1WS/3d1m7Blg+ZwiLq36WvjQqFGsX3Gik99NV+VXp/PW3n+Q62xyPdbvGOCfjPqjW+/SKMig==", + "dev": true, + "dependencies": { + "markdown-it": "14.0.0", + "markdownlint-micromark": "0.1.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.12.1.tgz", + "integrity": "sha512-RcK+l5FjJEyrU3REhrThiEUXNK89dLYNJCYbvOUKypxqIGfkcgpz8g08EKqhrmUbYfYoLC5nEYQy53NhJSEtfQ==", + "dev": true, + "dependencies": { + "globby": "14.0.0", + "jsonc-parser": "3.2.0", + "markdownlint": "0.33.0", + "markdownlint-cli2-formatter-default": "0.0.4", + "micromatch": "4.0.5", + "yaml": "2.3.4" + }, + "bin": { + "markdownlint-cli2": "markdownlint-cli2.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2-formatter-default": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.4.tgz", + "integrity": "sha512-xm2rM0E+sWgjpPn1EesPXx5hIyrN2ddUnUwnbCsD/ONxYtw3PX6LydvdH6dciWAoFDpwzbHM1TO7uHfcMd6IYg==", + "dev": true, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" + } + }, + "node_modules/markdownlint-cli2/node_modules/globby": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", + "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^1.0.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-cli2/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-cli2/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-cli2/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/markdownlint-micromark": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.8.tgz", + "integrity": "sha512-1ouYkMRo9/6gou9gObuMDnvZM8jC/ly3QCFQyoSPCS2XV1ZClU0xpKbL1Ar3bWWRT1RnBZkWUEiNKrI2CwiBQA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -14889,6 +15073,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -17220,6 +17413,12 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -17280,6 +17479,18 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", diff --git a/src/webapp/package.json b/src/webapp/package.json index 115b66852..1619a8c60 100644 --- a/src/webapp/package.json +++ b/src/webapp/package.json @@ -47,5 +47,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "markdownlint-cli2": "^0.12.1" } } diff --git a/src/webapp/public/locales/de/translation.json b/src/webapp/public/locales/de/translation.json index f3bd89782..d1a4391d6 100644 --- a/src/webapp/public/locales/de/translation.json +++ b/src/webapp/public/locales/de/translation.json @@ -138,8 +138,8 @@ "title": "Wähle ein Album, einen Ordner oder einen Song aus" }, "folders": { - "no-music": "Keine Musik vorhanden!", - "empty-folder": "Dieser Ordner ist leer!", + "no-music": "☝️ Keine Musik vorhanden!", + "empty-folder": "Dieser Ordner ist leer! 🙈", "show-folder-content": "Zeige den Ordnerinhalt an", "back-button-label": "Zurück" }, diff --git a/src/webapp/public/locales/en/translation.json b/src/webapp/public/locales/en/translation.json index 348d3771d..74fd9a696 100644 --- a/src/webapp/public/locales/en/translation.json +++ b/src/webapp/public/locales/en/translation.json @@ -138,8 +138,8 @@ "title": "Select an album, folder or song" }, "folders": { - "no-music": "No music found!", - "empty-folder": "This folder is empty!", + "no-music": "☝️ No music found!", + "empty-folder": "This folder is empty! 🙈", "show-folder-content": "Show folder content", "back-button-label": "Back" }, diff --git a/src/webapp/src/components/Cards/controls/actions/play-music/selected-folder.js b/src/webapp/src/components/Cards/controls/actions/play-music/selected-folder.js index bf1976fac..3191c36cf 100644 --- a/src/webapp/src/components/Cards/controls/actions/play-music/selected-folder.js +++ b/src/webapp/src/components/Cards/controls/actions/play-music/selected-folder.js @@ -8,7 +8,6 @@ import { import NoMusicSelected from './no-music-selected'; import FolderTypeAvatar from '../../../../Library/lists/folders/folder-type-avatar'; -import { DEFAULT_AUDIO_DIR } from '../../../../../config'; const SelectedFolder = ({ values: [folder] }) => { // TODO: Implement type correctly @@ -19,7 +18,7 @@ const SelectedFolder = ({ values: [folder] }) => { - + ); 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 75882dd0d..2c6d99180 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 @@ -29,7 +29,9 @@ const AlbumListItem = ({ albumartist, album, isButton = true }) => { album: album }); if (result) { - setCoverImage(`/cover-cache/${result}`); + if(result !== 'CACHE_PENDING') { + setCoverImage(`/cover-cache/${result}`); + } }; } diff --git a/src/webapp/src/components/Library/lists/folders/folder-list-item.js b/src/webapp/src/components/Library/lists/folders/folder-list-item.js index 755feef15..3be77fbbc 100644 --- a/src/webapp/src/components/Library/lists/folders/folder-list-item.js +++ b/src/webapp/src/components/Library/lists/folders/folder-list-item.js @@ -13,7 +13,6 @@ import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import request from '../../../../utils/request'; import FolderLink from './folder-link'; import FolderTypeAvatar from './folder-type-avatar'; -import { DEFAULT_AUDIO_DIR } from '../../../../config'; const FolderListItem = ({ folder, @@ -21,12 +20,12 @@ const FolderListItem = ({ registerMusicToCard, }) => { const { t } = useTranslation(); - const { type, name, path } = folder; + const { type, name, relpath } = folder; const playItem = () => { switch(type) { - case 'directory': return request('play_folder', { folder: path, recursive: true }); - case 'file': return request('play_single', { song_url: path.replace(`${DEFAULT_AUDIO_DIR}/`, '') }); + case 'directory': return request('play_folder', { folder: relpath, recursive: true }); + case 'file': return request('play_single', { song_url: relpath }); // TODO: Add missing Podcast // TODO: Add missing Stream default: return; @@ -35,8 +34,8 @@ const FolderListItem = ({ const registerItemToCard = () => { switch(type) { - case 'directory': return registerMusicToCard('play_folder', { folder: path, recursive: true }); - case 'file': return registerMusicToCard('play_single', { song_url: path.replace(`${DEFAULT_AUDIO_DIR}/`, '') }); + case 'directory': return registerMusicToCard('play_folder', { folder: relpath, recursive: true }); + case 'file': return registerMusicToCard('play_single', { song_url: relpath }); // TODO: Add missing Podcast // TODO: Add missing Stream default: return; @@ -50,7 +49,7 @@ const FolderListItem = ({ type === 'directory' ? diff --git a/src/webapp/src/components/Library/lists/folders/folder-list.js b/src/webapp/src/components/Library/lists/folders/folder-list.js index 36a9bcebd..3222e4234 100644 --- a/src/webapp/src/components/Library/lists/folders/folder-list.js +++ b/src/webapp/src/components/Library/lists/folders/folder-list.js @@ -1,12 +1,17 @@ import React, { memo } from 'react'; import { dropLast } from "ramda"; +import { useTranslation } from 'react-i18next'; -import { List } from '@mui/material'; +import { + List, + ListItem, + Typography, +} from '@mui/material'; import FolderListItem from './folder-list-item'; import FolderListItemBack from './folder-list-item-back'; -import { ROOT_DIRS } from '../../../../config'; +import { ROOT_DIR } from '../../../../config'; const FolderList = ({ dir, @@ -14,13 +19,14 @@ const FolderList = ({ isSelecting, registerMusicToCard, }) => { + const { t } = useTranslation(); + const getParentDir = (dir) => { - // TODO: ROOT_DIRS should be removed after paths are relative const decodedDir = decodeURIComponent(dir); - if (ROOT_DIRS.includes(decodedDir)) return undefined; + if (decodedDir === ROOT_DIR) return undefined; - const parentDir = dropLast(1, decodedDir.split('/')).join('/'); + const parentDir = dropLast(1, decodedDir.split('/')).join('/') || ROOT_DIR; return parentDir; } @@ -29,11 +35,14 @@ const FolderList = ({ return ( {parentDir && - + + } + {folders.length === 0 && + + {t('library.folders.empty-folder')} + } - {folders.map((folder, key) => + {folders.length > 0 && folders.map((folder, key) => { const { t } = useTranslation(); - const { dir = './' } = useParams(); + const { dir = ROOT_DIR } = useParams(); const [folders, setFolders] = useState([]); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -49,9 +51,8 @@ const Folders = ({ if (isLoading) return ; if (error) return {t('library.loading-error')}; - if (!filteredFolders.length) { - if (musicFilter) return {`☝️ ${t('library.folders.no-music')}`}; - return {`${t('library.folders.empty-folder')} 🙈`}; + if (musicFilter && !filteredFolders.length) { + return {t('library.folders.no-music')}; } return ( diff --git a/src/webapp/src/components/Library/lists/index.js b/src/webapp/src/components/Library/lists/index.js index 22970d9a9..e2b7a2d46 100644 --- a/src/webapp/src/components/Library/lists/index.js +++ b/src/webapp/src/components/Library/lists/index.js @@ -28,7 +28,7 @@ const LibraryLists = () => { const [cardId] = useState(searchParams.get('cardId')); const [musicFilter, setMusicFilter] = useState(''); - const handleMusicFolder = (event) => { + const handleMusicFilter = (event) => { setMusicFilter(event.target.value); }; @@ -49,7 +49,7 @@ const LibraryLists = () => { {isSelecting && }