Skip to content

Instantly share code, notes, and snippets.

@phnahes
Last active December 15, 2025 18:22
Show Gist options
  • Select an option

  • Save phnahes/6696ec1194768293908ecc387a1b5e04 to your computer and use it in GitHub Desktop.

Select an option

Save phnahes/6696ec1194768293908ecc387a1b5e04 to your computer and use it in GitHub Desktop.
Download Youtube Playlist mp3 files
#!/bin/bash
################################################################################
# YouTube Playlist to MP3 Downloader Script
#
# Description:
# This script automatically downloads YouTube playlists and converts
# them to high-quality MP3 files. Uses yt-dlp for downloading
# and audio conversion.
#
# Dependencies:
# - yt-dlp: video download tool
# - jq: command-line JSON processor
# - ffmpeg: audio/video converter (used by yt-dlp)
# - normalize-audio: audio volume normalization tool
#
# Author: Paulo Nahes
# Date: 11/30/2025
################################################################################
# ==================== DEFAULT SETTINGS ====================
# Default playlist URL (can be overridden with -u)
DEFAULT_PLAYLIST_URL="https://www.youtube.com/watch?v=&list=xxxxxxxxxxx"
# Default destination directory (can be overridden with -d)
DEFAULT_DEST_DIR="musicas"
# Default audio format (can be overridden with -f)
DEFAULT_AUDIO_FORMAT="mp3"
# Default audio quality: 0 (best) to 9 (worst)
DEFAULT_AUDIO_QUALITY="0"
# Default delay between downloads in seconds (can be overridden with -w)
DEFAULT_DELAY="5"
# Random delay (0 = disabled, 1 = enabled)
RANDOM_DELAY="1"
# Random delay range (minimum and maximum in seconds)
RANDOM_DELAY_MIN="0"
RANDOM_DELAY_MAX="30"
# Filename sanitization (0 = disabled, 1 = enabled)
SANITIZE_NAMES="1"
# Resume/checkpoint system
ENABLE_RESUME="1"
# Force re-download even if file exists (0 = no, 1 = yes)
FORCE_REDOWNLOAD="0"
# Temporary file for URLs
TEMP_URLS_FILE="urls_temp_$$.txt"
# Download progress log file
DOWNLOAD_LOG_FILE=".download_progress.log"
# Failed downloads log file
FAILED_LOG_FILE=".download_failed.log"
# ==================== OUTPUT COLORS ====================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# ==================== FUNCTIONS ====================
show_help() {
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}Script de Download de Playlists do YouTube em MP3${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${YELLOW}DESCRIÇÃO:${NC}"
echo " Este script baixa automaticamente playlists do YouTube e converte"
echo " os vídeos para arquivos MP3 de alta qualidade. Cada faixa é salva"
echo " com índice da playlist e título do vídeo."
echo ""
echo -e "${YELLOW}USO:${NC}"
echo " $0 [opções]"
echo ""
echo -e "${YELLOW}OPÇÕES:${NC}"
echo " -u URL URL da playlist do YouTube"
echo " (padrão: $DEFAULT_PLAYLIST_URL)"
echo ""
echo " -d DIR Diretório de destino para os arquivos MP3"
echo " (padrão: $DEFAULT_DEST_DIR)"
echo ""
echo " -f FORMAT Formato de áudio (mp3, aac, m4a, opus, vorbis, wav)"
echo " (padrão: $DEFAULT_AUDIO_FORMAT)"
echo ""
echo " -q QUALITY Qualidade de áudio (0-9, onde 0 é a melhor)"
echo " (padrão: $DEFAULT_AUDIO_QUALITY)"
echo ""
echo " -w SECONDS Tempo de espera entre downloads em segundos"
echo " (padrão: $DEFAULT_DELAY)"
echo ""
echo " -r Ativa delay ALEATÓRIO entre downloads"
echo " (entre $RANDOM_DELAY_MIN e $RANDOM_DELAY_MAX segundos, ignora -w)"
echo ""
echo " -s Desabilita sanitização de nomes de arquivos"
echo " (por padrão, caracteres especiais são removidos)"
echo ""
echo " -c Desabilita sistema de checkpoint/resume"
echo " (por padrão, downloads já feitos são pulados)"
echo ""
echo " -F FORÇA re-download de todas as faixas"
echo " (ignora arquivos existentes e checkpoint)"
echo ""
echo " -h Mostra esta mensagem de ajuda"
echo ""
echo -e "${YELLOW}EXEMPLOS:${NC}"
echo " # Usar configurações padrão"
echo " $0"
echo ""
echo " # Baixar playlist específica para diretório customizado"
echo " $0 -u \"https://www.youtube.com/playlist?list=PLxxxxxxxx\" -d \"minhas_musicas\""
echo ""
echo " # Baixar com qualidade média e sem delay"
echo " $0 -q 5 -w 0"
echo ""
echo " # Baixar em formato AAC de alta qualidade"
echo " $0 -f aac -q 0"
echo ""
echo " # Usar delay aleatório entre downloads (0-15 segundos)"
echo " $0 -r"
echo ""
echo " # Combinar delay aleatório com outras opções"
echo " $0 -u \"URL_DA_PLAYLIST\" -d \"musicas\" -r"
echo ""
echo " # Retomar download interrompido (checkpoint automático)"
echo " $0"
echo ""
echo " # Forçar re-download de tudo"
echo " $0 -F"
echo ""
echo -e "${YELLOW}DEPENDÊNCIAS NECESSÁRIAS:${NC}"
echo " - yt-dlp (instalação: brew install yt-dlp ou pip install yt-dlp)"
echo " - jq (instalação: brew install jq ou apt-get install jq)"
echo " - ffmpeg (instalação: brew install ffmpeg ou apt-get install ffmpeg)"
echo " - normalize-audio (instalação: brew install normalize ou apt-get install normalize-audio)"
echo ""
echo -e "${YELLOW}NOTAS:${NC}"
echo " - Os arquivos são salvos como: \"001 - Titulo da Musica.mp3\""
echo " - Caracteres especiais são removidos automaticamente (use -s para desabilitar)"
echo " - Índices são formatados com zeros à esquerda (001, 002, etc.)"
echo " - Sistema de checkpoint salva progresso automaticamente"
echo " - Downloads já concluídos são pulados automaticamente"
echo " - O script aguarda entre downloads para evitar sobrecarga"
echo " - URLs inválidas ou downloads com falha são reportados"
echo " - Arquivos temporários são limpos automaticamente"
echo ""
echo -e "${YELLOW}FALHAS COMUNS DE DOWNLOAD:${NC}"
echo " - Vídeos com restrição de idade: Não podem ser baixados"
echo " - Vídeos privados/deletados: Não estão mais disponíveis"
echo " - Conteúdo com restrição geográfica: Bloqueado na sua região"
echo " - Conteúdo premium: Requer YouTube Premium"
echo " - Lives ao vivo: Podem falhar se estiverem transmitindo"
echo " - Limite de requisições: Muitas requisições (use -w para delay)"
echo " - Problemas de rede: Conexão instável ou timeout"
echo ""
echo " Falhas são registradas em: .download_failed.log"
echo " Verifique este arquivo para mensagens de erro detalhadas"
echo ""
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
exit 0
}
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[✓]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[⚠]${NC} $1"
}
log_error() {
echo -e "${RED}[✗]${NC} $1"
}
sanitize_filename() {
local filename="$1"
# Convert to lowercase if desired (commented out by default)
# filename=$(echo "$filename" | tr '[:upper:]' '[:lower:]')
# Remove problematic special characters
# Removes: / \ : * ? " < > | [ ] { } ! @ # $ % ^ & = + ` ~ ; '
filename=$(echo "$filename" | sed 's/[\/\\:*?"<>|]/ /g')
filename=$(echo "$filename" | sed 's/[\[\]{}!@#$%^&=+`~;'\'']//g')
# Replace parentheses with spaces
filename=$(echo "$filename" | sed 's/[()]/ /g')
# Remove extra underscores and replace with spaces
filename=$(echo "$filename" | sed 's/_/ /g')
# Remove commas and semicolons
filename=$(echo "$filename" | sed 's/[,;]//g')
# Replace multiple spaces with a single space
filename=$(echo "$filename" | sed 's/ */ /g')
# Replace multiple hyphens with a single hyphen
filename=$(echo "$filename" | sed 's/--*/-/g')
# Remove leading and trailing spaces
filename=$(echo "$filename" | sed 's/^ *//;s/ *$//')
# Remove leading and trailing hyphens
filename=$(echo "$filename" | sed 's/^-*//;s/-*$//')
# Remove leading dots (but keep at the end for extensions)
filename=$(echo "$filename" | sed 's/^\.*//g')
# Limit filename length (maximum 150 characters to avoid problems)
if [ ${#filename} -gt 150 ]; then
filename="${filename:0:150}"
# Remove spaces/hyphens that may remain at the end after truncation
filename=$(echo "$filename" | sed 's/[ -]*$//')
fi
# If the name is empty, use a default name
if [ -z "$filename" ]; then
filename="track"
fi
echo "$filename"
}
check_dependencies() {
log_info "Checking required dependencies..."
local missing_deps=0
# Check yt-dlp
if ! command -v yt-dlp &> /dev/null; then
log_error "yt-dlp not found!"
echo " Install with: brew install yt-dlp OR pip install yt-dlp"
missing_deps=1
else
log_success "yt-dlp found: $(command -v yt-dlp)"
fi
# Check jq
if ! command -v jq &> /dev/null; then
log_error "jq not found!"
echo " Install with: brew install jq OR apt-get install jq"
missing_deps=1
else
log_success "jq found: $(command -v jq)"
fi
# Check ffmpeg
if ! command -v ffmpeg &> /dev/null; then
log_error "ffmpeg not found!"
echo " Install with: brew install ffmpeg OR apt-get install ffmpeg"
missing_deps=1
else
log_success "ffmpeg found: $(command -v ffmpeg)"
fi
# Check normalize-audio
if ! command -v normalize-audio &> /dev/null && ! command -v normalize &> /dev/null; then
log_error "normalize-audio not found!"
echo " Install with: brew install normalize OR apt-get install normalize-audio"
missing_deps=1
else
if command -v normalize-audio &> /dev/null; then
log_success "normalize-audio found: $(command -v normalize-audio)"
else
log_success "normalize found: $(command -v normalize)"
fi
fi
if [ $missing_deps -eq 1 ]; then
log_error "Missing dependencies. Please install them before continuing."
exit 1
fi
log_success "All dependencies are installed!"
echo ""
}
validate_url() {
local url="$1"
# Check if URL contains youtube.com or youtu.be
if [[ ! "$url" =~ youtube\.com|youtu\.be ]]; then
log_error "Invalid URL. Must be a YouTube URL."
return 1
fi
# Check if contains 'list=' (playlist indicator)
if [[ ! "$url" =~ list= ]]; then
log_warning "The URL may not be a playlist (missing 'list=' in URL)."
log_warning "Script will continue, but may process only one video."
fi
return 0
}
create_directory() {
local dir="$1"
if [ -d "$dir" ]; then
log_info "Directory already exists: $dir"
else
log_info "Creating directory: $dir"
if mkdir -p "$dir"; then
log_success "Directory created successfully!"
else
log_error "Failed to create directory: $dir"
exit 1
fi
fi
# Check write permissions
if [ ! -w "$dir" ]; then
log_error "No write permission for directory: $dir"
exit 1
fi
}
cleanup() {
if [ -f "$TEMP_URLS_FILE" ]; then
log_info "Removing temporary file..."
rm -f "$TEMP_URLS_FILE"
fi
}
is_track_downloaded() {
local track_index="$1"
local log_file="$2"
# Check in log file
if [ -f "$log_file" ] && grep -q "^${track_index}|" "$log_file" 2>/dev/null; then
return 0 # Already downloaded
fi
return 1 # Not downloaded
}
mark_track_downloaded() {
local track_index="$1"
local title="$2"
local log_file="$3"
# Add to log: INDEX|TITLE|TIMESTAMP
echo "${track_index}|${title}|$(date '+%Y-%m-%d %H:%M:%S')" >> "$log_file"
}
mark_track_failed() {
local track_index="$1"
local url="$2"
local error_msg="$3"
local log_file="$4"
# Add to failed log: INDEX|URL|ERROR|TIMESTAMP
echo "${track_index}|${url}|${error_msg}|$(date '+%Y-%m-%d %H:%M:%S')" >> "$log_file"
}
check_file_exists() {
local dest_dir="$1"
local track_index="$2"
local format="$3"
# Search for file starting with track index
local found_file=$(find "$dest_dir" -name "${track_index} - *.$format" -type f 2>/dev/null | head -n 1)
if [ -n "$found_file" ]; then
echo "$found_file"
return 0
fi
return 1
}
load_checkpoint_info() {
local log_file="$1"
if [ -f "$log_file" ]; then
local completed_count=$(wc -l < "$log_file" | tr -d ' ')
echo "$completed_count"
else
echo "0"
fi
}
normalize_audio_files() {
local dest_dir="$1"
local audio_format="$2"
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log_info "Starting audio normalization process..."
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Find all audio files
local audio_files=()
while IFS= read -r -d '' file; do
audio_files+=("$file")
done < <(find "$dest_dir" -name "*.$audio_format" -type f -print0 2>/dev/null)
local total_files=${#audio_files[@]}
if [ $total_files -eq 0 ]; then
log_warning "No audio files found to normalize."
return 0
fi
log_info "Found $total_files audio file(s) to normalize"
echo ""
# Determine which normalize command to use
local normalize_cmd=""
if command -v normalize-audio &> /dev/null; then
normalize_cmd="normalize-audio"
elif command -v normalize &> /dev/null; then
normalize_cmd="normalize"
else
log_error "normalize command not found!"
return 1
fi
log_info "Using normalize command: $normalize_cmd"
log_info "This may take a while depending on the number of files..."
echo ""
# Normalize all files at once
log_info "Normalizing audio levels..."
if "$normalize_cmd" -b "${audio_files[@]}" 2>&1 | while IFS= read -r line; do
echo " $line"
done; then
log_success "Audio normalization completed successfully!"
return 0
else
log_error "Audio normalization failed!"
return 1
fi
}
# Ensure cleanup on interruption
trap cleanup EXIT INT TERM
# ==================== ARGUMENT PROCESSING ====================
PLAYLIST_URL="$DEFAULT_PLAYLIST_URL"
DEST_DIR="$DEFAULT_DEST_DIR"
AUDIO_FORMAT="$DEFAULT_AUDIO_FORMAT"
AUDIO_QUALITY="$DEFAULT_AUDIO_QUALITY"
DELAY="$DEFAULT_DELAY"
USE_RANDOM_DELAY="$RANDOM_DELAY"
USE_SANITIZE="$SANITIZE_NAMES"
USE_RESUME="$ENABLE_RESUME"
FORCE_DOWNLOAD="$FORCE_REDOWNLOAD"
while getopts "u:d:f:q:w:rscFh" opt; do
case $opt in
u)
PLAYLIST_URL="$OPTARG"
;;
d)
DEST_DIR="$OPTARG"
;;
f)
AUDIO_FORMAT="$OPTARG"
;;
q)
AUDIO_QUALITY="$OPTARG"
;;
w)
DELAY="$OPTARG"
;;
r)
USE_RANDOM_DELAY="1"
;;
s)
USE_SANITIZE="0"
;;
c)
USE_RESUME="0"
;;
F)
FORCE_DOWNLOAD="1"
;;
h)
show_help
;;
\?)
log_error "Invalid option: -$OPTARG"
echo "Use -h for help"
exit 1
;;
:)
log_error "Option -$OPTARG requires an argument"
echo "Use -h for help"
exit 1
;;
esac
done
# ==================== MAIN SCRIPT ====================
# Show help if no arguments provided
if [ $# -eq 0 ]; then
show_help
fi
echo ""
log_info "═══════════════════════════════════════════════════════════"
log_info " Starting YouTube Playlist Download"
log_info "═══════════════════════════════════════════════════════════"
echo ""
# Check dependencies
check_dependencies
# Validate URL
log_info "Validating playlist URL..."
if ! validate_url "$PLAYLIST_URL"; then
exit 1
fi
log_success "URL validated!"
echo ""
# Create/check destination directory
log_info "Setting up destination directory..."
create_directory "$DEST_DIR"
echo ""
# Configure checkpoint log file
CHECKPOINT_LOG="$DEST_DIR/$DOWNLOAD_LOG_FILE"
FAILED_LOG="$DEST_DIR/$FAILED_LOG_FILE"
# Check if previous checkpoint exists
PREVIOUSLY_DOWNLOADED=0
if [ "$USE_RESUME" -eq 1 ] && [ "$FORCE_DOWNLOAD" -eq 0 ]; then
PREVIOUSLY_DOWNLOADED=$(load_checkpoint_info "$CHECKPOINT_LOG")
if [ "$PREVIOUSLY_DOWNLOADED" -gt 0 ]; then
log_info "Checkpoint found: $PREVIOUSLY_DOWNLOADED track(s) already downloaded"
fi
fi
# If force re-download, clear checkpoint
if [ "$FORCE_DOWNLOAD" -eq 1 ]; then
if [ -f "$CHECKPOINT_LOG" ]; then
log_warning "FORCE mode enabled: removing previous checkpoint..."
rm -f "$CHECKPOINT_LOG"
fi
fi
# Display settings
log_info "Download settings:"
echo " Playlist URL: $PLAYLIST_URL"
echo " Directory: $DEST_DIR"
echo " Format: $AUDIO_FORMAT"
echo " Quality: $AUDIO_QUALITY (0=best, 9=worst)"
if [ "$USE_RANDOM_DELAY" -eq 1 ]; then
echo " Delay between downloads: RANDOM (${RANDOM_DELAY_MIN}-${RANDOM_DELAY_MAX}s)"
else
echo " Delay between downloads: ${DELAY}s"
fi
if [ "$USE_SANITIZE" -eq 1 ]; then
echo " Name sanitization: ENABLED"
else
echo " Name sanitization: DISABLED"
fi
if [ "$USE_RESUME" -eq 1 ] && [ "$FORCE_DOWNLOAD" -eq 0 ]; then
echo " Resume system: ENABLED"
else
echo " Resume system: DISABLED"
fi
if [ "$FORCE_DOWNLOAD" -eq 1 ]; then
echo " Force mode: ENABLED (re-download everything)"
fi
echo ""
# Extract playlist URLs list
log_info "Extracting playlist URLs..."
if ! yt-dlp --flat-playlist -J "$PLAYLIST_URL" 2>/dev/null | jq -r '.entries[] | .url' > "$TEMP_URLS_FILE"; then
log_error "Failed to extract playlist URLs. Check the URL."
exit 1
fi
# Check if there are URLs
if [ ! -s "$TEMP_URLS_FILE" ]; then
log_error "No URLs found in playlist."
exit 1
fi
TOTAL_TRACKS=$(wc -l < "$TEMP_URLS_FILE" | tr -d ' ')
log_success "Found $TOTAL_TRACKS tracks in playlist!"
echo ""
# Process each track
CURRENT_TRACK=0
SUCCESSFUL_DOWNLOADS=0
FAILED_DOWNLOADS=0
SKIPPED_DOWNLOADS=0
while IFS= read -r URL; do
CURRENT_TRACK=$((CURRENT_TRACK + 1))
echo ""
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log_info "Processing track $CURRENT_TRACK of $TOTAL_TRACKS"
log_info "URL: $URL"
log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Format index with leading zeros (001, 002, etc.)
TRACK_INDEX=$(printf "%03d" $CURRENT_TRACK)
# Check if track was already downloaded (checkpoint)
SKIP_DOWNLOAD=0
if [ "$USE_RESUME" -eq 1 ] && [ "$FORCE_DOWNLOAD" -eq 0 ]; then
# Check in checkpoint log
if is_track_downloaded "$TRACK_INDEX" "$CHECKPOINT_LOG"; then
log_warning "Track already downloaded (checkpoint). Skipping..."
SKIPPED_DOWNLOADS=$((SKIPPED_DOWNLOADS + 1))
SKIP_DOWNLOAD=1
else
# Check if file exists in directory
EXISTING_FILE=$(check_file_exists "$DEST_DIR" "$TRACK_INDEX" "$AUDIO_FORMAT")
if [ -n "$EXISTING_FILE" ]; then
log_warning "File already exists: $(basename "$EXISTING_FILE")"
log_info "Updating checkpoint..."
# Add to checkpoint
EXISTING_TITLE=$(basename "$EXISTING_FILE" | sed "s/${TRACK_INDEX} - //;s/\.$AUDIO_FORMAT$//")
mark_track_downloaded "$TRACK_INDEX" "$EXISTING_TITLE" "$CHECKPOINT_LOG"
SKIPPED_DOWNLOADS=$((SKIPPED_DOWNLOADS + 1))
SKIP_DOWNLOAD=1
fi
fi
fi
# If should skip, continue to next track
if [ "$SKIP_DOWNLOAD" -eq 1 ]; then
continue
fi
# Execute download
DOWNLOAD_SUCCESS=0
TRACK_TITLE=""
ERROR_LOG_FILE="$DEST_DIR/.error_${TRACK_INDEX}.log"
if [ "$USE_SANITIZE" -eq 1 ]; then
# Download with temporary template and rename later
TEMP_TEMPLATE="$DEST_DIR/temp_${CURRENT_TRACK}_%(title)s.%(ext)s"
if yt-dlp -x --audio-format "$AUDIO_FORMAT" --audio-quality "$AUDIO_QUALITY" \
-o "$TEMP_TEMPLATE" "$URL" 2>"$ERROR_LOG_FILE"; then
# Find downloaded file
DOWNLOADED_FILE=$(find "$DEST_DIR" -name "temp_${CURRENT_TRACK}_*.$AUDIO_FORMAT" -type f | head -n 1)
if [ -n "$DOWNLOADED_FILE" ]; then
# Extract title from filename
FILENAME=$(basename "$DOWNLOADED_FILE")
TITLE=$(echo "$FILENAME" | sed "s/temp_${CURRENT_TRACK}_//;s/\.$AUDIO_FORMAT$//")
# Sanitize title
CLEAN_TITLE=$(sanitize_filename "$TITLE")
# New filename
NEW_FILENAME="${TRACK_INDEX} - ${CLEAN_TITLE}.${AUDIO_FORMAT}"
NEW_FILEPATH="$DEST_DIR/$NEW_FILENAME"
# Rename file
mv "$DOWNLOADED_FILE" "$NEW_FILEPATH"
log_success "Download completed: $NEW_FILENAME"
SUCCESSFUL_DOWNLOADS=$((SUCCESSFUL_DOWNLOADS + 1))
DOWNLOAD_SUCCESS=1
TRACK_TITLE="$CLEAN_TITLE"
# Remove error log if successful
rm -f "$ERROR_LOG_FILE"
else
log_error "Downloaded file not found."
FAILED_DOWNLOADS=$((FAILED_DOWNLOADS + 1))
# Log failure
if [ -f "$ERROR_LOG_FILE" ]; then
ERROR_MSG=$(tail -n 1 "$ERROR_LOG_FILE" | tr -d '\n')
log_warning "Error: $ERROR_MSG"
mark_track_failed "$TRACK_INDEX" "$URL" "$ERROR_MSG" "$FAILED_LOG"
fi
fi
else
FAILED_DOWNLOADS=$((FAILED_DOWNLOADS + 1))
log_error "Failed to download this track."
# Read and display error
if [ -f "$ERROR_LOG_FILE" ]; then
ERROR_MSG=$(grep -i "error\|ERROR" "$ERROR_LOG_FILE" | tail -n 1 | tr -d '\n')
if [ -z "$ERROR_MSG" ]; then
ERROR_MSG=$(tail -n 1 "$ERROR_LOG_FILE" | tr -d '\n')
fi
log_warning "Reason: ${ERROR_MSG:0:200}"
mark_track_failed "$TRACK_INDEX" "$URL" "$ERROR_MSG" "$FAILED_LOG"
else
mark_track_failed "$TRACK_INDEX" "$URL" "Unknown error" "$FAILED_LOG"
fi
fi
# Clean up error log
rm -f "$ERROR_LOG_FILE"
else
# Download without sanitization
OUTPUT_TEMPLATE="$DEST_DIR/${TRACK_INDEX} - %(title)s.%(ext)s"
if yt-dlp -x --audio-format "$AUDIO_FORMAT" --audio-quality "$AUDIO_QUALITY" \
-o "$OUTPUT_TEMPLATE" "$URL" 2>"$ERROR_LOG_FILE"; then
SUCCESSFUL_DOWNLOADS=$((SUCCESSFUL_DOWNLOADS + 1))
log_success "Download completed successfully!"
DOWNLOAD_SUCCESS=1
# Try to extract title from created file
CREATED_FILE=$(find "$DEST_DIR" -name "${TRACK_INDEX} - *.$AUDIO_FORMAT" -type f | head -n 1)
if [ -n "$CREATED_FILE" ]; then
TRACK_TITLE=$(basename "$CREATED_FILE" | sed "s/${TRACK_INDEX} - //;s/\.$AUDIO_FORMAT$//")
fi
# Remove error log if successful
rm -f "$ERROR_LOG_FILE"
else
FAILED_DOWNLOADS=$((FAILED_DOWNLOADS + 1))
log_error "Failed to download this track."
# Read and display error
if [ -f "$ERROR_LOG_FILE" ]; then
ERROR_MSG=$(grep -i "error\|ERROR" "$ERROR_LOG_FILE" | tail -n 1 | tr -d '\n')
if [ -z "$ERROR_MSG" ]; then
ERROR_MSG=$(tail -n 1 "$ERROR_LOG_FILE" | tr -d '\n')
fi
log_warning "Reason: ${ERROR_MSG:0:200}"
mark_track_failed "$TRACK_INDEX" "$URL" "$ERROR_MSG" "$FAILED_LOG"
else
mark_track_failed "$TRACK_INDEX" "$URL" "Unknown error" "$FAILED_LOG"
fi
fi
# Clean up error log
rm -f "$ERROR_LOG_FILE"
fi
# Register in checkpoint if download successful
if [ "$DOWNLOAD_SUCCESS" -eq 1 ] && [ "$USE_RESUME" -eq 1 ]; then
if [ -z "$TRACK_TITLE" ]; then
TRACK_TITLE="Unknown Track"
fi
mark_track_downloaded "$TRACK_INDEX" "$TRACK_TITLE" "$CHECKPOINT_LOG"
log_info "Checkpoint updated."
fi
# Delay between downloads (except for the last one)
if [ $CURRENT_TRACK -lt $TOTAL_TRACKS ]; then
if [ "$USE_RANDOM_DELAY" -eq 1 ]; then
# Generate random number between RANDOM_DELAY_MIN and RANDOM_DELAY_MAX
CURRENT_DELAY=$((RANDOM % (RANDOM_DELAY_MAX - RANDOM_DELAY_MIN + 1) + RANDOM_DELAY_MIN))
log_info "Waiting ${CURRENT_DELAY}s (random) before next track..."
sleep "$CURRENT_DELAY"
elif [ "$DELAY" -gt 0 ]; then
log_info "Waiting ${DELAY}s before next track..."
sleep "$DELAY"
fi
fi
done < "$TEMP_URLS_FILE"
# Final summary
echo ""
echo ""
log_info "═══════════════════════════════════════════════════════════"
log_info " DOWNLOAD SUMMARY"
log_info "═══════════════════════════════════════════════════════════"
log_success "Successful downloads: $SUCCESSFUL_DOWNLOADS"
if [ $SKIPPED_DOWNLOADS -gt 0 ]; then
log_warning "Skipped tracks (already exist): $SKIPPED_DOWNLOADS"
fi
if [ $FAILED_DOWNLOADS -gt 0 ]; then
log_error "Failed downloads: $FAILED_DOWNLOADS"
fi
log_info "Total tracks: $TOTAL_TRACKS"
log_info "Destination directory: $(cd "$DEST_DIR" && pwd)"
if [ "$USE_RESUME" -eq 1 ]; then
log_info "Checkpoint saved at: $CHECKPOINT_LOG"
fi
if [ $FAILED_DOWNLOADS -gt 0 ] && [ -f "$FAILED_LOG" ]; then
log_info "Failed tracks log: $FAILED_LOG"
fi
log_info "═══════════════════════════════════════════════════════════"
echo ""
TOTAL_COMPLETED=$((SUCCESSFUL_DOWNLOADS + SKIPPED_DOWNLOADS))
if [ $TOTAL_COMPLETED -eq $TOTAL_TRACKS ]; then
log_success "All tracks have been processed! 🎵"
elif [ $SUCCESSFUL_DOWNLOADS -gt 0 ]; then
log_success "Download completed with $SUCCESSFUL_DOWNLOADS new track(s)! 🎵"
if [ $FAILED_DOWNLOADS -gt 0 ]; then
log_warning "Run again to retry failed tracks."
fi
else
if [ $SKIPPED_DOWNLOADS -eq $TOTAL_TRACKS ]; then
log_success "All tracks were already downloaded previously! 🎵"
else
log_error "No new downloads completed successfully."
if [ $FAILED_DOWNLOADS -gt 0 ]; then
log_warning "Run again to retry failed tracks."
fi
exit 1
fi
fi
# Normalize audio levels after all downloads
echo ""
echo ""
if normalize_audio_files "$DEST_DIR" "$AUDIO_FORMAT"; then
log_success "All audio files have been normalized to the same volume level! 🔊"
else
log_warning "Audio normalization encountered issues. Files may have different volume levels."
fi
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment