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.
This commit is contained in:
7 files changed
+348
No files matched your search
@@ -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
|
## 👀 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:
|
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:
|
||||||
|
|
||||||
|
|||||||
Executable
+56
@@ -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 <playbook> [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[@]}" \
|
||||||
|
"$@"
|
||||||
Executable
+44
@@ -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
|
||||||
Executable
+124
@@ -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 <cuefile> [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()
|
||||||
Executable
+47
@@ -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 <command>"
|
||||||
|
echo "Commands:"
|
||||||
|
echo " split - auto-find .cue in current dir and split the audio"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PLAYBOOK="$1"
|
||||||
|
|
||||||
|
if [ -z "$PLAYBOOK" ]; then
|
||||||
|
echo "Usage: run <provisioning|initialization|compliance>"
|
||||||
|
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"
|
||||||
Executable
+33
@@ -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
|
||||||
Reference in new issue
Block a user