makemkv-headless

Log | Files | Refs | README

README.md (5096B)


      1 # makemkv-headless
      2 
      3 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.
      4 
      5 ## Target
      6 
      7 | | |
      8 |---|---|
      9 | Host | `192.168.0.240` (`proxmox_host` in `inventory/hosts.yml`) — bare Proxmox VE host, not a container/VM guest |
     10 | User | `cjr` |
     11 | Optical drive | `/dev/sr0` |
     12 | Library | Local `/mnt/Movies` (this host is the storage server; a separate desktop role mounts it over NFS elsewhere) |
     13 
     14 ## Running the playbook
     15 
     16 ```bash
     17 # Full provision
     18 ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass
     19 
     20 # Individual roles
     21 ansible-playbook -i inventory/hosts.yml site.yml --tags flatpaks --ask-vault-pass
     22 ansible-playbook -i inventory/hosts.yml site.yml --tags gpu      --ask-vault-pass
     23 ansible-playbook -i inventory/hosts.yml site.yml --tags scripts  --ask-vault-pass
     24 
     25 # Dry run
     26 ansible-playbook -i inventory/hosts.yml site.yml --check --ask-vault-pass
     27 ```
     28 
     29 Lint with `ansible-lint site.yml`.
     30 
     31 ## Secrets
     32 
     33 `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`.
     34 
     35 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:
     36 
     37 ```bash
     38 cp group_vars/all/vault.yml.example group_vars/all/vault.yml
     39 ansible-vault edit group_vars/all/vault.yml   # fill in real values
     40 ```
     41 
     42 ## Roles
     43 
     44 | Role | Purpose |
     45 |------|---------|
     46 | `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) |
     47 | `gpu` | Adds `cjr` to `video`/`render` groups for VA-API hardware encoding access |
     48 | `scripts` | Creates a local `jellyfin` group (see below), deploys `rip`, `encode`, and `notify` to `~/.scripts/` and adds it to `PATH` |
     49 
     50 Key variables live in `group_vars/all/vars.yml`.
     51 
     52 ### Jellyfin group / GID note
     53 
     54 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`).
     55 
     56 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.
     57 
     58 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.
     59 
     60 ## The rip → encode pipeline
     61 
     62 `rip`, `encode`, and `notify` are deployed to `~/.scripts/` on the target and used interactively over SSH.
     63 
     64 ```
     65 rip [disc] [title] [crf] [--software] [--name "Movie Title"]
     66 ```
     67 
     68 1. Verifies `/dev/sr0` exists and a disc is actually readable in it.
     69 2. Queries the disc title via `makemkvcon info` and proposes an auto-detected movie name.
     70 3. Prompts for a name override — auto-accepts the detected name after 60 seconds of no input.
     71 4. Checks `/mnt/Movies/.rip.db` (SQLite) for a prior rip of the same disc and warns before re-ripping.
     72 5. Extracts the main feature to `/mnt/Movies` with `makemkvcon`, then calls `encode` on the result.
     73 6. Records the rip in the database, sends a Pushover notification, triggers a Jellyfin library scan, and ejects the disc.
     74 
     75 ```
     76 encode <input.mkv> [output.mkv] [crf] [--software]
     77 ```
     78 
     79 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.
     80 
     81 Query the rip history:
     82 
     83 ```bash
     84 sqlite3 /mnt/Movies/.rip.db "SELECT title, output_file, ripped_at FROM rips ORDER BY ripped_at DESC;"
     85 ```
     86 
     87 ## Naming conventions
     88 
     89 Output files must be Jellyfin/TMDB-scrapable: `Movie Title (Year)/Movie Title (Year).mkv`, spaces not dots.