The front-end build (npm ci, build, rsync, stamp) runs as handlers, and a failure in
any role after pi_musicmouse threw the queue away. On musicdolphin that left the
backend serving the API only, with no web/dist, across several runs. Flush the
handlers at the end of the role, so the front-end depends on this role succeeding and
not on every role after it.
Also install ffmpeg, which the backend needs to pull the audio out of podcast feeds
that publish video episodes ("Wissen macht Ah!"), and chromium.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
256 lines
9.9 KiB
YAML
256 lines
9.9 KiB
YAML
---
|
|
- name: Packages
|
|
ansible.builtin.apt:
|
|
name:
|
|
- git
|
|
- libvlc5 # runtime lib python-vlc binds against
|
|
- vlc-plugin-base
|
|
- ffmpeg # podcast feeds that publish video episodes get their audio extracted with it
|
|
- chromium
|
|
- alsa-utils
|
|
- samba
|
|
- python3
|
|
- python3-venv
|
|
- python3-pip
|
|
cache_valid_time: 7200
|
|
|
|
- name: Checkout Musicmouse repo
|
|
ansible.builtin.git:
|
|
repo: "{{ pi_musicmouse_repo }}"
|
|
dest: /opt/musicmouse
|
|
version: "{{ musicmouse_version }}"
|
|
accept_hostkey: true
|
|
force: true
|
|
register: pi_musicmouse_checkout
|
|
|
|
- name: Check the checkout has the layout this role knows
|
|
ansible.builtin.stat:
|
|
path: /opt/musicmouse/python-backend/pyproject.toml
|
|
register: pi_musicmouse_pyproject
|
|
# Meaningless under --check on a host that has never been deployed: the git task
|
|
# above reports "changed" without writing anything, so there is nothing to stat.
|
|
when: not ansible_check_mode
|
|
|
|
- name: Fail on a pre-rearchitecture checkout
|
|
ansible.builtin.fail:
|
|
msg: >-
|
|
{{ musicmouse_version }} has no python-backend/pyproject.toml, so it predates the
|
|
rearchitecture and this role cannot install it - the venv, the systemd unit and
|
|
the web/ build all live somewhere else on those commits. Point
|
|
musicmouse_version at a branch that has it (deploy/musicdolphin), or set
|
|
mediapi_install_kidsmusic to false for this host and leave whatever is installed
|
|
alone. Stopping here rather than failing three tasks deeper with the real reason
|
|
buried.
|
|
when: not ansible_check_mode and not pi_musicmouse_pyproject.stat.exists
|
|
|
|
- name: Find leftover uv installs
|
|
# This role used to install uv here to get a Python the distribution does not ship.
|
|
# It manages neither any more, and a stray uv on PATH is a confusing thing to find
|
|
# when debugging the venv, so take them back out. Nothing else put them there.
|
|
ansible.builtin.find:
|
|
paths: /opt
|
|
patterns: "uv-*"
|
|
file_type: directory
|
|
register: pi_musicmouse_stale_uv
|
|
|
|
- name: Remove leftover uv installs
|
|
ansible.builtin.file:
|
|
path: "{{ item }}"
|
|
state: absent
|
|
loop: >-
|
|
{{ (pi_musicmouse_stale_uv.files | map(attribute='path') | list)
|
|
+ ['/usr/local/bin/uv', '/usr/local/bin/uvx'] }}
|
|
|
|
- name: Get the system Python version
|
|
ansible.builtin.command:
|
|
cmd: python3 -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
|
register: pi_musicmouse_system_python
|
|
changed_when: false
|
|
|
|
- name: Get the version the venv was built with
|
|
ansible.builtin.command:
|
|
cmd: /opt/musicmouse/.venv/bin/python -c "import sys; print('%d.%d' % sys.version_info[:2])"
|
|
register: pi_musicmouse_venv_python
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Discard a venv built by a different Python
|
|
# `creates:` below would otherwise keep an old interpreter forever - which is exactly
|
|
# what a switch away from a separately-installed Python leaves behind. A venv holds
|
|
# absolute paths into the interpreter that made it, so it cannot be migrated; the
|
|
# cheap and correct move is to rebuild it.
|
|
ansible.builtin.file:
|
|
path: /opt/musicmouse/.venv
|
|
state: absent
|
|
when: >-
|
|
pi_musicmouse_venv_python.rc == 0
|
|
and pi_musicmouse_venv_python.stdout | trim != pi_musicmouse_system_python.stdout | trim
|
|
|
|
- name: Create the virtualenv from the system Python
|
|
ansible.builtin.command:
|
|
cmd: "python3 -m venv /opt/musicmouse/.venv"
|
|
creates: /opt/musicmouse/.venv/bin/python
|
|
|
|
# Installing the backend is deliberately not a handler hanging off the checkout.
|
|
# Tied to the checkout, one failed run strands the device: the notification is dropped
|
|
# when the play fails, the next run finds the checkout already current and notifies
|
|
# nothing, and the venv stays empty while the service crash-loops on
|
|
# ModuleNotFoundError. Instead, decide from what is actually on the device - the commit
|
|
# it has checked out, and whether the venv can import the package at all.
|
|
#
|
|
# pip's own output cannot answer this either: `musicmouse` is a local path dependency,
|
|
# so every run rebuilds and reinstalls it. Keying off that restarts the service on
|
|
# every run and rescans the whole library for nothing.
|
|
- name: Read which commit the venv was built from
|
|
ansible.builtin.command:
|
|
cmd: cat /opt/musicmouse/.venv/.installed-commit
|
|
register: pi_musicmouse_stamp
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Check the venv can actually import the backend
|
|
ansible.builtin.command:
|
|
cmd: /opt/musicmouse/.venv/bin/python -c "import musicmouse"
|
|
register: pi_musicmouse_importable
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Install the backend into the venv
|
|
ansible.builtin.command:
|
|
cmd: "/opt/musicmouse/.venv/bin/pip install /opt/musicmouse/python-backend"
|
|
when: >-
|
|
pi_musicmouse_importable.rc != 0
|
|
or pi_musicmouse_stamp.stdout | trim != pi_musicmouse_commit
|
|
changed_when: true # guarded by `when`, so reaching this means it installed something
|
|
notify: Restart musicmouse
|
|
|
|
- name: Record which commit the venv was built from
|
|
ansible.builtin.copy:
|
|
content: "{{ pi_musicmouse_commit }}\n"
|
|
dest: /opt/musicmouse/.venv/.installed-commit
|
|
mode: "0644"
|
|
|
|
- name: Create media directory
|
|
ansible.builtin.file:
|
|
path: /media/musicmouse
|
|
state: directory
|
|
mode: "0755"
|
|
|
|
- name: Install config file
|
|
ansible.builtin.copy:
|
|
src: "{{ pi_musicmouse_config_file }}"
|
|
dest: /media/musicmouse/config.yml
|
|
mode: "0644"
|
|
# Install-once: the app writes this file itself (parent mode's volume keys, the
|
|
# remote-control mapping). See pi_musicmouse_force_config in defaults.
|
|
force: "{{ pi_musicmouse_force_config }}"
|
|
notify: Restart musicmouse
|
|
|
|
- name: Install tippen curriculum
|
|
ansible.builtin.copy:
|
|
src: "{{ pi_musicmouse_curriculum_file }}"
|
|
dest: /media/musicmouse/tippen-curriculum.yml
|
|
mode: "0644"
|
|
when: pi_musicmouse_curriculum_file | length > 0
|
|
notify: Restart musicmouse
|
|
|
|
- name: Install systemd service file (from the checked-out repo, not a copy kept here)
|
|
ansible.builtin.copy:
|
|
src: /opt/musicmouse/python-backend/musicmouse.service
|
|
dest: /etc/systemd/system/musicmouse.service
|
|
mode: "0644"
|
|
remote_src: true
|
|
notify: Restart musicmouse
|
|
|
|
- name: Add script to autostart and start now
|
|
ansible.builtin.systemd:
|
|
name: musicmouse
|
|
state: started
|
|
enabled: "yes"
|
|
daemon_reload: "yes"
|
|
|
|
- name: Ensure local frontend build cache directory exists
|
|
ansible.builtin.file:
|
|
path: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
|
|
state: directory
|
|
mode: "0755"
|
|
delegate_to: localhost
|
|
become: false
|
|
vars:
|
|
# `become: false` above is not enough on its own. pi_standard_setup's defaults set
|
|
# `ansible_become: true`, role defaults are play-wide host variables, and the
|
|
# `ansible_become` *variable* outranks the `become` *keyword* in Ansible's
|
|
# precedence - so every task here tried to sudo on the control machine. Those
|
|
# defaults are needed to bootstrap a fresh Pi (connect as `pi`, become root, enable
|
|
# root login), so override the variable here rather than removing them.
|
|
ansible_become: false
|
|
|
|
- name: Checkout Musicmouse repo for frontend build (control machine)
|
|
ansible.builtin.git:
|
|
repo: "{{ pi_musicmouse_repo }}"
|
|
dest: "{{ pi_musicmouse_build_cache }}/{{ musicmouse_version | regex_replace('/', '_') }}"
|
|
version: "{{ musicmouse_version }}"
|
|
accept_hostkey: true
|
|
force: true
|
|
delegate_to: localhost
|
|
become: false
|
|
vars:
|
|
ansible_become: false # see the first delegated task in tasks/main.yml
|
|
register: pi_musicmouse_web_checkout
|
|
notify: Build frontend
|
|
|
|
- name: Read which commit the frontend on the device was built from
|
|
# Same guard as the venv's .installed-commit above, and for the same reason. Hanging
|
|
# the build chain off the control machine's checkout changing has a hole: a run that
|
|
# fails after the checkout leaves the device's dist as it was, and every later run
|
|
# finds the checkout current and rebuilds nothing, while ansible reports no changes.
|
|
# Stale is the dangerous case - a missing dist is at least obvious - and it is exactly
|
|
# what happens after a dist is replaced by hand, which is easy to do while chasing a
|
|
# performance problem on the device.
|
|
#
|
|
# The stamp lives beside dist/ rather than inside it: `Sync frontend` rsyncs with
|
|
# --delete, so anything in dist/ that the build did not produce is removed.
|
|
ansible.builtin.command:
|
|
cmd: cat /opt/musicmouse/web/.built-commit
|
|
register: pi_musicmouse_web_stamp
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Queue a frontend build when the device's is missing or from another commit
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
Frontend on the device is at
|
|
{{ pi_musicmouse_web_stamp.stdout | default('(none)', true) | trim }},
|
|
wanted {{ pi_musicmouse_web_checkout.after }} - queueing a build
|
|
when: pi_musicmouse_web_stamp.stdout | default('', true) | trim
|
|
!= pi_musicmouse_web_checkout.after
|
|
changed_when: true
|
|
notify: Build frontend
|
|
|
|
- name: Samba setup
|
|
ansible.builtin.copy:
|
|
src: smb.conf
|
|
dest: /etc/samba/
|
|
mode: "0644"
|
|
notify: Restart samba
|
|
|
|
- name: Enable samba
|
|
ansible.builtin.systemd:
|
|
name: smbd
|
|
state: started
|
|
enabled: true
|
|
|
|
- name: Build, sync and restart now rather than at the end of the play
|
|
# Handlers only run once the whole play is through, and a failure in any later role
|
|
# (squeezelite, shairport, ...) throws the queue away. The .built-commit guard above
|
|
# re-queues the build on the next run, but a play that keeps failing elsewhere then
|
|
# never delivers a front-end at all: the backend serves the API only and the kiosk
|
|
# shows nothing. Flushing here ties the front-end to this role succeeding, not to
|
|
# every role after it.
|
|
ansible.builtin.meta: flush_handlers
|
|
# manual steps:
|
|
# - set samba passwords with smbpasswd
|
|
# - copy music into /media/musicmouse: scripts/sync-library.sh in the app repo, which
|
|
# also carries the cover and analysis cache over (or use the samba share, and let
|
|
# the device spend the DSP time itself)
|