makemkv-headless

Log | Files | Refs | README

commit c6e098bad6dfea4bc1addff41fed7692f0794dda
Author: Chris Roberts <chris.roberts@learningunix.net>
Date:   Tue,  7 Jul 2026 04:30:39 -0500

created initial playbooks

Diffstat:
A.gitignore | 15+++++++++++++++
ACLAUDE.md | 19+++++++++++++++++++
AREADME.md | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Agroup_vars/all/vars.yml | 5+++++
Agroup_vars/all/vault.yml.example | 10++++++++++
Ainventory/hosts.yml | 6++++++
Aroles/flatpaks/tasks/main.yml | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aroles/gpu/tasks/main.yml | 7+++++++
Aroles/scripts/files/encode.sh | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aroles/scripts/tasks/main.yml | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aroles/scripts/templates/notify.sh.j2 | 21+++++++++++++++++++++
Aroles/scripts/templates/rip.sh.j2 | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asite.yml | 14++++++++++++++
13 files changed, 584 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,15 @@ +# Ansible +*.retry +.ansible/ +.vault_pass +.vault_pass.txt +group_vars/all/vault.yml + +# Python +__pycache__/ +*.pyc + +# Editor/OS +*.swp +*.swo +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md @@ -0,0 +1,19 @@ +# Project: Create ansible role +This project is to create ansible roles that provision a debian based system to rip and encode dvd and Blueray movies. + +## Architecture +Use a site.yml and a roles based directory structure. Use pushover to send notifications. + +## Important notes +Use flatpaks for makemkv and ffmpeg if available +Explain everything that claude code does +When more that one option exists explain them all +Don't guess. If claude code doesn't know something inform the user and let them make the decision. +The target machine is 192.168.0.240. It is a proxmox server. The target directory is /mnt/Movies. +The provided scripts use gpu encoding. Esnure the user in the proper groups for doing so. +This project will become a git repo + + +## Assets +There is a scripts directory in the project folder which contains the needed rip, encode, and notification scripts. +There is an ansible vault file in the project directory. It contains the pushover user and app keys we will use. diff --git a/README.md b/README.md @@ -0,0 +1,89 @@ +# makemkv-headless + +Ansible playbook that provisions a bare Proxmox VE host as a headless Blu-ray/DVD archiving box: MakeMKV (via Flatpak) and ffmpeg, GPU group access for hardware encoding, and a `rip`/`encode`/`notify` script set for turning a disc into a Jellyfin-ready x265 file — no desktop or VNC session required, since ripping happens directly on the host that already owns the movie library. + +## Target + +| | | +|---|---| +| Host | `192.168.0.240` (`proxmox_host` in `inventory/hosts.yml`) — bare Proxmox VE host, not a container/VM guest | +| User | `cjr` | +| Optical drive | `/dev/sr0` | +| Library | Local `/mnt/Movies` (this host is the storage server; a separate desktop role mounts it over NFS elsewhere) | + +## Running the playbook + +```bash +# Full provision +ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass + +# Individual roles +ansible-playbook -i inventory/hosts.yml site.yml --tags flatpaks --ask-vault-pass +ansible-playbook -i inventory/hosts.yml site.yml --tags gpu --ask-vault-pass +ansible-playbook -i inventory/hosts.yml site.yml --tags scripts --ask-vault-pass + +# Dry run +ansible-playbook -i inventory/hosts.yml site.yml --check --ask-vault-pass +``` + +Lint with `ansible-lint site.yml`. + +## Secrets + +`group_vars/all/vault.yml` is Ansible Vault-encrypted and must hold `pushover_app_token`, `pushover_user_key`, and `jellyfin_api_key`. Always pass `--ask-vault-pass`. + +This repo is public, so `vault.yml` is gitignored and never committed — not even encrypted. `group_vars/all/vault.yml.example` documents the required keys with placeholder values. On a new checkout: + +```bash +cp group_vars/all/vault.yml.example group_vars/all/vault.yml +ansible-vault edit group_vars/all/vault.yml # fill in real values +``` + +## Roles + +| Role | Purpose | +|------|---------| +| `flatpaks` | Flathub remote, MakeMKV flatpak (granted access to `home` + `/mnt/Movies`), DVD libs (`libdvdcss` via `libdvd-pkg`), `ffmpeg` (apt — no standalone ffmpeg CLI Flatpak exists on Flathub) | +| `gpu` | Adds `cjr` to `video`/`render` groups for VA-API hardware encoding access | +| `scripts` | Creates a local `jellyfin` group (see below), deploys `rip`, `encode`, and `notify` to `~/.scripts/` and adds it to `PATH` | + +Key variables live in `group_vars/all/vars.yml`. + +### Jellyfin group / GID note + +The `rip` script `chgrp`s each new movie folder so Jellyfin (a separate host, `192.168.0.241`) can read it. On `192.168.0.240`, GID `110` is already taken by the system `postfix` group, so this project uses a dedicated local group `jellyfin` with **GID 1100** instead (`jellyfin_gid` in `group_vars/all/vars.yml`). + +This GID does **not** numerically match Jellyfin's real group on `192.168.0.241` (which is GID 110, named `jellyfin` there). Verified this doesn't matter in practice: the NFSv4 mount between these hosts has `nfs4_disable_idmapping=Y` on both ends (confirmed via `/sys/module/nfs{,d}/parameters/nfs4_disable_idmapping`), so permission checks use raw numeric IDs, not names — but the existing movie library is already world-readable (`644`/`777` files, and `rip`'s `chmod 2775` on new folders includes the "other" `r-x` bit), so Jellyfin can already read everything regardless of group membership. The `chgrp` step is effectively cosmetic today, not load-bearing. + +If file/directory permissions are ever tightened to remove world-read access, this mismatch would start to matter — at that point, either add a matching GID 1100 group to `192.168.0.241` and put the `jellyfin` service user in it, or enable proper NFSv4 idmapping (`nfs4_disable_idmapping=N` + running, matching-domain `nfs-idmapd` on both hosts) so group *names* rather than numbers are what's compared. + +## The rip → encode pipeline + +`rip`, `encode`, and `notify` are deployed to `~/.scripts/` on the target and used interactively over SSH. + +``` +rip [disc] [title] [crf] [--software] [--name "Movie Title"] +``` + +1. Verifies `/dev/sr0` exists and a disc is actually readable in it. +2. Queries the disc title via `makemkvcon info` and proposes an auto-detected movie name. +3. Prompts for a name override — auto-accepts the detected name after 60 seconds of no input. +4. Checks `/mnt/Movies/.rip.db` (SQLite) for a prior rip of the same disc and warns before re-ripping. +5. Extracts the main feature to `/mnt/Movies` with `makemkvcon`, then calls `encode` on the result. +6. Records the rip in the database, sends a Pushover notification, triggers a Jellyfin library scan, and ejects the disc. + +``` +encode <input.mkv> [output.mkv] [crf] [--software] +``` + +Transcodes to x265 — `hevc_vaapi` hardware encoding on `/dev/dri/renderD128` by default, or `libx265` software with `--software` (also auto-selected for SD/DVD sources, since VA-API can't handle their BT.601 color space) — and strips non-English audio/subtitle tracks. + +Query the rip history: + +```bash +sqlite3 /mnt/Movies/.rip.db "SELECT title, output_file, ripped_at FROM rips ORDER BY ripped_at DESC;" +``` + +## Naming conventions + +Output files must be Jellyfin/TMDB-scrapable: `Movie Title (Year)/Movie Title (Year).mkv`, spaces not dots. diff --git a/group_vars/all/vars.yml b/group_vars/all/vars.yml @@ -0,0 +1,5 @@ +--- +rip_user: cjr + +jellyfin_url: http://192.168.0.241:8096 +jellyfin_gid: 1100 diff --git a/group_vars/all/vault.yml.example b/group_vars/all/vault.yml.example @@ -0,0 +1,10 @@ +--- +# Copy this file to vault.yml, fill in real values, then encrypt it: +# cp group_vars/all/vault.yml.example group_vars/all/vault.yml +# ansible-vault encrypt group_vars/all/vault.yml +# +# vault.yml is gitignored — it never gets committed, even encrypted. + +pushover_app_token: CHANGEME +pushover_user_key: CHANGEME +jellyfin_api_key: CHANGEME diff --git a/inventory/hosts.yml b/inventory/hosts.yml @@ -0,0 +1,6 @@ +--- +all: + hosts: + proxmox_host: + ansible_host: 192.168.0.240 + ansible_user: cjr diff --git a/roles/flatpaks/tasks/main.yml b/roles/flatpaks/tasks/main.yml @@ -0,0 +1,58 @@ +--- +- name: Install flatpak + ansible.builtin.apt: + name: flatpak + state: present + update_cache: true + tags: [flatpaks, install] + +- name: Add Flathub remote (system-wide) + community.general.flatpak_remote: + name: flathub + state: present + flatpakrepo_url: https://flathub.org/repo/flathub.flatpakrepo + method: system + tags: [flatpaks, configure] + +- name: Grant MakeMKV access to home and movie library + ansible.builtin.command: + cmd: flatpak override --user --filesystem=home --filesystem=/mnt/Movies com.makemkv.MakeMKV + become: true + become_user: "{{ rip_user }}" + changed_when: true + tags: [flatpaks, makemkv] + +- name: Install MakeMKV from Flathub + community.general.flatpak: + name: com.makemkv.MakeMKV + state: present + remote: flathub + method: system + tags: [flatpaks, makemkv] + +- name: Enable contrib repository + ansible.builtin.apt_repository: + repo: "deb http://deb.debian.org/debian {{ ansible_facts['distribution_release'] }} main contrib non-free" + state: present + update_cache: true + tags: [flatpaks, dvd] + +- name: Install DVD support packages + ansible.builtin.apt: + name: + - libdvd-pkg + - libdvdread8 + - libdvdnav4 + state: present + tags: [flatpaks, dvd] + +- name: Build and install libdvdcss + ansible.builtin.command: dpkg-reconfigure -f noninteractive libdvd-pkg + changed_when: true + tags: [flatpaks, dvd] + +- name: Install ffmpeg + ansible.builtin.apt: + name: ffmpeg + state: present + tags: [flatpaks, ffmpeg] diff --git a/roles/gpu/tasks/main.yml b/roles/gpu/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Add user to video and render groups for GPU access + ansible.builtin.user: + name: "{{ rip_user }}" + groups: video,render + append: true + tags: [gpu] diff --git a/roles/scripts/files/encode.sh b/roles/scripts/files/encode.sh @@ -0,0 +1,99 @@ +#!/bin/bash +set -euo pipefail + +SOFTWARE=0 + +usage() { + echo "Usage: encode <input.mkv> [output.mkv] [crf] [--software]" + echo "" + echo " input.mkv source file (required)" + echo " output.mkv destination file (default: input_x265.mkv)" + echo " crf quality level 18-28 (default: 22, lower = better quality)" + echo " --software use software x265 encoder (slower, higher quality)" + echo "" + echo "Examples:" + echo " encode movie.mkv" + echo " encode movie.mkv movie_encoded.mkv" + echo " encode movie.mkv movie_encoded.mkv 18" + echo " encode movie.mkv movie_encoded.mkv 18 --software" + exit 1 +} + +[[ $# -lt 1 ]] && usage + +INPUT="$1" +OUTPUT="${2:-${INPUT%.*}_x265.mkv}" +CRF="${3:-22}" + +for arg in "$@"; do + [[ "$arg" == "--software" ]] && SOFTWARE=1 +done + +if [[ ! -f "$INPUT" ]]; then + echo "Error: input file not found: $INPUT" + exit 1 +fi + +# Build stream maps — probe for English audio and subtitle tracks +MAPS="-map 0:v" + +if ffprobe -v quiet -select_streams "a:m:language:eng" -show_streams "$INPUT" 2>/dev/null | grep -q "^index="; then + MAPS="$MAPS -map 0:a:m:language:eng" +else + MAPS="$MAPS -map 0:a" +fi + +if ffprobe -v quiet -select_streams "s:m:language:eng" -show_streams "$INPUT" 2>/dev/null | grep -q "^index="; then + MAPS="$MAPS -map 0:s:m:language:eng" +fi + +# Auto-detect SD content — hevc_vaapi can't handle BT.601/TV-range color space +# (standard DVD format). SD is small enough that software is faster and better quality. +if [[ $SOFTWARE -eq 0 ]]; then + HEIGHT=$(ffprobe -v quiet -select_streams v:0 \ + -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$INPUT" 2>/dev/null | head -1) + if [[ -n "$HEIGHT" && "$HEIGHT" -le 576 ]]; then + echo "SD content detected (${HEIGHT}p) — switching to software encoder" + SOFTWARE=1 + fi +fi + +echo "Input: $INPUT" +echo "Output: $OUTPUT" +echo "CRF/QP: $CRF" + +if [[ $SOFTWARE -eq 1 ]]; then + echo "Encoder: libx265 (software)" + echo "" + ffmpeg -probesize 100M -analyzeduration 100M \ + -i "$INPUT" \ + $MAPS \ + -c:v libx265 \ + -crf "$CRF" \ + -preset medium \ + -c:a copy \ + -c:s copy \ + "$OUTPUT" +else + echo "Encoder: hevc_vaapi (hardware)" + echo "" + ffmpeg -probesize 100M -analyzeduration 100M \ + -vaapi_device /dev/dri/renderD128 \ + -i "$INPUT" \ + $MAPS \ + -vf 'format=nv12,hwupload,scale_vaapi=format=nv12' \ + -c:v hevc_vaapi \ + -qp "$CRF" \ + -c:a copy \ + -c:s copy \ + "$OUTPUT" +fi + +INSIZE=$(du -sh "$INPUT" | cut -f1) +OUTSIZE=$(du -sh "$OUTPUT" | cut -f1) +echo "" +echo "Done. $INPUT ($INSIZE) -> $OUTPUT ($OUTSIZE)" + +if command -v notify &>/dev/null && [[ "${MAKEMKV_RIP:-0}" != "1" ]]; then + notify "Encode complete" "$(basename "$OUTPUT") — ${INSIZE} → ${OUTSIZE}" +fi diff --git a/roles/scripts/tasks/main.yml b/roles/scripts/tasks/main.yml @@ -0,0 +1,70 @@ +--- +- name: Install script dependencies + ansible.builtin.apt: + name: + - sqlite3 + - curl + state: present + update_cache: true + tags: [scripts] + +- name: Create jellyfin group for movie library permissions + ansible.builtin.group: + name: jellyfin + gid: "{{ jellyfin_gid }}" + state: present + tags: [scripts] + +- name: Add user to jellyfin group + ansible.builtin.user: + name: "{{ rip_user }}" + groups: jellyfin + append: true + tags: [scripts] + +- name: Create ~/.scripts directory + ansible.builtin.file: + path: /home/{{ rip_user }}/.scripts + state: directory + owner: "{{ rip_user }}" + group: "{{ rip_user }}" + mode: '0755' + tags: [scripts] + +- name: Copy encode script + ansible.builtin.copy: + src: encode.sh + dest: /home/{{ rip_user }}/.scripts/encode + owner: "{{ rip_user }}" + group: "{{ rip_user }}" + mode: '0755' + tags: [scripts] + +- name: Deploy rip script + ansible.builtin.template: + src: rip.sh.j2 + dest: /home/{{ rip_user }}/.scripts/rip + owner: "{{ rip_user }}" + group: "{{ rip_user }}" + mode: '0755' + tags: [scripts] + +- name: Deploy notify script + ansible.builtin.template: + src: notify.sh.j2 + dest: /home/{{ rip_user }}/.scripts/notify + owner: "{{ rip_user }}" + group: "{{ rip_user }}" + mode: '0755' + tags: [scripts] + +- name: Add ~/.scripts to PATH in .bashrc + ansible.builtin.lineinfile: + path: /home/{{ rip_user }}/.bashrc + line: 'export PATH="$HOME/.scripts:$PATH"' + state: present + create: true + owner: "{{ rip_user }}" + group: "{{ rip_user }}" + mode: '0644' + tags: [scripts] diff --git a/roles/scripts/templates/notify.sh.j2 b/roles/scripts/templates/notify.sh.j2 @@ -0,0 +1,21 @@ +#!/bin/bash +# Usage: notify "title" "message" +# notify "message" (uses default title "MakeMKV") + +PUSHOVER_TOKEN="{{ pushover_app_token }}" +PUSHOVER_USER="{{ pushover_user_key }}" +PUSHOVER_URL="https://api.pushover.net/1/messages.json" + +case $# in + 1) TITLE="MakeMKV" ; MESSAGE="$1" ;; + 2) TITLE="$1" ; MESSAGE="$2" ;; + *) echo "Usage: notify [title] message" >&2; exit 1 ;; +esac + +curl -s --max-time 10 \ + -F "token=$PUSHOVER_TOKEN" \ + -F "user=$PUSHOVER_USER" \ + -F "title=$TITLE" \ + -F "message=$MESSAGE" \ + "$PUSHOVER_URL" > /dev/null \ + || echo "Warning: Pushover notification failed" >&2 diff --git a/roles/scripts/templates/rip.sh.j2 b/roles/scripts/templates/rip.sh.j2 @@ -0,0 +1,171 @@ +#!/bin/bash +set -euo pipefail + +OUTPUT_DIR="/mnt/Movies" +DB="/mnt/Movies/.rip.db" +SOFTWARE="" +NAME_OVERRIDE="" + +usage() { + echo "Usage: rip [disc] [title] [crf] [--software] [--name \"Movie Title\"]" + echo "" + echo " disc disc number (default: 0)" + echo " title title number (default: 0, usually the main feature)" + echo " crf encode quality 18-28 (default: 22, lower = better quality)" + echo " --software use software x265 encoder instead of hardware" + echo " --name output filename base (default: auto-detected from disc)" + echo "" + echo "Examples:" + echo " rip" + echo " rip 0 1" + echo " rip 0 0 18" + echo " rip 0 0 18 --software" + echo " rip --name \"Raiders of the Lost Ark (1981)\"" + echo " rip 0 0 22 --name \"Raiders of the Lost Ark (1981)\"" + exit 1 +} + +[[ "${1:-}" == "-h" ]] && usage + +positional=() +while [[ $# -gt 0 ]]; do + case "$1" in + --software) SOFTWARE="--software"; shift ;; + --name) NAME_OVERRIDE="$2"; shift 2 ;; + *) positional+=("$1"); shift ;; + esac +done + +DISC="${positional[0]:-0}" +TITLE_NUM="${positional[1]:-0}" +CRF="${positional[2]:-22}" + +DRIVE="/dev/sr0" + +if [[ ! -b "$DRIVE" ]]; then + echo "Error: no optical drive found at $DRIVE" + exit 1 +fi + +if ! dd if="$DRIVE" of=/dev/null bs=2048 count=1 status=none 2>/dev/null; then + echo "Error: no disc detected in $DRIVE" + exit 1 +fi + +sql_str() { printf '%s' "${1//\'/\'\'}"; } + +init_db() { + sqlite3 "$DB" <<'SQL' +CREATE TABLE IF NOT EXISTS rips ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + disc_id TEXT NOT NULL, + title TEXT, + output_file TEXT, + output_bytes INTEGER, + ripped_at TEXT NOT NULL DEFAULT (datetime('now')) +); +SQL +} + +# Always query disc info — needed for duplicate detection regardless of --name +echo "Querying disc info..." +RAW=$(flatpak run --command=makemkvcon com.makemkv.MakeMKV \ + -r info "disc:$DISC" 2>/dev/null \ + | awk -F'"' '/^CINFO:2,/ { print $2; exit }' || echo "") + +# Determine output movie name +if [[ -n "$NAME_OVERRIDE" ]]; then + MOVIE_NAME="${NAME_OVERRIDE//\//-}" +else + if [[ -n "$RAW" ]]; then + MOVIE_NAME=$(python3 -c " +import sys, re +JUNK = {'ws', 'fs', 'bd', 'uhd', 'hd', '3d', 'disc1', 'disc2', 'disc3', 'disc4'} +parts = sys.argv[1].replace('_', ' ').split() +clean = [p for p in parts if p and p.lower() not in JUNK and not re.match(r'^\d+x\d+$', p.lower())] +print(' '.join(p.capitalize() for p in clean)) +" "$RAW") + else + MOVIE_NAME="" + fi + echo "" + if ! read -r -t 60 -p "Movie name [${MOVIE_NAME:-enter name}] (60s timeout): " input; then + echo "" + echo "No input received in 60s, using default: ${MOVIE_NAME:-<none>}" + fi + [[ -n "${input:-}" ]] && MOVIE_NAME="$input" + if [[ -z "$MOVIE_NAME" ]]; then + echo "Error: movie name is required" + exit 1 + fi +fi + +# Duplicate check +if [[ -n "$RAW" ]] && command -v sqlite3 &>/dev/null; then + init_db + EXISTING=$(sqlite3 "$DB" \ + "SELECT title, output_file, ripped_at FROM rips WHERE disc_id='$(sql_str "$RAW")' LIMIT 1;") + if [[ -n "$EXISTING" ]]; then + echo "" + echo "Warning: this disc has already been ripped:" + echo " $EXISTING" + echo "" + read -r -p "Rip again anyway? [y/N] " confirm + [[ "${confirm,,}" != "y" ]] && { echo "Aborted."; exit 0; } + fi +fi + +echo "Ripping title $TITLE_NUM from disc $DISC to $OUTPUT_DIR..." +MARKER=$(mktemp) +flatpak run --command=makemkvcon com.makemkv.MakeMKV \ + mkv "disc:$DISC" "$TITLE_NUM" "$OUTPUT_DIR" + +RIPPED=$(find "$OUTPUT_DIR" -maxdepth 2 -name "*.mkv" -not -name "*_x265.mkv" \ + -newer "$MARKER" -type f | head -1) +rm -f "$MARKER" + +if [[ -z "$RIPPED" ]]; then + echo "Error: could not find ripped MKV in $OUTPUT_DIR" + exit 1 +fi +echo "Ripped: $RIPPED" + +if [[ -n "$MOVIE_NAME" ]]; then + mkdir -p "$OUTPUT_DIR/$MOVIE_NAME" + chgrp {{ jellyfin_gid }} "$OUTPUT_DIR/$MOVIE_NAME" 2>/dev/null || true + chmod 2775 "$OUTPUT_DIR/$MOVIE_NAME" + DEST="$OUTPUT_DIR/$MOVIE_NAME/$MOVIE_NAME.mkv" + mv "$RIPPED" "$DEST" + RIPPED="$DEST" + echo "Moved to: $RIPPED" +fi + +ENCODED="${RIPPED%.*}_x265.mkv" +echo "Encoding with English audio and subtitles only..." +MAKEMKV_RIP=1 encode "$RIPPED" "$ENCODED" "$CRF" $SOFTWARE + +INSIZE=$(du -sh "$RIPPED" | cut -f1) +OUTSIZE=$(du -sh "$ENCODED" | cut -f1) +rm "$RIPPED" + +# Record in DB +if [[ -n "$RAW" ]] && command -v sqlite3 &>/dev/null; then + OUTPUT_BYTES=$(stat -c%s "$ENCODED") + sqlite3 "$DB" "INSERT INTO rips (disc_id, title, output_file, output_bytes) \ + VALUES ('$(sql_str "$RAW")', '$(sql_str "$MOVIE_NAME")', '$(sql_str "$ENCODED")', $OUTPUT_BYTES);" +fi + +echo "" +echo "Done. Kept $ENCODED ($OUTSIZE), removed source ($INSIZE)" + +command -v notify &>/dev/null && notify "Rip complete" "$MOVIE_NAME — ${INSIZE} → ${OUTSIZE}" + +echo "Triggering Jellyfin library scan..." +curl -s -X POST "{{ jellyfin_url }}/ScheduledTasks/Running/7738148ffcd07979c7ceb148e06b3aed" \ + -H "X-Emby-Token: {{ jellyfin_api_key }}" \ + -o /dev/null \ + && echo "Scan triggered." \ + || echo "Warning: could not reach Jellyfin at {{ jellyfin_url }}" + +echo "Ejecting disc..." +eject "$DRIVE" 2>/dev/null || echo "Warning: could not eject $DRIVE" diff --git a/site.yml b/site.yml @@ -0,0 +1,14 @@ +--- +- name: Configure Proxmox headless ripping host + hosts: proxmox_host + become: true + + roles: + - role: flatpaks + tags: [flatpaks] + + - role: gpu + tags: [gpu] + + - role: scripts + tags: [scripts]