Skill: easonc13/m3u8-downloader (version 1.0.0)

--- name: m3u8-downloader description: Download encrypted m3u8/HLS videos using parallel downloads. Use when given an m3u8 URL to download a video, especially encrypted HLS streams with AES-128. --- # M3U8 Video Downloader Download HLS/m3u8 videos with parallel segment downloads and automatic decryption. ## Prerequisites - `aria2c` (install: `brew install aria2`) - `ffmpeg` (install: `brew install ffmpeg`) ## Full Workflow (From Webpage to MP4) ### Step 1: Extract m3u8 URL from webpage If given a webpage URL (not a direct m3u8), use browser automation to find the stream URL: ```javascript // In browser console or via browser tool evaluate (() => { // Check HLS.js player instance if (window.hls && window.hls.url) return window.hls.url; if (window.player && window.player.hls && window.player.hls.url) return window.player.hls.url; // Search window objects for m3u8 URLs const allVars = Object.keys(window).filter(k => { try { return window[k] && typeof window[k] === 'object' && window[k].url && window[k].url.includes('m3u8'); } catch(e) { return false; } }); return allVars.length > 0 ? allVars.map(k => window[k].url) : 'not found'; })() ``` Use `profile=openclaw` (isolated browser) to avoid browser history. ### Step 2: Handle Master Playlist (Multi-Quality) Master playlists list quality variants, not segments: ```bash curl -s "https://example.com/playlist.m3u8" # Output example: # #EXT-X-STREAM-INF:BANDWIDTH=8247061,RESOLUTION=1920x1080 # 1080p/video.m3u8 # #EXT-X-STREAM-INF:BANDWIDTH=4738061,RESOLUTION=1280x720 # 720p/video.m3u8 ``` Pick the highest quality (e.g., 1080p) and fetch that sub-playlist: ```bash BASE_URL="https://example.com" curl -s "${BASE_URL}/1080p/video.m3u8" ``` ### Step 3: Extract Segment URLs Segments may have non-standard extensions (e.g., `.jpeg` instead of `.ts`): ```bash mkdir -p /tmp/video_download && cd /tmp/video_download BASE_URL="https://example.com/1080p" curl -s "${BASE_URL}/video.m3u8" | grep -E "^[^#]" | while read seg; do echo "${BASE_URL}/${seg}" done > urls.txt # Count segments wc -l urls.txt ``` ### Step 4: Parallel Download with aria2c ```bash aria2c -i urls.txt -j 16 -x 16 -s 16 --file-allocation=none -c true \ --console-log-level=warn --summary-interval=30 ``` - `-j 16`: 16 concurrent downloads - `-x 16`: 16 connections per file - `-c true`: continue partial downloads ### Step 5: Merge with ffmpeg ```bash # Get segment count NUM_SEGMENTS=$(wc -l < urls.txt) # Generate file list (adjust filename pattern as needed) for i in $(seq 0 $((NUM_SEGMENTS-1))); do echo "file 'video${i}.jpeg'" # or video${i}.ts done > filelist.txt # Merge (copy streams, no re-encoding) ffmpeg -y -f concat -safe 0 -i filelist.txt -c copy ~/Downloads/output.mp4 ``` ### Step 6: Cleanup ```bash rm -rf /tmp/video_download ``` ## Quick Script Usage ```bash ~/clawd/skills/m3u8-downloader/scripts/download.sh "https://example.com/video.m3u8" "output_name" ``` Note: The script may not handle all edge cases (master playlists, non-standard extensions). Use manual process above for complex streams. ## Handling Encrypted Streams (AES-128) Look for `#EXT-X-KEY:METHOD=AES-128,URI="enc.key"` in the playlist: ```bash curl -s "https://example.com/path/enc.key" -o enc.key ffmpeg -allowed_extensions ALL -i local_playlist.m3u8 -c copy output.mp4 ``` ## Output Final video saved as `~/Downloads/<output_name>.mp4`

path: scripts/download.sh
content: #!/bin/bash
# M3U8 Video Downloader
# Usage: ./download.sh <m3u8_url> [output_name]

set -e

M3U8_URL="$1"
OUTPUT_NAME="${2:-video_$(date +%Y%m%d_%H%M%S)}"
WORK_DIR="$HOME/Downloads/m3u8_${OUTPUT_NAME}"
OUTPUT_FILE="$HOME/Downloads/${OUTPUT_NAME}.mp4"

if [ -z "$M3U8_URL" ]; then
    echo "Usage: $0 <m3u8_url> [output_name]"
    exit 1
fi

# Check dependencies
command -v aria2c >/dev/null 2>&1 || { echo "aria2c required. Install: brew install aria2"; exit 1; }
command -v ffmpeg >/dev/null 2>&1 || { echo "ffmpeg required. Install: brew install ffmpeg"; exit 1; }

echo "🎬 M3U8 Downloader"
echo "URL: $M3U8_URL"
echo "Output: $OUTPUT_FILE"
echo ""

