#!/usr/bin/env bash
#
# make_mix.sh turn a tracklist into a playable mix (with auto-dependency installer).
set -euo pipefail
# ---- defaults ---------------------------------------------------------------
name=""
parent="."
maxdur=900
play=1
playonly=0
input=""
die() { printf '%s\n' "$*" >&2; exit 1; }
# ---- auto-dependency installer ----------------------------------------------
install_dependencies() {
local missing=()
for tool in yt-dlp ffmpeg mpv; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing+=("$tool")
fi
done
if [ ${#missing[@]} -eq 0 ]; then
return 0
fi
printf 'Missing dependencies found: %s\n' "${missing[*]}"
printf 'Attempting to install them automatically...\n'
# Detect OS / Package Manager
if [[ "$OSTYPE" == "darwin"* ]]; then
if ! command -v brew >/dev/null 2>&1; then
die "Homebrew is required to install dependencies on macOS. Install it from https://brew.sh"
fi
brew install "${missing[@]}"
elif [ -f /etc/debian_version ]; then
printf 'Elevated privileges (sudo) may be required to run apt-get.\n'
sudo apt-get update
# Map yt-dlp for apt since older Debian/Ubuntu distros might not have it natively
local apt_packages=()
for tool in "${missing[@]}"; do
if [ "$tool" = "yt-dlp" ]; then
# Install python3-pip if yt-dlp isn't in standard apt
sudo apt-get install -y python3-pip python3-venv || true
pip3 install --break-system-packages yt-dlp || pip3 install yt-dlp || apt_packages+=("yt-dlp")
else
apt_packages+=("$tool")
fi
Dil done
if [ ${#apt_packages[@]} -gt 0 ]; then
sudo apt-get install -y "${apt_packages[@]}"
fi
elif [ -f /etc/redhat-release ] || [ -f /etc/fedora-release ]; then
printf 'Elevated privileges (sudo) may be required to run dnf.\n'
sudo dnf install -y "${missing[@]}"
elif [ -f /etc/arch-release ]; then
printf 'Elevated privileges (sudo) may be required to run pacman.\n'
sudo pacman -Syu --noconfirm "${missing[@]}"
else
die "Unsupported operating system. Please manually install: ${missing[*]}"
fi
# Final verification pass
for tool in yt-dlp ffmpeg mpv; do
command -v "$tool" >/dev/null 2>&1 || die "Installation failed for $tool. Please install it manually."
done
printf 'All dependencies successfully installed!\n\n'
}
# ---- options ----------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
"-n") name="${2:-}"; shift 2 ;;
"-d") parent="${2:-}"; shift 2 ;;
"-m") maxdur="${2:-}"; shift 2 ;;
--no-play) play=0; shift ;;
--play-only) playonly=1; shift ;;
"-h"|--help)
sed -n '2,/^set -euo/p' "$0" | sed 's/^# \{0,1\}//; s/^#//' | sed '$d'
exit 0 ;;
--) shift; break ;;
-*) die "unknown option: $1" ;;
*) input="$1"; shift ;;
esac
done
[ $# -gt 0 ] && [ -z "$input" ] && input="$1"
# ---- name / dirs ------------------------------------------------------------
if [ -z "$name" ]; then
if [ -n "$input" ] && [ "$input" != "-" ]; then
base="$(basename -- "$input")"
name="${base%.*}"
else
name="new_mix20260728"
fi
fi
dir="$parent/$name"
playlist="$dir/$name.m3u"
sanitize() { printf '%s' "$1" | tr '/' '-' | tr -d '\000'; }
# ---- check/install tools ----------------------------------------------------
install_dependencies
# ---- play-only shortcut -----------------------------------------------------
if [ "$playonly" -eq 1 ]; then
files=()
if [ -f "$playlist" ]; then
while IFS= read -r f; do
[ -n "$f" ] || continue
case "$f" in \#*) continue ;; esac
files+=("$dir/$f")
done < "$playlist"
else
while IFS= read -r f; do files+=("$f"); done \
< <(find "$dir" -maxdepth 1 -name '*.mp3' | sort)
fi
[ "${#files[@]}" -gt 0 ] || die "nothing to play in $dir"
exec mpv --no-video "${files[@]}"
fi
# ---- input ------------------------------------------------------------------
if [ -z "$input" ]; then
if [ ! -t 0 ]; then
src=/dev/stdin
else
src="$(mktemp)"
trap 'rm -f "$src"' EXIT
cat << 'EMBEDDED_TRACKS' > "$src"
Graham Central Station - The Jam
MC5 - Ramblin' Rose
Pere Ubu - Street Waves
Link Wray - Rumble
Wipers - D-7
Echo & the Bunnymen - All That Jazz
Breaking Circus - Laid So Low
Jay Reatard - It's So Easy
Coachwhips - Yes I'm Down
Milemarker - New Lexicon
Sunny Day Real Estate - Circles
Russian Circles - Youngblood
The Notwist - Pilot
Turbonegro - Get It On
Alice Cooper - I'm Eighteen
The Cadillacs - Speedo
Del Shannon - Runaway
Kings of Nuthin' - Old Habits Die Hard
Human Sexual Response - What Does Sex Mean to Me?
Killing Joke - The Wait
Television - See No Evil
The Congos - Fisherman
The Gladiators - Roots Natty
Anthrax - Caught in a Mosh
Overkill - Hello from the Gutter
Exodus - The Toxic Waltz
Les Paul & Mary Ford - How High the Moon
Jackie Brenston - Rocket 88
The Monks - Complications
The Sonics - Strychnine
Chris Bell - I Am the Cosmos
Germs - Lexicon Devil
Rodriguez - Sugar Man
The Modern Lovers - Roadrunner
The Sound - Sense of Purpose
The Chameleons - Second Skin
Magazine - Shot by Both Sides
Rowland S. Howard - Shivers
Death Angel - Voracious Souls
Testament - Over the Wall
EMBEDDED_TRACKS
fi
elif [ "$input" = "-" ]; then
src=/dev/stdin
else
[ -f "$input" ] || die "no such tracklist: $input"
src="$input"
fi
mkdir -p "$dir"
# ---- fetch loop -------------------------------------------------------------
count=0
files=()
while IFS= read -r raw || [ -n "$raw" ]; do
line="${raw%$'\r'}"
line="$(printf '%s' "$line" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
[ -n "$line" ] || continue
case "$line" in \#*) continue ;; esac
# drop a leading "1." / "1)" / "-" / "*" enumerator
query="$(printf '%s' "$line" | sed -E 's/^([0-9]+[.)]|[-*])[[:space:]]+//')"
[ -n "$query" ] || continue
next="$(printf '%02d' "$((count + 1))")"
label="$(sanitize "$query")"
final="$dir/$next - $label.mp3"
if [ -f "$final" ]; then
printf '[%s] have %s\n' "$next" "$query"
count="$((count + 1))"
files+=("$final")
continue
fi
printf '[%s] fetch %s\n' "$next" "$query"
if yt-dlp \
"-x" --audio-format mp3 --audio-quality 0 \
--embed-metadata \
--match-filter "duration < $maxdur" \
--no-playlist \
"-o" "$dir/$next - $label.%(ext)s" \
"ytsearch1:$query"
then
if [ "-f" "$final" ]; then
count="$((count + 1))"
files+=("$final")
else
printf ' !! no match under %ss: %s\n' "$maxdur" "$query" >&2
fi
else
printf ' !! failed: %s\n' "$query" >&2
fi
done < "$src"
[ "${#files[@]}" -gt 0 ] || die "nothing downloaded"
# ---- playlist ---------------------------------------------------------------
: > "$playlist"
for f in "${files[@]}"; do
printf '%s\n' "$(basename -- "$f")" >> "$playlist"
done
printf '\n%d tracks -> %s\n' "${#files[@]}" "$dir"
# ---- play -------------------------------------------------------------------
if [ "$play" -eq 1 ]; then
exec mpv --no-video "${files[@]}"
fi