yt-dlp: Download YouTube Channel By Playlist

I wanted to download some YouTube videos to re-host for myself on Plex so that I could use the downloads feature to watch them offline, using the yt-dlp tool (a fork of the original yt-dl tool with additional features). Another advantage of this is that it eliminates ads. One issue though is that you lose some of the discovery and organization features on YouTube. In order to try to keep the videos organized, I wanted to preserve playlist info and video metadata. Here’s the script I used to do this. Note that ffmpeg also needs to be in the same folder, or added to the system path.

.\yt-dlp.exe "" -S res:1080,ext:mp4:m4a -o "D:\Plex\YouTube\%(uploader)s\%(playlist_title)s\%(playlist_index)s %(title)s [%(id)s].%(ext)s" --add-metadata --compat-options embed-metadata --download-archive "D:\archive.txt" --match-filter "!is_live"

Here’s a quick breakdown of what it’s doing:

.\yt-dlp.exe “” Run yt-dlp in a Powershell terminal and provide the URL of the playlists page for the channel you want to download.
-S res:1080,ext:mp4:m4a Specify the resolution and format to download.

There’s a ton of potential options here. “-S best” will get the max quality video, though in my case I wanted to limit file size. For some downloads I use 720 as the resolution.

-o “D:\Plex\YouTube\%(uploader)s\%(playlist_title)s\%(playlist_index)s %(title)s [%(id)s].%(ext)s” Specify both the output path and file name format.

In my case I’m outputting to a network drive in a Plex library named “YouTube.” I’m then creating a folder for each channel and a sub-folder for each playlist.

The %(title)s [%(id)s].%(ext)s file name format is close to the default, but I’m prefixing it with the %(playlist_index)s to make sorting easy. This may not be necessary depending on the video name or type of playlist.

–add-metadata –compat-options embed-metadata Embeds YouTube video metadata in the video. The compatibility options make it follow the default for the classic yt-dl program rather than yt-dlp.
–download-archive “D:\archive.txt” Logs the ID of each video downloaded to a text file to avoid repeats.
–match-filter “!is_live” Skips any videos in the playlist that are live streams.

Several of the channels I wanted to download had live streams that were just regurgitating other videos in the playlist in a livestream format. I didn’t need to download these, but since they were part of the playlist, yt-dlp would start capturing the stream and it would block the rest of the downloads. This avoided that problem.

Once I downloaded all the playlists, I wanted to capture anything that wasn’t specifically sorted into a playlist. I used this command:

.\yt-dlp.exe "" -S res:1080,ext:mp4:m4a -o "P:\Plex\YouTube\%(uploader)s\Uploads\%(playlist_index)s %(title)s [%(id)s].%(ext)s" --add-metadata --compat-options embed-metadata --download-archive "D:\archive.txt" --match-filter "!is_live"

This placed the rest of the videos into a playlist sub-folder just labeled “uploads”, and captures everything on the channels videos page, rather than playlists. This includes all channel videos, but by using the download-archive command anything that was already downloaded from a playlist is excluded.

Just to document a couple other yt-dlp parameters I’ve used, not specifically to get videos by playlist:

With ffmpeg transcoding to h.265 (very slow):

--exec "ffmpeg -i {} -c:v libx265 -crf 28 -c:a copy {}.mp4"

To only save part of a video, add this to ffmpeg processing:

-ss 0 -t 120

This approach also assumes videos are only assigned to one playlist, if that were not the case the resulting playlist folder structure may not be very useful.

To get Plex to use the embedded video metadata, the Personal Media Shows agent needs to be selected for the YouTube library:


Full documentation for yt-dlp is available on GitHub.

Also note that when running large downloads, YouTube may throttle the downloads.

After downloading I noticed these videos seemed to be taking up more space on my Synology drive than expected. After some investigation, I realized it’s because the yt-dlp tool downloads many temporary .part files and then stitches them into the final video. Synology was moving each deleted part file to the recycle bin, which was effectively doubling the space used on the Synology drive. After clearing the recycle bins (log in to Synology web interface > Control Panel > Shared Folder > Action > Empty All Recycle Bins), I reclaimed the extra space. I could also download to another computer and then copy over to the NAS, but that would have required a lot of free drive space on another computer.

Mount Synology NAS to Raspberry Pi Using NFS

Here’s the equipment I used, this should work similarly on other software versions:

  • Synology NAS Running DSM 7.0
  • Raspberry Pi running Raspbian 10 (Buster)

