From 63689c3fc008ff755d87547e9496131a3fc99f63 Mon Sep 17 00:00:00 2001 From: thesabear Date: Tue, 25 Aug 2026 11:29:04 +0300 Subject: [PATCH] add projects/ - standalone scripts alongside the Docker stacks Two real, working scripts: an Ansible playbook run-menu (the same pattern used identically across both the fleet-config and infra- provisioning repos), and cuesplit, a frame-accurate FLAC/.cue splitting utility. Sanitized paths only, logic untouched. README gets a new "Scripts & Tooling" section pointing at it, placed right after "What's Running" alongside the other repo-structure pointer. --- README.md | 9 +++ projects/ansible-run-menu/run | 56 +++++++++++++ projects/ansible-run-menu/run-menu | 44 ++++++++++ projects/cuesplit/cuesplit.py | 124 +++++++++++++++++++++++++++++ projects/cuesplit/run | 47 +++++++++++ projects/iac-run-menu/run | 35 ++++++++ projects/iac-run-menu/run-menu | 33 ++++++++ 7 files changed, 348 insertions(+) create mode 100755 projects/ansible-run-menu/run create mode 100755 projects/ansible-run-menu/run-menu create mode 100755 projects/cuesplit/cuesplit.py create mode 100755 projects/cuesplit/run create mode 100755 projects/iac-run-menu/run create mode 100755 projects/iac-run-menu/run-menu diff --git a/README.md b/README.md index abc0c26..a97eaf8 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,15 @@ Authentik, self-hosted, fronts single sign-on fleet-wide — native OIDC where a --- +## Scripts & Tooling + +A few standalone scripts live in [`projects/`](projects/) alongside the Docker stacks — real tooling, not demo code: + +- **Ansible run-menu** — an interactive playbook runner, the same pattern used identically across both the fleet-config and infra-provisioning repos, so running anything doesn't mean remembering exact flags. +- **cuesplit** — a small FLAC-splitting utility, frame-accurate against a .cue sheet. + +--- + ## 👀 What I Have Explored I use my homelab as a real-world playground to deepen my understanding of core IT concepts. Some of the areas I've learned and put into practice are: diff --git a/projects/ansible-run-menu/run b/projects/ansible-run-menu/run new file mode 100755 index 0000000..8bd24fe --- /dev/null +++ b/projects/ansible-run-menu/run @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -e + +PLAYBOOK="$1" +shift || true # capture any extra ansible-playbook flags (--check, --limit, -vv, etc.) + +if [ -z "$PLAYBOOK" ]; then + echo "Usage: run [extra ansible-playbook args]" + echo "" + echo "Available playbooks:" + echo " os_update_vms" + echo " os_update_pve" + echo " os_update_pve_verify" + echo " os_update_pbs" + echo " container_update" + echo " app_update" + echo " disk_cleanup" + echo " automation_self_update" + echo " dashy_fleet_audit" + echo " dashy_selfhosting_coverage" + echo " emby_tag_sync" + echo " jellyfin_tag_sync" + echo "" + echo "Examples:" + echo " run os_update_vms" + echo " run os_update_vms --check --limit caddy -vv" + exit 1 +fi + +ANSIBLE_DIR="/path/to/ansible" +VENV="$HOME/ansible-modern" +SSH_KEY="$HOME/.ssh/auto" +VAULT_FILE="$ANSIBLE_DIR/.vault_pass.txt" +INVENTORY="$ANSIBLE_DIR/inventory.yml" + +case "$PLAYBOOK" in + os_update_vms|os_update_pve|os_update_pve_verify|os_update_pbs|container_update|app_update|disk_cleanup|automation_self_update|dashy_fleet_audit|dashy_selfhosting_coverage|emby_tag_sync|jellyfin_tag_sync) + ;; + *) + echo "Unknown playbook: $PLAYBOOK" + exit 1 + ;; +esac + +cd "$ANSIBLE_DIR" + +VAULT_ARGS=() +if [ -f "$VAULT_FILE" ]; then + VAULT_ARGS=(--vault-password-file "$VAULT_FILE") +fi + +exec "$VENV/bin/ansible-playbook" \ + -i "$INVENTORY" \ + "playbooks/${PLAYBOOK}.yml" \ + "${VAULT_ARGS[@]}" \ + "$@" diff --git a/projects/ansible-run-menu/run-menu b/projects/ansible-run-menu/run-menu new file mode 100755 index 0000000..65b389b --- /dev/null +++ b/projects/ansible-run-menu/run-menu @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -e + +# Path to your existing wrapper +WRAPPER="/path/to/ansible/run" + +# List of playbooks +PLAYBOOKS=("os_update_vms" "os_update_pve" "os_update_pve_verify" "os_update_pbs" "container_update" "app_update" "disk_cleanup" "automation_self_update" "dashy_fleet_audit" "dashy_selfhosting_coverage" "emby_tag_sync" "jellyfin_tag_sync" "Quit") + +# Interactive menu +PS3="Select playbook to run (number): " + +# Colored header (yellow) +YELLOW="\033[1;33m" +RESET="\033[0m" +echo -e "${YELLOW}=== Ansible Playbook Runner ===${RESET}" + +select opt in "${PLAYBOOKS[@]}"; do + case $opt in + "os_update_vms"|"os_update_pve"|"os_update_pve_verify"|"os_update_pbs"|"container_update"|"app_update"|"disk_cleanup"|"automation_self_update"|"dashy_fleet_audit"|"dashy_selfhosting_coverage"|"emby_tag_sync"|"jellyfin_tag_sync") + read -p "Dry run (--check)? [y/N]: " dry_run + read -p "Limit to a single host? (leave blank for all): " limit_host + + ARGS=() + if [[ "$dry_run" =~ ^[Yy]$ ]]; then + ARGS+=(--check) + fi + if [ -n "$limit_host" ]; then + ARGS+=(--limit "$limit_host") + fi + + echo "Running playbook: $opt ${ARGS[*]}" + "$WRAPPER" "$opt" "${ARGS[@]}" + break + ;; + "Quit") + echo "Exiting." + exit 0 + ;; + *) + echo "Invalid selection. Please choose a number from the list." + ;; + esac +done diff --git a/projects/cuesplit/cuesplit.py b/projects/cuesplit/cuesplit.py new file mode 100755 index 0000000..c55a431 --- /dev/null +++ b/projects/cuesplit/cuesplit.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +cuesplit.py — split FLAC files using a .cue sheet, via ffmpeg. +Handles multi-FILE cues, frame-accurate (MM:SS:FF) timestamps, and uses +INDEX 01 (track start, skipping pregap) as the split point. + +Usage: + python3 cuesplit.py "album.cue" [output_dir] + +Output dir defaults to "split". +""" +import re +import subprocess +import sys +from pathlib import Path + + +def parse_cue(cue_path): + """Returns list of (filename, [(track_num, index01_time_str), ...])""" + files = [] + current_file = None + current_tracks = None + pending_track = None + + with open(cue_path, encoding="utf-8", errors="replace") as f: + for line in f: + line = line.strip() + + m = re.match(r'FILE\s+"([^"]+)"', line, re.IGNORECASE) + if m: + if current_file is not None: + files.append((current_file, current_tracks)) + current_file = m.group(1) + current_tracks = [] + continue + + m = re.match(r'TRACK\s+(\d+)\s+AUDIO', line, re.IGNORECASE) + if m: + pending_track = int(m.group(1)) + continue + + m = re.match(r'INDEX\s+01\s+(\d+:\d+:\d+)', line, re.IGNORECASE) + if m and pending_track is not None: + current_tracks.append((pending_track, m.group(1))) + pending_track = None + continue + + if current_file is not None: + files.append((current_file, current_tracks)) + + return files + + +def cue_time_to_seconds(t): + mm, ss, ff = map(int, t.split(":")) + return mm * 60 + ss + ff / 75.0 + + +def seconds_to_ffmpeg_ts(total_seconds): + mm = int(total_seconds // 60) + ss = total_seconds - mm * 60 + return f"{mm:02d}:{ss:06.3f}" + + +def main(): + if len(sys.argv) < 2: + print("Usage: python3 cuesplit.py [output_dir]") + sys.exit(1) + + cue_path = Path(sys.argv[1]) + out_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("split") + out_dir.mkdir(parents=True, exist_ok=True) + + if not cue_path.exists(): + print(f"Cue file not found: {cue_path}") + sys.exit(1) + + files = parse_cue(cue_path) + if not files: + print("No FILE/TRACK entries found in cue — check the cue format.") + sys.exit(1) + + track_counter = 1 + + for flac_name, tracks in files: + flac_path = cue_path.parent / flac_name + if not flac_path.exists(): + print(f"WARNING: referenced audio file not found: {flac_path}") + continue + + if not tracks: + print(f"WARNING: no tracks found for {flac_name}") + continue + + starts = [cue_time_to_seconds(t) for _, t in tracks] + + for i, start in enumerate(starts): + out_file = out_dir / f"{track_counter:02d}.flac" + ss = seconds_to_ffmpeg_ts(start) + + cmd = ["ffmpeg", "-y", "-i", str(flac_path), "-ss", ss] + + if i + 1 < len(starts): + to = seconds_to_ffmpeg_ts(starts[i + 1]) + cmd += ["-to", to] + + cmd += [str(out_file)] + + print(f"Track {track_counter}: {flac_name} [{ss}" + f"{' -> ' + to if i + 1 < len(starts) else ' -> end'}]") + + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f" FAILED: {result.stderr[-500:]}") + else: + print(f" OK -> {out_file}") + + track_counter += 1 + + print(f"\nDone. {track_counter - 1} tracks written to {out_dir}/") + + +if __name__ == "__main__": + main() diff --git a/projects/cuesplit/run b/projects/cuesplit/run new file mode 100755 index 0000000..15c5fb1 --- /dev/null +++ b/projects/cuesplit/run @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# ~/Projects/cuesplit/run — task dispatcher (symlinked into ~/.local/bin) +# Usage: run split +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" +CUESPLIT_PY="$SCRIPT_DIR/cuesplit.py" + +cmd_split() { + # Find exactly one .cue file in the current directory + shopt -s nullglob + cues=(*.cue *.CUE) + shopt -u nullglob + + if [ ${#cues[@]} -eq 0 ]; then + echo "No .cue file found in $(pwd)" + exit 1 + fi + + if [ ${#cues[@]} -gt 1 ]; then + echo "Multiple .cue files found, pick one:" + select cue in "${cues[@]}"; do + [ -n "$cue" ] && break + done + else + cue="${cues[0]}" + fi + + echo "Using cue: $cue" + python3 "$CUESPLIT_PY" "$cue" split + + echo "Opening split/ in Picard..." + picard "$(pwd)/split" >/dev/null 2>&1 & + disown +} + +case "${1:-}" in + split) + cmd_split + ;; + *) + echo "Usage: run " + echo "Commands:" + echo " split - auto-find .cue in current dir and split the audio" + exit 1 + ;; +esac diff --git a/projects/iac-run-menu/run b/projects/iac-run-menu/run new file mode 100755 index 0000000..a142425 --- /dev/null +++ b/projects/iac-run-menu/run @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -e + +PLAYBOOK="$1" + +if [ -z "$PLAYBOOK" ]; then + echo "Usage: run " + exit 1 +fi + +ANSIBLE_DIR="/path/to/iac/ansible" +VENV="$HOME/ansible-modern" +SSH_KEY="$HOME/.ssh/auto" +VAULT_FILE="$ANSIBLE_DIR/.vault_pass.txt" + +# Decide inventory based on playbook +case "$PLAYBOOK" in + initialization) + INVENTORY="$ANSIBLE_DIR/inventory_initialization.yml" + ;; + provisioning|compliance) + INVENTORY="$ANSIBLE_DIR/inventory.yml" + ;; + *) + echo "Unknown playbook: $PLAYBOOK" + exit 1 + ;; +esac + +cd "$ANSIBLE_DIR" + +exec "$VENV/bin/ansible-playbook" \ + -i "$INVENTORY" \ + "playbooks/${PLAYBOOK}.yml" \ + --vault-password-file "$VAULT_FILE" diff --git a/projects/iac-run-menu/run-menu b/projects/iac-run-menu/run-menu new file mode 100755 index 0000000..8e4a10e --- /dev/null +++ b/projects/iac-run-menu/run-menu @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -e + +# Path to your existing wrapper +WRAPPER="/path/to/iac/ansible/run" + +# List of playbooks +PLAYBOOKS=("initialization" "provisioning" "compliance" "Quit") + +# Interactive menu +PS3="Select playbook to run (number): " + +# Colored header (yellow) +YELLOW="\033[1;33m" +RESET="\033[0m" +echo -e "${YELLOW}=== Ansible Playbook Runner ===${RESET}" + +select opt in "${PLAYBOOKS[@]}"; do + case $opt in + "initialization"|"provisioning"|"compliance") + echo "Running playbook: $opt" + "$WRAPPER" "$opt" + break + ;; + "Quit") + echo "Exiting." + exit 0 + ;; + *) + echo "Invalid selection. Please choose a number from the list." + ;; + esac +done