Files
youtube-sync-docker/sync.sh
ComputerGuru ef903c86d1 fix: wire settings.json to actually drive runtime behavior
Settings page saved to /config/settings.json but nothing downstream read
that file. Schedule changes were silently ignored; max_quality and
sleep_interval changes were silently ignored. The "Settings saved
successfully" flash was a lie.

Fix:
- sync.sh reads max_quality + sleep_interval from settings.json on each
  run (jq -er ... // empty, falling back to env vars on missing/malformed
  file)
- entrypoint.sh reads sync_schedule from settings.json before setting up
  cron, and writes the crond PID to /var/run/crond.pid so Flask can
  SIGHUP it
- app.py adds apply_schedule(): rewrites /etc/crontabs/root, signals
  crond via the recorded PID, restarts crond if the PID is stale, drops
  the crontab when schedule is set to "manual". save_settings_route
  invokes it only when the schedule actually changed; any failure
  flashes a warning so the save still succeeds with the user informed
- bare `except: pass` in get_settings replaced with explicit exception
  types + stderr warning so debugging malformed settings is possible
- sync.sh: one bad channel no longer aborts the whole loop under set -e
- Dockerfile adds jq for the JSON reads in sync.sh / entrypoint.sh
- README: two stale github.com URLs fixed to Gitea; new Running Tests
  section under Building From Source
- tests/test_settings.py: 3 pytest cases covering get_settings()'s
  three branches (missing file, valid file, malformed JSON)

Settings hierarchy unchanged: env-var defaults seed the UI; settings.json
wins when present and parseable.

Timezone (TZ) is not applied live - tzdata is locked in at process start.
Same behavior as before; not in scope for this commit.
2026-05-31 19:21:43 -07:00

119 lines
4.1 KiB
Bash

#!/bin/bash
# YouTube Channel Sync Script
# Reads channel configuration from /config/channels.txt
set -e
DOWNLOAD_DIR="${DOWNLOAD_DIR:-/downloads}"
CONFIG_DIR="${CONFIG_DIR:-/config}"
COOKIES_FILE="${CONFIG_DIR}/cookies.txt"
CHANNELS_FILE="${CONFIG_DIR}/channels.txt"
SETTINGS_FILE="${CONFIG_DIR}/settings.json"
MAX_QUALITY="${MAX_QUALITY:-1080}"
SLEEP_INTERVAL="${SLEEP_INTERVAL:-2}"
# Override env-var defaults with values from settings.json when present and parseable.
# Missing or malformed settings.json silently falls back to env-var defaults.
if [ -f "$SETTINGS_FILE" ]; then
if VAL=$(jq -er '.max_quality // empty' "$SETTINGS_FILE" 2>/dev/null); then
MAX_QUALITY="$VAL"
fi
if VAL=$(jq -er '.sleep_interval // empty' "$SETTINGS_FILE" 2>/dev/null); then
SLEEP_INTERVAL="$VAL"
fi
fi
# Check if channels file exists
if [ ! -f "$CHANNELS_FILE" ]; then
echo "[ERROR] Channels file not found: $CHANNELS_FILE"
echo "[INFO] Creating example channels.txt file..."
cat > "$CHANNELS_FILE" << 'EOF'
# YouTube Channel Configuration
# Format: CHANNEL_ID|Channel Name
# One channel per line. Lines starting with # are ignored.
#
# Example:
# UCfDNi1aEljAQ17mUrfUjkvg|Alton Brown
# UCoq2qlWgvvKJzW_hBkLIE8w|Flavour Trip
EOF
echo "[INFO] Please edit $CHANNELS_FILE and add your channels"
exit 1
fi
# Check if cookies file exists
COOKIES_PARAM=""
if [ -f "$COOKIES_FILE" ]; then
echo "[INFO] Using cookies from: $COOKIES_FILE"
COOKIES_PARAM="--cookies $COOKIES_FILE"
else
echo "[WARNING] No cookies file found at: $COOKIES_FILE"
echo "[WARNING] Some videos may fail to download without authentication"
fi
# Parse and sync channels
echo "=========================================="
echo "YouTube Channel Sync - $(date)"
echo "=========================================="
while IFS='|' read -r CHANNEL_ID CHANNEL_NAME || [ -n "$CHANNEL_ID" ]; do
# Skip empty lines and comments
[[ -z "$CHANNEL_ID" || "$CHANNEL_ID" =~ ^[[:space:]]*# ]] && continue
# Trim whitespace
CHANNEL_ID=$(echo "$CHANNEL_ID" | xargs)
CHANNEL_NAME=$(echo "$CHANNEL_NAME" | xargs)
echo ""
echo "=========================================="
echo "Syncing: $CHANNEL_NAME"
echo "Channel ID: $CHANNEL_ID"
echo "=========================================="
# Don't let one bad channel kill the whole loop. yt-dlp can exit non-zero
# for individual videos even with --ignore-errors (e.g. age-gated, geo-blocked,
# member-only). We log and continue rather than aborting the entire sync.
yt-dlp \
$COOKIES_PARAM \
--format "bestvideo[ext=mp4][height<=${MAX_QUALITY}]+bestaudio[ext=m4a]/best[ext=mp4]/best" \
--merge-output-format mp4 \
--output "$DOWNLOAD_DIR/$CHANNEL_NAME/Season %(upload_date>%Y)s/S%(upload_date>%Y)sE%(playlist_index)03d - %(title).100B - [%(id)s].%(ext)s" \
--write-info-json \
--embed-thumbnail \
--embed-metadata \
--write-subs \
--sub-langs "en.*" \
--embed-subs \
--download-archive "$DOWNLOAD_DIR/$CHANNEL_NAME/.downloaded.txt" \
--no-overwrites \
--ignore-errors \
--sleep-interval "$SLEEP_INTERVAL" \
--max-sleep-interval 5 \
"https://www.youtube.com/channel/$CHANNEL_ID/videos" \
|| echo "[WARNING] channel $CHANNEL_NAME ($CHANNEL_ID) failed; continuing with remaining channels"
# Create tvshow.nfo for Emby/Plex
echo ""
echo "Creating tvshow.nfo for $CHANNEL_NAME..."
cat > "$DOWNLOAD_DIR/$CHANNEL_NAME/tvshow.nfo" << EOFNFO
<?xml version='1.0' encoding='utf-8'?>
<tvshow>
<title>$CHANNEL_NAME</title>
<showtitle>$CHANNEL_NAME</showtitle>
<plot>YouTube channel: $CHANNEL_NAME</plot>
<genre>YouTube</genre>
<studio>YouTube</studio>
<premiered></premiered>
<uniqueid type="youtube" default="true">$CHANNEL_ID</uniqueid>
</tvshow>
EOFNFO
echo "Done with $CHANNEL_NAME"
echo ""
done < "$CHANNELS_FILE"
echo "=========================================="
echo "All channels synced successfully!"
echo "Completed: $(date)"
echo "=========================================="