Automate downloading of Youtube Channels with YT-DLP and PowerShell

Watching or listening to Youtube playlists can be very frustrating with all the ads that are being thrown at you and this seems to have gotten worse over the years. A way around this is to download your favourite Youtube playlists. You can import these into your media-server, like Emby or Jellyfin, and watch the downloaded videos on which ever screen you have setup with this server.

You can add as many playlists and channels for auto-download as you’d like. Add it to a Scheduler or Cron job to keep your local copies up to date (The script won’t redownload the already downloaded files, it checks). The size of your disk is the limit.

Anyway, this PowerShell script automates the downloading of YouTube videos from a list of subscriptions. Before running, ensure yt-dlp is installed in the specified directory. Start by editing the script to set your desired download, log, and yt-dlp directories. Populate your subscription file with YouTube URLs—each line can contain a channel, playlist, or video link. Execute the script in PowerShell; it will update yt-dlp, then download videos into organized folders based on the content type, maintaining a log for tracking. Videos are downloaded at up to 1080p resolution, with metadata and subtitles embedded.

Note: This script uses the cookies from Google Chrome to authenticate to Youtube. Make sure you’ve logged into Youtube recently with Chrome for this to work. If you’re using another browser, you can modify the script below by exchanging the word “chrome” for the name of your browser of choice. (don’t use the quotes when changing the browsername in the script.)

1. Prerequisites

Install Required Software

  • PowerShell: Ensure you have PowerShell available on your system. It’s included in any recent Windows OS by default, but it’s best to download an up to date version from the official repo.
  • yt-dlp: From the official repo, download and place yt-dlp.exe in a directory of your choice. This script assumes it’s located in D:\YT-DLP, but you can point the variable at where ever yt-dlp is located.
  • FFmpeg: Install FFmpeg and ensure it’s added to your system’s PATH. This is required for merging video and audio streams and the script will exit with an error if it’s not installed.

Familiarize Yourself with the Script’s Variables

Change the variables below to match your own environment. Directory paths are in quotes.

2. Running the Script

Open PowerShell

  • Open PowerShell as an administrator. This is often necessary for scripts that create or modify files and directories.

Set Execution Policy (If Needed)

PowerShell’s execution policy might prevent you from running scripts. Open the PowerShell console and set it to allow script execution: Set-ExecutionPolicy RemoteSigned

Choose Y when prompted to change the policy.

Navigate to the Script Location

  • Use the cd command to navigate to the directory containing your script.

Run the Script

  • Execute the script by typing its name or using .\ followed by the script name, assuming you’re in the script’s directory: .\your-script-name.ps1

3. Script Behavior and Outputs

Directories Setup

  • The script checks if the specified directories for YouTube downloads, downloader files, and logs exist. If they don’t, it creates them.

Software Checks

  • It verifies the presence of yt-dlp.exe in the designated directory and checks if FFmpeg is installed and available in the system path. It will also auto-check for newer releases of yt-dlp.exe and update if needed

Logging

  • Messages, such as updates and errors, are logged to a file and printed to screen for real-time feedback.

Download Process

  • The script reads URLs from the $subscriptionFile and attempts to download videos from each channel, organizing them into directories named after the channel and origin names. The origins currently supported, are:
    – videos (saved in ChannelName\Season 0)
    – podcasts (saved in ChannelName\Podcasts)
    – streams (saved in ChannelName\Streams)
    – shorts (saved in ChannelName\Shorts)
    – releases (saved in ChannelName\Releases)
    – playlists (saved in ChannelName\PlaylistName)
  • It handles incremental downloads, ensuring that only new videos are added to the playlists.

# Set UTF-8 encoding
$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# Variables
$DebugLogging = 1

