#!/usr/bin/env bash # # song_downloader.sh — Songs aus segments.json suchen und als Video- oder # Audio-Segment mit yt-dlp herunterladen. # # Abhaengigkeiten: jq, yt-dlp, ffmpeg (fuer --force-keyframes-at-cuts / mp3-Extraktion) # Arch: sudo pacman -S jq yt-dlp ffmpeg # # Nutzung: # ./song_downloader.sh # interaktiver Suchmodus # ./song_downloader.sh # direkt mit Suchbegriff starten # ./song_downloader.sh -a # Audio (mp3) statt Video # ./song_downloader.sh -f meine.json # andere segments.json verwenden # set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" JSON_FILE="${SCRIPT_DIR}/segments.json" OUT_DIR="${SCRIPT_DIR}/downloads" MODE="video" # video | audio # --------------------------------------------------------------------------- # Argumente parsen # --------------------------------------------------------------------------- QUERY="" while [[ $# -gt 0 ]]; do case "$1" in -a|--audio) MODE="audio" shift ;; -v|--video) MODE="video" shift ;; -f|--file) JSON_FILE="$2" shift 2 ;; -o|--out) OUT_DIR="$2" shift 2 ;; -h|--help) grep '^#' "$0" | sed 's/^#//' exit 0 ;; *) QUERY="$1" shift ;; esac done # --------------------------------------------------------------------------- # Vorbedingungen pruefen # --------------------------------------------------------------------------- for bin in jq yt-dlp; do if ! command -v "$bin" &>/dev/null; then echo "Fehler: '$bin' ist nicht installiert (siehe Kopf des Scripts)." >&2 exit 1 fi done if [[ ! -f "$JSON_FILE" ]]; then echo "Fehler: Segment-Datei nicht gefunden: $JSON_FILE" >&2 exit 1 fi mkdir -p "$OUT_DIR" # --------------------------------------------------------------------------- # JSON in eine flache, mit Tabs getrennte Liste umwandeln: # songname \t videoId \t start \t end \t herkunfts-videotitel # --------------------------------------------------------------------------- build_index() { jq -r ' .[] as $stream | $stream.songs[] | [ .name, $stream.videoId, (.range[0]|tostring), (.range[1]|tostring), $stream.name ] | @tsv ' "$JSON_FILE" } sanitize_filename() { # Entfernt Zeichen, die auf den meisten Dateisystemen Probleme machen echo "$1" | sed -e 's#[/\\:*?"<>|]#_#g' -e 's/ */ /g' -e 's/^ *//;s/ *$//' } format_time() { local secs="$1" printf '%02d:%02d:%02d' $((secs/3600)) $(((secs%3600)/60)) $((secs%60)) } # --------------------------------------------------------------------------- # Song(s) anhand des Suchbegriffs finden (case-insensitive, Substring) # --------------------------------------------------------------------------- search_songs() { local term="$1" build_index | awk -F'\t' -v term="$term" ' BEGIN { IGNORECASE = 1 } index($1, term) > 0 { print } ' } # --------------------------------------------------------------------------- # Einen Treffer herunterladen # song_name, video_id, start, end, source_title # --------------------------------------------------------------------------- download_segment() { local song_name="$1" video_id="$2" start="$3" end="$4" source_title="$5" local clean_name url start_ts end_ts outfile clean_name="$(sanitize_filename "$song_name")" url="https://www.youtube.com/watch?v=${video_id}" start_ts="$(format_time "$start")" end_ts="$(format_time "$end")" echo "-> Lade '${song_name}' (${start_ts}-${end_ts}) aus '${source_title}' [${video_id}] ..." if [[ "$MODE" == "audio" ]]; then outfile="${OUT_DIR}/${clean_name}.%(ext)s" yt-dlp \ --download-sections "*${start}-${end}" \ --force-keyframes-at-cuts \ -x --audio-format mp3 \ --embed-thumbnail --embed-metadata \ -o "$outfile" \ "$url" else outfile="${OUT_DIR}/${clean_name}.%(ext)s" yt-dlp \ --download-sections "*${start}-${end}" \ --force-keyframes-at-cuts \ -f "bv*+ba/b" --merge-output-format mp4 \ --embed-thumbnail --embed-metadata \ -o "$outfile" \ "$url" fi } # --------------------------------------------------------------------------- # Auswahl aus Treffern anzeigen und Download anstossen # --------------------------------------------------------------------------- select_and_download() { local matches="$1" local count count="$(echo "$matches" | grep -c . || true)" if [[ "$count" -eq 0 ]]; then echo "Keine Songs gefunden." return fi echo echo "Gefundene Songs (Modus: $MODE):" local i=1 while IFS=$'\t' read -r song_name video_id start end source_title; do printf ' [%2d] %-45s (%s-%s, aus "%s")\n' \ "$i" "$song_name" "$(format_time "$start")" "$(format_time "$end")" "$source_title" i=$((i+1)) done <<< "$matches" echo " [ a] alle herunterladen" echo " [ q] abbrechen" echo read -rp "Auswahl (Nummer, 'a' oder Komma-getrennte Liste): " choice if [[ "$choice" == "q" ]]; then echo "Abgebrochen." return fi if [[ "$choice" == "a" ]]; then while IFS=$'\t' read -r song_name video_id start end source_title; do download_segment "$song_name" "$video_id" "$start" "$end" "$source_title" done <<< "$matches" return fi IFS=',' read -ra picks <<< "$choice" for pick in "${picks[@]}"; do pick="$(echo "$pick" | tr -d ' ')" line="$(echo "$matches" | sed -n "${pick}p")" if [[ -z "$line" ]]; then echo "Ungueltige Auswahl: $pick" >&2 continue fi IFS=$'\t' read -r song_name video_id start end source_title <<< "$line" download_segment "$song_name" "$video_id" "$start" "$end" "$source_title" done } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- if [[ -z "$QUERY" ]]; then read -rp "Suchbegriff (Songname): " QUERY fi matches="$(search_songs "$QUERY")" select_and_download "$matches"