This tutorial is based on these instructions from Synology, with modifications because they didn’t work exactly as listed:

Enable NFS on the Synology NAS:

  1. From the Synology web interface, Go to Control Panel > File Services > NFS
  2. Check Enable NFS Service. Set Maximum NFS Protocol to NFSv4.1 and click Apply.

Create NFS Permissions:

  1. Still in the Synology web interface, go to Control Panel > Shared Folder
  2. Select the folder you want to share and click Edit
  3. Click NFS Permissions > Create
    1. Enter a hostname or IP for the Raspberry Pi that will be accessing the folder.
    2. Leave Privilege on Read / Write and leave other settings at default, and click Save.
  4. Note the Mount path at the bottom of the folder edit page, then click Save again to save the folder settings.



Mount share on the Raspberry Pi:

  1. From the terminal – install nfs if not already installed:
    1. sudo apt update
    2. sudo apt install nfs-common
  2. Create the mount directory – change the path as appropriate:
    1. sudo mkdir /mnt/share
  3. Here’s where Synology instructions didn’t work. I kept getting a timeout error when trying to mount the share. This answer helped me get on the right track.
    1. To do a one time mount to ensure everything is working – replace IP address/ hostname and paths are appropriate:
      1. sudo mount -t nfs4 -o proto=tcp,port=2049 192.168.XX.XX:/volume1/share /mnt/share
    2. To mount so it persists after a reboot (thanks to this article):
      1. sudo nano /etc/fstab
      2. Add this line to the bottom, again replacing ip addresses / hostname and file paths as needed:
        1. 192.168.XX.XX:/volume1/share /mnt/share nfs defaults 0 0
      3. Exit, save the file, and reboot the pi (sudo reboot now).
      4. Once the Pi is back up, ensure the mount worked with ls /mnt/share

PowerShell Terminal: Menu Template

I created this for part of a bigger project and figured it’d make a good generic template for any PowerShell script needing a basic menu using the up and down arrow keys and enter key to make a selection.  For example data it lists the items in the root of the C: drive, but can easily be supplied any array of strings to list out in the menu. The chosen item is stored as a string in the $selection variable to be used later in the script.

<# Title: PowerShell Menu Template | Author: Michael Casey | Date: July 3rd, 2019 | Website: #>

#Sample list data to populate menu:
#Set $List to any array to populate the menu with custom options
$List = Get-ChildItem -Path C:\ -Name

#menu offset to allow space to write a message above the menu
$xmin = 3
$ymin = 3