# Set necessary directories
$youtubeDirectory = "C:\Youtube".TrimEnd('\')
$downloaderDirectory = "C:\Scripts".TrimEnd('\')
$logFileDirectory = "$downloaderDirectory\Logs".TrimEnd('\')
$subscriptionFile = "$downloaderDirectory\Subscriptions.txt"

# Ensure log and YouTube directories exist and if not exist, then create
New-Item -ItemType Directory -Force -Path $logFileDirectory, $youtubeDirectory | Out-Null

# Log file path
$logFilePath = Join-Path $logFileDirectory "download_log.txt"

# Check if yt-dlp.exe is present
if (-not (Test-Path "$downloaderDirectory\yt-dlp.exe")) {
    Write-Host "YT-DLP is not present at the indicated location. Please change the downloader directory or copy yt-dlp to $downloaderDirectory!" -ForegroundColor Red
    exit
}

# Check if ffmpeg is installed and added to the system path
try {
    & ffmpeg -version | Out-Null
} catch {
    Write-Host "FFMPEG is not installed or not added to the System Path. Ffmpeg is needed to merge downloaded high-quality audio and video streams. Please install FFmpeg!" -ForegroundColor Red
    exit
}

# Function to log messages
function Log-Message {
    param ([string]$message)
    $timeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$($timeStamp): $message"
    Add-Content -Path $logFilePath -Value $logMessage
    Write-Output $logMessage
}

# Update yt-dlp
Log-Message "Updating yt-dlp..."
& "$downloaderDirectory\yt-dlp.exe" -U | Out-Null

# Function to extract and sanitize the channel name from a URL
function Extract-ChannelName {
    param ([string]$url)
    $channelName = $null

    if ($url -match "/user/([^/?&]+)") {
        $channelName = $Matches[1]
    } elseif ($url -match "/channel/([^/?&]+)") {
        $channelName = $Matches[1]
    } elseif ($url -match "/c/([^/?&]+)") {
        $channelName = $Matches[1]
    } elseif ($url -match "/@([^/?&]+)") {
        $channelName = $Matches[1]
    }

    return $channelName -replace "[^\w\d\s]", "" -replace "  ", " " -replace "   ", " " -replace "    ", " "
}

# Function to download content based on URL type
function Download-Content {
    param (
        [string]$channelUrl
    )
    
    $channelName = Extract-ChannelName $channelUrl
    if (-not $channelName) {
        Log-Message "Unable to extract channel name from URL: $channelUrl"
        return
    }

    $channelDirectory = Join-Path $youtubeDirectory $channelName
    New-Item -ItemType Directory -Force -Path $channelDirectory | Out-Null

    switch -Regex ($channelUrl) {
        "videos$" {
            $targetDirectory = Join-Path $channelDirectory "Season 0"
            $logMessage = "Downloading all videos to Season 0 for channel: $channelName"
        }
        "podcasts$" {
            $targetDirectory = Join-Path $channelDirectory "Podcasts"
            $logMessage = "Downloading all podcasts to Podcasts folder for channel: $channelName"
        }
        "streams$" {
            $targetDirectory = Join-Path $channelDirectory "Streams"
            $logMessage = "Downloading all streams to Streams folder for channel: $channelName"
        }
        "shorts$" {
            $targetDirectory = Join-Path $channelDirectory "Shorts"
            $logMessage = "Downloading all shorts to Shorts folder for channel: $channelName"
        }
        "releases$" {
            $targetDirectory = Join-Path $channelDirectory "Releases"
            $logMessage = "Downloading all releases to Releases folder for channel: $channelName"
        }
        "playlists$" {
            # Explicitly handle playlists
            Download-ChannelPlaylists -channelUrl $channelUrl
            return
        }
        default {
            Log-Message "URL does not match any known patterns. Please check the URL: $channelUrl"
            return
        }
    }

    New-Item -ItemType Directory -Force -Path $targetDirectory | Out-Null
    Log-Message $logMessage

    $outputTemplate = Join-Path -Path $targetDirectory -ChildPath "%(title)s.%(ext)s"
    & "$downloaderDirectory\yt-dlp.exe" --cookies-from-browser chrome --download-archive "$targetDirectory\archive.txt" --windows-filenames --embed-thumbnail --add-metadata --embed-chapters --merge-output-format mkv --remux-video mkv -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" -o $outputTemplate --write-sub --sub-langs "en,es,nl" --progress "$channelUrl"
}

# Function to download playlists for a given channel with incremental numbering
function Download-ChannelPlaylists {
    param (
        [string]$channelUrl
    )
    
    $channelName = Extract-ChannelName $channelUrl
    if (-not $channelName) {
        Log-Message "Unable to extract channel name from URL: $channelUrl"
        return
    }

    $channelDirectory = Join-Path $youtubeDirectory $channelName
    New-Item -ItemType Directory -Force -Path $channelDirectory | Out-Null

    Log-Message "Retrieving playlists for channel: $channelName"
    
    $playlistsInfo = & "$downloaderDirectory\yt-dlp.exe" --cookies-from-browser chrome --flat-playlist --dump-single-json --extractor-args "youtubetab:skip=authcheck" "$channelUrl" 2>&1
    $playlists = $playlistsInfo | ConvertFrom-Json -ErrorAction SilentlyContinue

    if ($playlists -and $playlists.entries) {
        foreach ($playlist in $playlists.entries) {
            $playlistName = $playlist.title -replace "[^\w\d\s]", "" -replace "  ", " " -replace "   ", " " -replace "    ", " "
            $playlistDirectory = Join-Path $channelDirectory $playlistName
            New-Item -ItemType Directory -Force -Path $playlistDirectory | Out-Null

            Log-Message "Starting download for playlist: $playlistName"

            # Incremental numbering logic
            $existingFiles = Get-ChildItem -Path $playlistDirectory -File
            $maxNumber = 0
            foreach ($file in $existingFiles) {
                if ($file.Name -match '^\d{3}') {
                    $number = [int]$file.Name.Substring(0, 3)
                    if ($number -gt $maxNumber) {
                        $maxNumber = $number
                    }
                }
            }

            $playlistVideos = & "$downloaderDirectory\yt-dlp.exe" --cookies-from-browser chrome --flat-playlist --get-id $playlist.url
            foreach ($videoId in $playlistVideos) {
                $maxNumber++
                $filePrefix = "{0:D3} " -f $maxNumber  # Adjusted for 3-digit numbering
                $outputTemplate = Join-Path -Path $playlistDirectory -ChildPath ($filePrefix + "%(title)s.%(ext)s")
                $archivePath = Join-Path -Path $playlistDirectory -ChildPath "archive.txt"
                
                & "$downloaderDirectory\yt-dlp.exe" --cookies-from-browser chrome --download-archive "$archivePath" --windows-filenames --embed-thumbnail --add-metadata --embed-chapters --merge-output-format mkv --remux-video mkv -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" -o $outputTemplate --write-sub --sub-langs "en,es,nl" --progress "https://www.youtube.com/watch?v=$videoId"
                
                Log-Message "Downloaded video with ID: $videoId to $outputTemplate"
            }

            Log-Message "Completed download for playlist: $playlistName"
        }
    } else {
        Log-Message "No valid playlist entries found for channel: $channelName"
    }
}

# Reading YouTube channel URLs from the subscription file
$Channels = Get-Content -Path $subscriptionFile
foreach ($Channel in $Channels) {
    Download-Content -channelUrl $Channel
}

# Test if Archive.txt exists
if (Test-Path "$targetDirectory\archive.txt") {
    Write-Host "Archive Exists - Adding new content Only..." -ForegroundColor Green
}

# Delete partial downloaded files
Remove-Item "$youtubeDirectory\*.f???.webm" -Recurse -Force
Remove-Item "$youtubeDirectory\*.temp.mkv" -Recurse -Force

# Removing empty folders
Get-ChildItem "$youtubeDirectory\*." -Directory | Where-Object { $_.GetFileSystemInfos().Count -eq 0 } | Remove-Item -Force

Log-Message "Script completed!"

Note: You’ll notice a folder “Season 0” is automatically created. This is for use with Emby. Emby does some strange things when displaying a folder when a library is selected as TV, but there is no folder named Season. This folder is now used to save separate videos not part of a playlist.

Enjoy!

Reader Comments

Leave a Reply

Your email address will not be published. Required fields are marked *