Files
Podcast-Downloader/podcast-downloader.sh
T

377 lines
11 KiB
Bash

#!/usr/bin/env bash
set -u
set -o pipefail
CONFIG_FILE="/opt/podcast-pownloader/PODCAST_NAME/podcast_settings.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "ERROR: Configuration file not found at $CONFIG_FILE"
exit 1
fi
source "$CONFIG_FILE"
validate_config() {
local required_vars=(
"DOWNLOAD_DIR" "FINAL_DIR" "LOG_DIR" "LOG_FILE"
"DOWNLOADS_LOG" "DOWNLOADS_LOCK" "MAX_LOG_SIZE"
"QUALITY" "VIDEO_FORMAT" "AUDIO_FORMAT"
"SLEEP_INTERVAL" "RATE_LIMIT" "CONNECTION_TIMEOUT" "MAX_RETRIES"
)
for var in "${required_vars[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "ERROR: Required variable '$var' not set in $CONFIG_FILE"
exit 1
fi
done
}
validate_config
mkdir -p "$DOWNLOAD_DIR" "$FINAL_DIR" "$LOG_DIR" "$(dirname "$DOWNLOADS_LOG")"
[[ -f "$LOG_FILE" ]] || : > "$LOG_FILE"
if [[ ! -f "$DOWNLOADS_LOG" ]]; then
{
echo "# YouTube Downloads Database - $(date)"
echo "# Format: VIDEO_ID|TITLE|FILENAME|DOWNLOADED_DATE"
} >> "$DOWNLOADS_LOG"
fi
rotate_log() {
local size
size=$(stat -c%s "$LOG_FILE" 2>/dev/null || stat -f%z "$LOG_FILE" 2>/dev/null || echo 0)
if [[ "$size" -gt "$MAX_LOG_SIZE" ]]; then
mv "$LOG_FILE" "$LOG_FILE.$(date +%s)"
echo "# YouTube Downloader Log - $(date)" > "$LOG_FILE"
log_message "Log rotated (previous log too large)"
fi
}
log_message() {
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $1" | tee -a "$LOG_FILE"
rotate_log
}
sanitize_filename() {
local name="$1"
name="${name//\//-}"
name="${name//:/-}"
name="${name//\*/-}"
name="${name//\?/}"
name="${name//\"/}"
name="${name//</}"
name="${name//>/}"
name="${name//|/-}"
name="${name//\\/}"
echo "$name" | tr -s ' ' ' ' | sed 's/^ *//; s/ *$//'
}
acquire_lock() {
local timeout=30
local elapsed=0
while [[ $elapsed -lt $timeout ]]; do
if mkdir "$DOWNLOADS_LOCK" 2>/dev/null; then
return 0
fi
sleep 1
((elapsed++))
done
return 1
}
release_lock() {
rmdir "$DOWNLOADS_LOCK" 2>/dev/null || true
}
is_downloaded() {
local video_id="${1:-}"
[[ -z "$video_id" ]] && return 1
acquire_lock || {
log_message "WARNING: Could not acquire lock for is_downloaded check"
return 1
}
grep -q -F -- "${video_id}|" "$DOWNLOADS_LOG"
local result=$?
release_lock
return $result
}
record_download() {
local video_id="${1:-}"
local title="${2:-}"
local filename="${3:-}"
acquire_lock || {
log_message "ERROR: Could not acquire lock for recording download"
return 1
}
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "${video_id}|${title}|${filename}|${timestamp}" >> "$DOWNLOADS_LOG"
local result=$?
release_lock
return $result
}
process_downloaded_video() {
local video_id="${1:-}"
local title="${2:-}"
local temp_dir="${3:-}"
if [[ -z "$video_id" || -z "$temp_dir" ]]; then
log_message "ERROR: Missing video_id or temp_dir in process_downloaded_video"
return 1
fi
if is_downloaded "$video_id"; then
log_message "SKIP: '$title' (already in database)"
find "$temp_dir" -maxdepth 1 -type f -name "${video_id}.*" -delete 2>/dev/null
return 0
fi
local video_file audio_file
video_file=$(find "$temp_dir" -maxdepth 1 -type f -name "${video_id}.video.*" 2>/dev/null | head -n 1)
audio_file=$(find "$temp_dir" -maxdepth 1 -type f -name "${video_id}.audio.mp3" 2>/dev/null | head -n 1)
if [[ -z "${video_file:-}" || ! -f "$video_file" ]]; then
log_message "ERROR: Video file not found for '$title' (id: $video_id)"
return 1
fi
if [[ -z "${audio_file:-}" || ! -f "$audio_file" ]]; then
log_message "ERROR: Audio MP3 file not found for '$title' (id: $video_id)"
return 1
fi
local clean_title base_name final_video final_audio
clean_title=$(sanitize_filename "$title")
base_name="$clean_title"
final_video="$FINAL_DIR/${base_name}.${VIDEO_FORMAT}"
final_audio="$FINAL_DIR/${base_name}.mp3"
if [[ -e "$final_video" || -e "$final_audio" ]]; then
base_name="${clean_title}_${video_id}"
final_video="$FINAL_DIR/${base_name}.${VIDEO_FORMAT}"
final_audio="$FINAL_DIR/${base_name}.mp3"
fi
if mv "$video_file" "$final_video" && mv "$audio_file" "$final_audio"; then
if record_download "$video_id" "$title" "${base_name}.${VIDEO_FORMAT} + ${base_name}.mp3"; then
log_message "MOVED: '$title' -> '${base_name}.${VIDEO_FORMAT}' and '${base_name}.mp3'"
return 0
else
log_message "ERROR: Failed to record download for '$title'"
return 1
fi
else
log_message "ERROR: Failed to move files for '$title' to $FINAL_DIR"
return 1
fi
}
wait_for_next() {
local remaining=$SLEEP_INTERVAL
while [[ $remaining -gt 0 ]]; do
printf "\rWaiting %ss before next download...\033[K" "$remaining"
sleep 1
((remaining--))
done
echo
}
format_time() {
local seconds=$1
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
local secs=$((seconds % 60))
if [[ $hours -gt 0 ]]; then
printf "%dh %dm %ds" "$hours" "$minutes" "$secs"
elif [[ $minutes -gt 0 ]]; then
printf "%dm %ds" "$minutes" "$secs"
else
printf "%ds" "$secs"
fi
}
estimate_download_time() {
local playlist_url="${1:-}"
[[ -z "$playlist_url" ]] && return 0
log_message "Estimating playlist size..."
local video_count
video_count=$(yt-dlp --no-warnings --flat-playlist --print "%(id)s" "$playlist_url" 2>/dev/null \
| grep -E '^[a-zA-Z0-9_-]{11}$' | wc -l)
if [[ $video_count -gt 0 ]]; then
local estimated_seconds=$(( (video_count * 300) + (video_count * SLEEP_INTERVAL) ))
log_message "Estimated: $video_count videos, approximately $(format_time "$estimated_seconds") total time"
fi
}
show_log() {
echo
echo "=== Downloaded Videos (Last 20) ==="
echo
tail -n 20 "$DOWNLOADS_LOG" 2>/dev/null | grep -E '^[a-zA-Z0-9_-]{11}\|' | \
awk -F'|' '{printf "%-15s | %-50s | %s\n", $1, substr($2,1,50), $4}' || echo "No downloads yet"
}
download_playlist() {
local playlist_url="${1:-}"
local temp_dir=""
local video_list=""
local current=0
local video_count=0
if [[ -z "$playlist_url" ]]; then
log_message "ERROR: No playlist URL provided"
echo "Usage: $0 [youtube_playlist_url]"
return 1
fi
cleanup() {
[[ -n "${video_list:-}" && -f "$video_list" ]] && rm -f "$video_list"
[[ -n "${temp_dir:-}" && -d "$temp_dir" ]] && rm -rf "$temp_dir"
}
trap cleanup EXIT
log_message "Starting download for playlist: $playlist_url"
log_message "Settings: Quality=$QUALITY, Rate Limit=$RATE_LIMIT, Sleep Interval=${SLEEP_INTERVAL}s"
temp_dir=$(mktemp -d) || {
log_message "ERROR: Failed to create temporary directory"
return 1
}
video_list=$(mktemp) || {
log_message "ERROR: Failed to create temporary file"
return 1
}
log_message "Fetching playlist information..."
yt-dlp \
--no-warnings \
--flat-playlist \
--socket-timeout "$CONNECTION_TIMEOUT" \
--retries "$MAX_RETRIES" \
--print "%(id)s" \
"$playlist_url" 2>>"$LOG_FILE" | grep -E '^[a-zA-Z0-9_-]{11}$' > "$video_list"
if [[ ! -s "$video_list" ]]; then
log_message "ERROR: Failed to fetch playlist or playlist is empty"
return 1
fi
video_count=$(wc -l < "$video_list")
log_message "Found $video_count videos to process"
while IFS= read -r video_id; do
((current++))
[[ -z "${video_id:-}" ]] && continue
if ! [[ "$video_id" =~ ^[a-zA-Z0-9_-]{11}$ ]]; then
log_message "[$current/$video_count] WARNING: Invalid video ID format: '$video_id'"
continue
fi
title=$(yt-dlp \
--no-warnings \
--skip-download \
--socket-timeout "$CONNECTION_TIMEOUT" \
--retries "$MAX_RETRIES" \
--print "%(title)s" \
"https://www.youtube.com/watch?v=$video_id" 2>>"$LOG_FILE" | head -n 1)
[[ -z "$title" || "$title" == "NA" ]] && title="$video_id"
if is_downloaded "$video_id"; then
log_message "[$current/$video_count] SKIP: '$title' (already downloaded)"
continue
fi
log_message "[$current/$video_count] Downloading: '$title'"
yt-dlp \
--no-warnings \
--format "$QUALITY" \
--merge-output-format "$VIDEO_FORMAT" \
--limit-rate "$RATE_LIMIT" \
--socket-timeout "$CONNECTION_TIMEOUT" \
--retries "$MAX_RETRIES" \
--no-continue \
--output "$temp_dir/%(id)s.video.%(ext)s" \
"https://www.youtube.com/watch?v=$video_id" \
2>>"$LOG_FILE"
video_status=$?
yt-dlp \
--no-warnings \
--extract-audio \
--audio-format mp3 \
--audio-quality 0 \
--socket-timeout "$CONNECTION_TIMEOUT" \
--retries "$MAX_RETRIES" \
--no-continue \
--output "$temp_dir/%(id)s.audio.%(ext)s" \
"https://www.youtube.com/watch?v=$video_id" \
2>>"$LOG_FILE"
audio_status=$?
if [[ $video_status -eq 0 && $audio_status -eq 0 ]]; then
if process_downloaded_video "$video_id" "$title" "$temp_dir"; then
log_message "[$current/$video_count] Successfully processed '$title'"
[[ $current -lt $video_count ]] && wait_for_next
else
log_message "[$current/$video_count] ERROR: Processing failed for '$title'"
fi
else
log_message "[$current/$video_count] ERROR: Download failed for '$title' (video: $video_status, audio: $audio_status)"
fi
done < "$video_list"
log_message "Playlist download completed"
return 0
}
main() {
local start_time end_time duration formatted_duration
start_time=$(date +%s)
local playlist_url="${1:-${PLAYLIST_URL:-}}"
if [[ -z "$playlist_url" ]]; then
echo "ERROR: No playlist URL provided and PLAYLIST_URL is not set in the config"
exit 1
fi
estimate_download_time "$playlist_url"
download_playlist "$playlist_url"
local result=$?
show_log
end_time=$(date +%s)
duration=$((end_time - start_time))
formatted_duration=$(format_time "$duration")
log_message "Total execution time: $formatted_duration"
log_message "Full downloads database saved to: $DOWNLOADS_LOG"
return $result
}
main "$@"