377 lines
11 KiB
Bash
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 "$@"
|
|
|