mkdir -p "$WORK_DIR"
cd "$WORK_DIR"

# Step 1: Fetch master playlist
echo "📋 Fetching playlist..."
MASTER=$(curl -sL "$M3U8_URL")
BASE_URL=$(dirname "$M3U8_URL")

# Check if it's a master playlist pointing to another m3u8
NESTED=$(echo "$MASTER" | grep -E "\.m3u8$" | head -1)
if [ -n "$NESTED" ]; then
    if [[ "$NESTED" == /* ]]; then
        # Absolute path
        PROTOCOL=$(echo "$M3U8_URL" | grep -oE "^https?://[^/]+")
        PLAYLIST_URL="${PROTOCOL}${NESTED}"
    elif [[ "$NESTED" == http* ]]; then
        PLAYLIST_URL="$NESTED"
    else
        PLAYLIST_URL="${BASE_URL}/${NESTED}"
    fi
    echo "Found nested playlist: $PLAYLIST_URL"
    PLAYLIST=$(curl -sL "$PLAYLIST_URL")
    BASE_URL=$(dirname "$PLAYLIST_URL")
else
    PLAYLIST="$MASTER"
    PLAYLIST_URL="$M3U8_URL"
fi

echo "$PLAYLIST" > playlist.m3u8

# Step 2: Get encryption key if present
KEY_LINE=$(echo "$PLAYLIST" | grep "EXT-X-KEY" | head -1)
if [ -n "$KEY_LINE" ]; then
    KEY_URI=$(echo "$KEY_LINE" | grep -oE 'URI="[^"]+"' | sed 's/URI="//;s/"$//')
    if [ -n "$KEY_URI" ]; then
        if [[ "$KEY_URI" == http* ]]; then
            KEY_URL="$KEY_URI"
        else
            KEY_URL="${BASE_URL}/${KEY_URI}"
        fi
        echo "🔑 Downloading encryption key from: $KEY_URL"
        curl -sL "$KEY_URL" -o enc.key
    fi
fi

# Step 3: Extract segment URLs
echo "📦 Extracting segment URLs..."
echo "$PLAYLIST" | grep -E "^https?://" > urls.txt || true

# If segments are relative paths
if [ ! -s urls.txt ]; then
    echo "$PLAYLIST" | grep -E "\.ts$" | while read -r line; do
        if [[ "$line" == /* ]]; then
            PROTOCOL=$(echo "$PLAYLIST_URL" | grep -oE "^https?://[^/]+")
            echo "${PROTOCOL}${line}"
        elif [[ "$line" == http* ]]; then
            echo "$line"
        else
            echo "${BASE_URL}/${line}"
        fi
    done > urls.txt
fi

TOTAL=$(wc -l < urls.txt | tr -d ' ')
echo "Found $TOTAL segments"

# Step 4: Parallel download
echo "⬇️  Downloading segments (16 threads)..."
aria2c -i urls.txt -j 16 -x 16 -s 16 --file-allocation=none --auto-file-renaming=false -c true --console-log-level=warn

# Step 5: Verify downloads
DOWNLOADED=$(ls *.ts 2>/dev/null | wc -l | tr -d ' ')
echo "Downloaded: $DOWNLOADED / $TOTAL segments"

if [ "$DOWNLOADED" -lt "$TOTAL" ]; then
    echo "⚠️  Warning: Some segments missing. Retrying..."
    aria2c -i urls.txt -j 16 -x 16 -s 16 --file-allocation=none --auto-file-renaming=false -c true --console-log-level=warn
fi

# Step 6: Merge with ffmpeg
echo "🔧 Merging segments..."

# Create local m3u8 pointing to downloaded files
if [ -f enc.key ]; then
    # Encrypted - need to create local playlist with key reference
    sed "s|URI=\"[^\"]*\"|URI=\"enc.key\"|g" playlist.m3u8 > local_playlist.m3u8
    # Replace remote URLs with local filenames
    for ts in *.ts; do
        sed -i '' "s|.*/${ts}|${ts}|g" local_playlist.m3u8 2>/dev/null || \
        sed -i "s|.*/${ts}|${ts}|g" local_playlist.m3u8
    done
    ffmpeg -y -allowed_extensions ALL -i local_playlist.m3u8 -c copy -bsf:a aac_adtstoasc "$OUTPUT_FILE"
else
    # No encryption - simple concat
    ls -v *.ts | sed "s/^/file '/" | sed "s/$/'/" > filelist.txt
    ffmpeg -y -f concat -safe 0 -i filelist.txt -c copy -bsf:a aac_adtstoasc "$OUTPUT_FILE"
fi

# Cleanup
echo "🧹 Cleaning up..."
rm -rf "$WORK_DIR"

echo ""
echo "✅ Done! Video saved to: $OUTPUT_FILE"
ls -lh "$OUTPUT_FILE"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)