#Write Menu
Write-Host ""
Write-Host "  Use the up / down arrow to navigate and Enter to make a selection"
[Console]::SetCursorPosition(0, $ymin)
foreach ($name in $List) {
    for ($i = 0; $i -lt $xmin; $i++) {
        Write-Host " " -NoNewline
    Write-Host "   " + $name

#Highlights the selected line
function Write-Highlighted {

    [Console]::SetCursorPosition(1 + $xmin, $cursorY + $ymin)
    Write-Host ">" -BackgroundColor Yellow -ForegroundColor Black -NoNewline
    Write-Host " " + $List[$cursorY] -BackgroundColor Yellow -ForegroundColor Black
    [Console]::SetCursorPosition(0, $cursorY + $ymin)     

#Undoes highlight
function Write-Normal {
    [Console]::SetCursorPosition(1 + $xmin, $cursorY + $ymin)
    Write-Host "  " + $List[$cursorY]  

#highlight first item by default
$cursorY = 0

$selection = ""
$menu_active = $true
while ($menu_active) {
    if ([console]::KeyAvailable) {
        $x = $Host.UI.RawUI.ReadKey()
        [Console]::SetCursorPosition(1, $cursorY)
        switch ($x.VirtualKeyCode) { 
            38 {
                #down key
                if ($cursorY -gt 0) {
                    $cursorY = $cursorY - 1

            40 {
                #up key
                if ($cursorY -lt $List.Length - 1) {
                    $cursorY = $cursorY + 1
            13 {
                #enter key
                $selection = $List[$cursorY]
                $menu_active = $false
    Start-Sleep -Milliseconds 5 #Prevents CPU usage from spiking while looping

Write-Host $selection
#May use switch statement here to process menu selection

Screenshot of the PowerShell menu template script running

Schedule Windows Notifications with PowerShell (Windows Task Scheduler / Toast Notifications)

I’ve found too many times that I can sit in front of a computer screen for several hours without taking a break but that later in the day the eye strain catches up with me. I wanted something to remind me to take a quick break every 20 minutes, following the 20-20-20 rule. There’s a lot of programs that do something like this. Workrave may be the most popular, but I’ve had a few problems with it and decided I could probably do something simpler with a scheduled PowerShell script.


First off, I need the notification. There’s a handful of blogs on how to make a toast notification in PowerShell. There’s a PowerShell module for it, but I’d rather not have to install an external module. From the creator of that module, here’s a working example of creating a toast notification in PowerShell:

$app = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
$Template = [Windows.UI.Notifications.ToastTemplateType]::ToastImageAndText01
#Gets the Template XML so we can manipulate the values
[xml]$ToastTemplate = ([Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent($Template).GetXml())
[xml]$ToastTemplate = @"
<toast launch="app-defined-string">
<binding template="ToastGeneric">
<text>DNS Alert…</text>
<text>We noticed that you are near Wasaki. Thomas left a 5 star rating after his last visit, do you want to try it?</text>
<action activationType="background" content="Remind me later" arguments="later"/>
$ToastXml = New-Object TypeName Windows.Data.Xml.Dom.XmlDocument
$notify = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app)

I’m just going to copy what I need and modify and condense it. For more reading on toast notifications in PowerShell this blog article and this one provide more detail, but be sure to note that since they were published, it’s become necessary to use a real app id to get the notification to show – details here.

Here’s the selection of code I’m using for the notification.


$app = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe'
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]

$Template = [Windows.UI.Notifications.ToastTemplateType]::ToastImageAndText01

$ToastTemplate = ([Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent($Template).GetXml())

$ToastTemplate = @"
<toast launch="app-defined-string">
    <binding template="ToastGeneric">
      <text>Prevent Eye Strain</text>
      <text>Take a 20 second break</text>

$ToastXml = New-Object -TypeName Windows.Data.Xml.Dom.XmlDocument

$notify = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app)



Running this script will create the following notification:

VBS Launcher

The one problem with this is that when run through the task scheduler, a PowerShell console window will flash briefly before the notification displays. To avoid this intrusive popup, a short VBScript program can be used to launch the PowerShell script we’ve just created.


Dim shell,command
command = "powershell.exe -nologo -command ""C:\Scripts\Reminder.ps1"""
Set shell = CreateObject("WScript.Shell")
shell.Run command,0



The next step is to schedule this script to run and produce the notification every 20 minutes. This ended up being way more complex than I originally imagined. The parameters for New-ScheduledTaskTrigger don’t allow setting a RepetitionInterval with a Daily or AtLogOn parameter, which makes doing this a little tricky. In the end, I took some inspiration from this solution. The best solution simply seemed to be to set up the task I want in the Windows Task Scheduler GUI, export the XML, and then use that in the PowerShell script. At this point I’m not really doing much in PowerShell itself to create the task, however, ultimately this still creates a code-based solution that can easily be put into other scripts or run on a server without a GUI.

After creating the task in Windows Task Scheduler, I ran

Export-ScheduledTask "BreakReminder"

in PowerShell and get the following XML:

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="">
    <Description>A reminder every 20 minutes to take a 20 second break to prevent eye strain.</Description>
  <Actions Context="Author">

I then squeezed that all into one line and end up with the following very messy lines of PowerShell code that can be used to create the task from scratch.  The Reminder.ps1 and Launcher.vbs files from above just need to be saved somewhere and the file paths in Launcher.vbs and the below code need to be updated accordingly.

$TaskXML = '<Task version="1.2" xmlns=""> <RegistrationInfo> <Description>A reminder every 20 minutes to take a 20 second break to prevent eye strain.</Description> <URI>\BreakReminder</URI> </RegistrationInfo> <Triggers> <CalendarTrigger> <Repetition> <Interval>PT20M</Interval> <Duration>P1D</Duration> <StopAtDurationEnd>true</StopAtDurationEnd> </Repetition> <StartBoundary>2019-06-10T00:00:00</StartBoundary> <Enabled>true</Enabled> <ScheduleByDay> <DaysInterval>1</DaysInterval> </ScheduleByDay> </CalendarTrigger> </Triggers> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>false</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>false</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <WakeToRun>false</WakeToRun> <ExecutionTimeLimit>P3D</ExecutionTimeLimit> <Priority>7</Priority> </Settings> <Actions Context="Author"> <Exec> <Command>C:\Scripts\Launcher.vbs</Command> </Exec> </Actions> </Task>'

Register-ScheduledTask "BreakReminder" -Xml $TaskXML.OuterXml

Raspberry Pi: Schedule Print Job

After having a couple sets of ink cartridges dry up on me after extended non-use, I learned that I’d need to print something regularly to keep my cheap inkjet printer working. After ignoring a few of my own reminders to print a test page manually, I decided to create an automated solution using a cron job on a Raspberry Pi I already have running on my home network.

To send the file to the printer, I used CUPS (documentation here or here). I followed the tutorial here to get it installed and activate the web interface.

With the printer added, I ran lpstat -p -d to get the exact name of the printer, then lp -d <printer> <file> to send a file to the printer.

To add the weekly cron job, I ran crontab -e and added the following line to get it to print weekly at noon on Sundays:

0 12 * * Sun lp -d HP_DeskJet_printer Sample.pdf


Raspberry Pi – Minimal Headless Setup

For my own quick reference, mostly taken from here:

  1. See end of article if SD card was previously used.
  2. Download Raspbian from
  3. Download and install Win32DiskImager from
  4. Using Win32 Disk Imager, write the ISO to the SD card
  5. Add a file called wpa_supplicant.conf in the boot drive with the following (adding your own SSID and password):
    ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
  6. Add a blank file named ssh (with no file extension) to the boot drive to enable ssh. In Windows notepad or similar, save the file as “ssh” (with the quote marks) to avoid it adding a .txt file extension.
  7. Insert the SD card to the Raspberry Pi, and plug in to power. Wait a few seconds, then use the Fing app to get its IP address on a home network, and use Putty to SSH in using default credentials of pi / raspberry.
  8. Change the default password by running
Edit: Make Raspberry Pi Email You Its IP Address
As mentioned above, I’ve previously used the Fing app to find the IP address of the Pi on my home network. Recently, I’ve needed to access the Pi on a university network and wanted to do so without connecting it to a monitor and keyboard / mouse. Here’s a script to email the IP address of the Pi to myself every time it’s booted. Just modify the email and password (I’m using a secondary email account so I don’t care about the password being stored in plain text):
#pip install netifaces
import pip
def install(package):
if hasattr(pip, 'main'):
pip.main(['install', package])
pip._internal.main(['install', package])
if __name__ == '__main__':

import smtplib
import netifaces as ni
import socket

ip = ni.ifaddresses('wlan0')[ni.AF_INET][0]['addr']
hostname = socket.gethostname()
message = hostname + "\n" + ip

fromaddr = ''
toaddrs = ''
username = ''
password = 'password'
server = smtplib.SMTP('', '587')
server.sendmail(fromaddr, toaddrs, message)

Add the following to /etc/rc.local after the line “# By default this script does nothing.” and point it to the script you just saved:

sleep 20
python /path/to/

If the SD card was previously used in a Raspberry Pi, the partitions will need to be merged again.

  1. Open Disk Management.
  2. Right click and select Delete Volume for both partitions on the SD card.
  3. Right click and select New Simple Volume to create one partition for the whole SD card. Make sure the partition is FAT32. Win32DiskImager will create two partitions again, but this way it initially sees the whole SD card.


Ubuntu: Resume Script in Terminal After Reboot

I needed to reboot in the middle of my shell script to install Kubernetes / Brigade, and my Googling found several people with similar questions, but no answers that quite worked for me. Here’s the basic code just to get a shell script to reboot and then resume in a visible terminal after the reboot.

The commands to be run after the reboot are echoed into a new shell script, so characters like slashes and dollar signs need to be escaped with a backslash.

Most of the posts with similar questions mentioned that running a shell script with sudo is a bad idea. Since my script is designed to be run in a sandboxed VM and most of the commands needed sudo permissions anyways, I figured it’s not a problem in this case.

#after reboot
echo "echo \"Post-reboot content goes here\"

#remove this from running at startup
rm ~/.config/autostart/myScript.desktop
rm /home/$SUDO_USER/" >> /home/$SUDO_USER/

#make file executable
chmod +x ~/

#set new script to run after reboot
cd .config/
mkdir -p "autostart/"
cd ..
echo "[Desktop Entry]
Exec=x-terminal-emulator -e sudo /home/$SUDO_USER/
Name=Testing" >> ~/.config/autostart/myScript.desktop

#before reboot
echo "Pre-reboot commands go here"
sleep 30

reboot -f

Running the script:
 After the reboot:

Self-Elevating PowerShell and Batch Scripts

Many scripts require admin permissions, but remembering to right click and select “Run as Administrator” can be a pain. Fortunately, both batch and PowerShell scripts can written so that they’ll prompt for admin permissions if not run as an administrator. This won’t get around not having administrator permissions, but will save you a couple clicks every run by jumping straight to the UAC prompt.

Neither of these scripts are mine, but I’m documenting them here for my own quick and frequent reference.

CMD / Batch scripts:

:: Elevate.cmd - Version 4
:: Automatically check & get admin rights
:: see "" for description
 @echo off
 ECHO =============================
 ECHO Running Admin shell
 ECHO =============================

 setlocal DisableDelayedExpansion
 set cmdInvoke=1
 set winSysFolder=System32
 set "batchPath=%~0"
 for %%k in (%0) do set batchName=%%~nk
 set "vbsGetPrivileges=%temp%\OEgetPriv_%batchName%.vbs"
 setlocal EnableDelayedExpansion

  if '%errorlevel%' == '0' ( goto gotPrivileges ) else ( goto getPrivileges )

  if '%1'=='ELEV' (echo ELEV & shift /1 & goto gotPrivileges)
  ECHO **************************************
  ECHO Invoking UAC for Privilege Escalation
  ECHO **************************************

  ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
  ECHO args = "ELEV " >> "%vbsGetPrivileges%"
  ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
  ECHO args = args ^& strArg ^& " "  >> "%vbsGetPrivileges%"
  ECHO Next >> "%vbsGetPrivileges%"

  if '%cmdInvoke%'=='1' goto InvokeCmd 

  ECHO UAC.ShellExecute "!batchPath!", args, "", "runas", 1 >> "%vbsGetPrivileges%"
  goto ExecElevation

  ECHO args = "/c """ + "!batchPath!" + """ " + args >> "%vbsGetPrivileges%"
  ECHO UAC.ShellExecute "%SystemRoot%\%winSysFolder%\cmd.exe", args, "", "runas", 1 >> "%vbsGetPrivileges%"

 "%SystemRoot%\%winSysFolder%\WScript.exe" "%vbsGetPrivileges%" %*
 exit /B

 setlocal & cd /d %~dp0
 if '%1'=='ELEV' (del "%vbsGetPrivileges%" 1>nul 2>nul  &  shift /1)

 REM Run shell as admin (example) - put here code as you like
 ECHO %batchName% Arguments: P1=%1 P2=%2 P3=%3 P4=%4 P5=%5 P6=%6 P7=%7 P8=%8 P9=%9
 REM put remaining commands here
 cmd /k

source (stackoverflow)


Fortunately this one’s a lot shorter:

If (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))

	$arguments = "&amp; '" + $myinvocation.mycommand.definition + "'"
	Start-Process powershell -Verb runAs -ArgumentList $arguments

source (stackoverflow)

PowerShell: Ping Range of IP Addresses

The other day I needed to scan a small section of our network which uses static IP addresses rather than DHCP to see which assigned IP addresses actually correlated with a working device and which were no longer being used by an active device. Though there are plenty of fancy tools for this, due to the simplicity of what I wanted I figured it would be easier to throw together a quick PowerShell script than install and learn a third-party tool.

Here’s what I came up with:

$Failure = @() #initialize array

#pings IP addresses from to
for ($i=0; $i -le 255; $i++)
	$IP = "192.168.1." + $i #append the iterator as the last byte of the IP address
	if(!(Test-Connection -ComputerName $IP -Quiet -Count 2)){ #if noting responds to the ping, add the IP address to an array
        $Failure += $IP


foreach ($site in $Failure) {
    Write-Host $site -ForegroundColor red #iterate through and write results from array

Note that this runs really slowly, but in this case there wasn’t any rush, so I could let it run in the background for a while.

Domain User Account Info

This is a short but incredibly useful command line command I’ve found for pulling information on domain accounts in an Active Directory environment:

net user [username] /domain

The net user command is designed to create / update domain accounts, but in this form just displays a ton of info that can be useful to IT, like when a user’s password expires, what active directory groups they’re a member of, etc. There’s plenty of other commands to do similar things, but this one is an easy way to get a readout of a ton of information.

Full documentation is available on Microsoft’s website.