ytdlWrapper.psm1

# Create module-wide variables.
$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$ModuleRoot\ytdlWrapper.psd1").ModuleVersion
$script:Folder = "$env:APPDATA\Powershell\ytdlWrapper"
$script:TemplateData = "$env:APPDATA\Powershell\ytdlWrapper\template-database.$ModuleVersion.xml"
$script:JobData = "$env:APPDATA\Powershell\ytdlWrapper\job-database.$ModuleVersion.xml"

# For the debug output to be displayed, $DebugPreference must be set
# to 'Continue' within the current session.
Write-Debug "`e[4mMODULE-WIDE VARIABLES`e[0m"
Write-Debug "Module root folder: $ModuleRoot"
Write-Debug "Module version: $ModuleVersion"
Write-Debug "Template Database file: $TemplateData"
Write-Debug "Job Database file: $JobData"
Write-Debug "Data Folder: $Folder"

# Create the module data-storage folder if it doesn't exist.
if (-not (Test-Path -Path "$env:APPDATA\Powershell\ytdlWrapper" -ErrorAction Ignore))
{
    New-Item -ItemType Directory -Path "$env:APPDATA" -Name "Powershell\ytdlWrapper" -Force -ErrorAction Stop -WhatIf:$false -Confirm:$false
}
if (-not (Test-Path -Path "$Folder\Templates" -ErrorAction Ignore))
{
    New-Item -ItemType Directory -Path "$Folder" -Name "Templates" -Force -ErrorAction Stop -WhatIf:$false -Confirm:$false
}
if (-not (Test-Path -Path "$Folder\Jobs" -ErrorAction Ignore))
{
    New-Item -ItemType Directory -Path "$Folder" -Name "Jobs" -Force -ErrorAction Stop -WhatIf:$false -Confirm:$false
}
Write-Debug "Created database folders!"

if ($null -eq (Get-Command youtube-dl.exe -ErrorAction SilentlyContinue))
{
    Write-Error "The 'youtube-dl.exe' binary could not be found! Make sure the %PATH% variable has the location of the binary."
}

# Potentially force this module script to dot-source the files, rather than
# load them in an alternative method.
$doDotSource = $global:ModuleDebugDotSource
$doDotSource = $true # Needed to make code coverage tests work

function Resolve-Path_i
{
    <#
    .SYNOPSIS
        Resolves a path, gracefully handling a non-existent path.
         
    .DESCRIPTION
        Resolves a path into the full path. If the path is invalid,
        an empty string will be returned instead.
         
    .PARAMETER Path
        The path to resolve.
         
    .EXAMPLE
        PS C:\> Resolve-Path_i -Path "~\Desktop"
         
        Returns 'C:\Users\...\Desktop"
 
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]
        $Path
    )
    
    # Run the command, silencing errors.
    $resolvedPath = Resolve-Path -Path $Path -ErrorAction Ignore
    
    # If NULL, then just return an empty string.
    if ($null -eq $resolvedPath)
    {
        $resolvedPath = ""
    }
    
    Write-Output $resolvedPath
}
function Import-ModuleFile {
    <#
    .SYNOPSIS
        Loads files into the module on module import.
        Only used in the project development environment.
        In built module, compiled code is within this module file.
         
    .DESCRIPTION
        This helper function is used during module initialization.
        It should always be dot-sourced itself, in order to properly function.
         
    .PARAMETER Path
        The path to the file to load.
         
    .EXAMPLE
        PS C:\> . Import-ModuleFile -File $function.FullName
         
        Imports the code stored in the file $function according to import policy.
         
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Path
    )
    
    # Get the resolved path to avoid any cross-OS issues.
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    
    if ($doDotSource)
    {
        # Load the file through dot-sourcing.
        . $resolvedPath    
        Write-Debug "Dot-sourcing file: $resolvedPath"
    }
    else
    {
        # Load the file through different method (unknown atm?).
        $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) 
        Write-Debug "Importing file: $resolvedPath"
    }
}

# ISSUE WITH BUILT MODULE FILE
# ----------------------------
# If this module file contains the compiled code below, as this is a "packaged"
# build, then that code *must* be loaded, and you cannot individually import
# and of the code files, even if they are there.
#
#
# If this module file is built, then it contains the class definitions below,
# and on Import-Module, this file is AST analysed and those class definitions
# are read-in and loaded.
#
# It's only once a command is run that this module file is executed, and if at
# that point this file starts to individually import the project files, it will
# end up re-defining the classes, and apparently that seems to cause issues
# later down the line.
#
#
# Therefore to prevent this issue, if this module file has been built and it
# contains the compile code below, that code will be used, and nothing else.
#
# The build script should also not package the individual files, so that the
# *only* possibility is to load the compiled code below and there is no way
# the individual files can be imported, as they don't exist.


# If this module file contains the compiled code, import that, but if it
# doesn't, then import the individual files instead.
$importIndividualFiles = $false
if ("<was built>" -eq '<was not built>')
{
    $importIndividualFiles = $true
    Write-Debug "Module not built! Importing individual files."
}

Write-Debug "`e[4mIMPORT DECISION`e[0m"
Write-Debug "Dot-sourcing: $doDotSource"
Write-Debug "Importing individual files: $importIndividualFiles"

# If importing code as individual files, perform the importing.
# Otherwise, the compiled code below will be loaded.
if ($importIndividualFiles)
{
    Write-Debug "!IMPORTING INDIVIDUAL FILES!"
    
    # Execute Pre-import actions.
    . Import-ModuleFile -Path "$ModuleRoot\internal\preimport.ps1"
    
    # Import all internal functions.
    foreach ($file in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $file.FullName
    }
    
    # Import all public functions.
    foreach ($file in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {    
        . Import-ModuleFile -Path $file.FullName
    }
    
    # Execute Post-import actions.
    . Import-ModuleFile -Path "$ModuleRoot\internal\postimport.ps1"
}
else
{
    Write-Debug "!LOADING COMPILED CODE!"
    
    #region Load compiled code
    enum JobState
{
    Valid
    InvalidPath
    MismatchedVariables
    UninitialisedVariables
    HasInputs
}

class YoutubeDlJob
{
    [string]$Name
    [string]$Path
    [hashtable]$_Variables
    [nullable[datetime]]$_lastExecutionTime
    [nullable[boolean]]$_lastExecutionSuccess
    
    # Constructor.
    YoutubeDlJob ([string]$name, [string]$path, [hashtable]$variableValues, [nullable[datetime]]$lastExecutionTime, 
        [nullable[boolean]]$lastExecutionSuccess)
    {
        $this.Name = $name
        $this.Path = $path
        $this._Variables = $variableValues
        $this._lastExecutionTime = $lastExecutionTime
        $this._lastExecutionSuccess = $lastExecutionSuccess
    }
        
    [JobState] GetState()
    {
        # Check through all the invalid states for a job.
        if ($this.HasInvalidPath())
        {
            return [JobState]::InvalidPath
        }
        if ($this.HasInputs())
        {
            return [JobState]::HasInputs
        }
        if ($this.HasMismatchedVariables())
        {
            return [JobState]::MismatchedVariables
        }
        if ($this.HasUninitialisedVariables())
        {
            return [JobState]::UninitialisedVariables
        }
        return [JobState]::Valid
    }
    
    [boolean] HasInvalidPath()
    {
        return [youtubeDlJob]::HasInvalidPath($this.Path)
    }
    static [boolean] HasInvalidPath([string]$path)
    {
        # Check whether the file path is valid.
        if (Test-Path -Path $path)
        {
            return $false
        }
        return $true
    }
    
    [boolean] HasInputs()
    {
        return [YoutubeDlJob]::HasInputs($this.Path)
    }
    static [boolean] HasInputs([string]$path)
    {
        # Check whether there are input definitions.
        if ((Read-ConfigDefinitions -Path $path -InputDefinitions).Count -eq 0)
        {
            return $false
        }
        return $true
    }
    
    [boolean] HasMismatchedVariables()
    {
        $configVariables =  Read-ConfigDefinitions -Path $this.Path -VariableDefinitions
        if (-not($configVariables.Count -eq 0))
        {
            $differenceA = $configVariables | Where-Object { $this._Variables.Keys -notcontains $_ }
            $differenceB = $this._Variables.Keys | Where-Object { $configVariables -notcontains $_ }
            if (($null -ne $differenceA) -or ($null -ne $differenceB))
            {
                return $true
            }
        }
        return $false
    }
    
    [boolean] HasUninitialisedVariables()
    {
        # Check that each variable has a value, i.e. is not uninitialised.
        foreach ($value in $this._Variables.Values)
        {
            if (($null -eq $value) -or [system.string]::IsNullOrWhiteSpace($value))
            {
                return $true
            }
        }
        return $false
    }
    
    
    [System.Collections.Generic.List[string]] GetVariables()
    {
        # Get the definitions within the file.
        return Read-ConfigDefinitions -Path $this.Path -VariableDefinitions
    }
    
    [System.Collections.Generic.List[string]] GetStoredVariables()
    {
        # Get the variable names defined in this object.
        $returnList = New-Object -TypeName System.Collections.Generic.List[string]
        foreach ($key in $this._Variables.Keys)
        {
            $returnList.Add($key)
        }
        
        return $returnList
    }
    
    [System.Collections.Generic.List[string]] GetMissingVariables()
    {
        # Get the variables which are missing in the object but present in
        # the configuration file.
        $configVariables =  Read-ConfigDefinitions -Path $this.Path -VariableDefinitions
        return $configVariables | Where-Object { $this._Variables.Keys -notcontains $_ }
    }
    
    [System.Collections.Generic.List[string]] GetUnnecessaryVariables()
    {
        # Get the variables which are present in the object but missing in
        # the configuration file.
        $configVariables =  Read-ConfigDefinitions -Path $this.Path -VariableDefinitions
        return $this._Variables.Keys | Where-Object { $configVariables -notcontains $_ }
    }
    
    [System.Collections.Generic.List[string]] GetNullVariables()
    {
        # Get any variable names defined in this object which don't have a value.
        $returnList = New-Object -TypeName System.Collections.Generic.List[string]
        foreach ($key in $this._Variables.Keys)
        {
            if (($null -eq $this._Variables[$key]) -or [system.string]::IsNullOrWhiteSpace($this._Variables[$key]))
            {
                $returnList.Add($key)
            }
        }
        
        return $returnList
    }
    
    [hashtable] GetScriptblocks()
    {
        # Get the scriptblock hashtable.
        return Read-ConfigDefinitions -Path $this.Path -VariableScriptblocks
    }
    
    [string] GetCompletedConfigFile()
    {
        # Go through all variable definitions and substitute the stored variable
        # value, before returning the modified file content string.
        $configFilestream = Get-Content -Path $this.Path -Raw
        foreach ($key in $this._Variables.Keys)
        {
            $configFilestream = $configFilestream -replace "v@{$key}{start{(?s)(.*?)}end}", $this._Variables[$key]
        }
        
        return $configFilestream
    }
}

enum TemplateState
{
    Valid
    InvalidPath
    NoInputs
}

class YoutubeDlTemplate
{
    [string]$Name
    [string]$Path
    
    # Constructor.
    YoutubeDlTemplate([string]$name, [string]$path)
    {
        $this.Name = $name
        $this.Path = $path
    }
    
    [TemplateState] GetState()
    {
        # Check through all the invalid states for a template.
        if ($this.HasInvalidPath())
        {
            return [TemplateState]::InvalidPath
        }
        if ($this.HasNoInput())
        {
            return [TemplateState]::NoInputs
        }
        return [TemplateState]::Valid
    }
    
    [boolean] HasInvalidPath()
    {
        return [YoutubeDlTemplate]::HasInvalidPath($this.Path)
    }
    static [boolean] HasInvalidPath([string]$path)
    {
        # Check whether the file path is valid.
        if (Test-Path -Path $path)
        {
            return $false
        }
        return $true
    }
    
    [boolean] HasNoInput()
    {
        return [YoutubeDlTemplate]::HasNoInput($this.Path)
    }
    static [boolean] HasNoInput([string]$path)
    {
        # Check whether the template has no inputs.
        if ((Read-ConfigDefinitions -Path $path -InputDefinitions).Count -gt 0)
        {
            return $false
        }
        return $true
    }
    
    
    [System.Collections.Generic.List[string]] GetInputs()
    {
        # Get the definitions within the file.
        return Read-ConfigDefinitions -Path $this.Path -InputDefinitions
    }
    
    [string] GetCompletedConfigFile([hashtable]$inputs)
    {
        # Go through all input definitions and substitute the user provided
        # value, before returning the modified file content string.
        $configFilestream = Get-Content -Path $this.Path -Raw
        foreach ($key in $inputs.Keys)
        {
            $configFilestream = $configFilestream -replace "i@{$key}", $inputs[$key]
        }
        
        return $configFilestream
    }
    
}


<#
.SYNOPSIS
    Starts the youtube-dl process and waits for it to finish.
 
.DESCRIPTION
    Starts the youtube-dl process and waits for it to finish.
     
.EXAMPLE
    PS C:\> Invoke-Process -Path $path
     
    Starts youtube-dl specifying the configuration file at the $path location.
     
.PARAMETER Path
    Path of the location of the configuration file to execute.
     
.INPUTS
    None
     
.OUTPUTS
    None
     
.NOTES
     
#>

function Invoke-Process
{
    
    [CmdletBinding()]
    Param
    (
        
        [Parameter(Position = 0, Mandatory = $true)]
        [string]
        $Path
        
    )
    
    # Define youtube-dl process information.
    $processStartupInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
        FileName = "youtube-dl"
        Arguments = "--config-location `"$Path`""
        UseShellExecute = $false
    }
    
    # Start and wait for youtube-dl to finish.
    $process = New-Object System.Diagnostics.Process
    $process.StartInfo = $processStartupInfo
    $process.Start() | Out-Null
    $process.WaitForExit()
    $process.Dispose()
    
}

<#
.SYNOPSIS
    Reads all the definitions from a configuration file. Can specify between
    input definitions, variable definitions, or variable scriptblocks.
     
.DESCRIPTION
    Reads all the definitions from a configuration file. Can specify between
    input definitions, variable definitions, or variable scriptblocks.
     
.PARAMETER Path
    Path of the location of the configuration file.
     
.PARAMETER InputDefinitions
    Get the input definitions names.
     
.PARAMETER VariableDefinitions
    Get the variable definition names.
     
.PARAMETER VariableScriptblocks
    Get the variable scriptblock strings.
     
.EXAMPLE
    PS C:\> Read-ConfigDefinitions -Path ~\conf.txt -InputDefinitions
     
    Reads in and generates a list of all input definitions.
     
.INPUTS
    None
     
.OUTPUTS
    System.Collections.Generic.List[string]
    Hashtable[string, scriptblock]
     
.NOTES
     
#>

function Read-ConfigDefinitions
{
    
    [CmdletBinding()]
    param
    (
        
        [Parameter(Position = 0, Mandatory = $true)]
        [String]
        $Path,
        
        [Parameter()]
        [switch]
        $InputDefinitions,
        
        [Parameter()]
        [switch]
        $VariableDefinitions,
        
        [Parameter()]
        [switch]
        $VariableScriptblocks
        
    )
    
    # If the file doesn't exist, quit early.
    if (-not (Test-Path -Path $Path))
    {
        return $null
    }
    
    # Read in the config file as a single string.
    $configFilestream = Get-Content -Path $Path -Raw
    $definitionList = New-Object -TypeName System.Collections.Generic.List[string]
    $hashList = @{}
    
    if ($InputDefinitions -eq $true)
    {
        # Find all matches to:
        # 1. --some-parameter i@{name} : full parameter definition
        # 1. -s i@{name} : shorthand parameter definition
        # 2. 'i@{Url}' : special case for url, since it doesn't have a flag
        # Also matches even if multiple parameter definitions are on the same line.
        $regex = [regex]::Matches($configFilestream, "(-(\S+)\s'?i@{(\w+)}'?)\s*")
        $url = [regex]::Match($configFilestream, "'i@{url}'", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
        
        # Add the definition name fields to the list.
        foreach ($match in $regex)
        {
            # .Group[1] is the whole match
            # .Group[2] is the 'some-parameter' or 's' match
            # .Group[3] is the 'name' match
            $definitionList.Add($match.Groups[3].Value)
        }
        # If a url input is detected, add that too.
        if ($url.Success)
        {
            $definitionList.Add("Url")
        }
    }
    else
    {
        # Find all matches to:
        # 1. --some-parameter v@{name}{start{scriptblock}end} : full parameter definition
        # 1. -s v@{name}{start{scritpblock}end} : shorthand parameter definition
        # Also matches even if multiple parameter definitions are on the same line.
        $regex = [regex]::Matches($configFilestream, "(-(\S+)\s'?v@{(\w+)}{start{(?s)(.*?)}end}'?)\s+")
        
        # Add the descriptor fields to the list.
        foreach ($match in $regex)
        {
            # .Group[1] is the whole match
            # .Group[2] is the 'some-parameter' or 's' match
            # .Group[3] is the 'name' match
            # .Group[4] is the 'scriptblock' match
            if ($VariableDefinitions -eq $true)
            {
                $definitionList.Add($match.Groups[3].Value)
            }
            elseif ($VariableScriptblocks -eq $true)
            {
                $hashList[$match.Groups[3].Value] = $match.Groups[4].Value
            }
        }
    }
    
    if ($VariableScriptblocks)
    {
        Write-Output $hashList
    }
    else
    {
        # Return the list as a List object, rather than as an array (by default).
        Write-Output $definitionList -NoEnumerate
    }
}

<#
.SYNOPSIS
    Reads all of the defined job objects.
     
.DESCRIPTION
    Reads all of the defined job objects.
     
.EXAMPLE
    PS C:\> $list = Read-Jobs
     
    Reads all of the job objects into a variable, for later manipulation.
     
.INPUTS
    None
     
.OUTPUTS
    System.Collections.Generic.List[YoutubeDlJob]
     
.NOTES
     
#>

function Read-Jobs
{
    # Create an empty list.
    $jobList = New-Object -TypeName System.Collections.Generic.List[YoutubeDlJob]
    
    # If the file doesn't exist, skip any importing.
    if (Test-Path -Path $script:JobData -ErrorAction SilentlyContinue)
    {
        # Read the xml data in.
        $xmlData = Import-Clixml -Path $script:JobData
        
        # Iterate through all the objects.
        foreach ($item in $xmlData)
        {
            # Rather than extracting the deserialised objects, which would
            # create a mess of serialised and non-serialised objects, create
            # new identical copies from scratch.
            if ($item.pstypenames[0] -eq "Deserialized.YoutubeDlJob")
            {
                $job = [YoutubeDlJob]::new($item.Name, $item.Path, $item._Variables, $item._lastExecutionTime, $item._lastExecutionSuccess)
                $jobList.Add($job)
            }
        }
    }
    
    # Return the list as a <List> object, rather than as an array,
    # (ps converts by default).
    Write-Output $jobList -NoEnumerate
}


<#
.SYNOPSIS
    Reads all of the defined template objects.
     
.DESCRIPTION
    Reads all of the defined template objects.
     
.EXAMPLE
    PS C:\> $list = Read-Templates
     
    Reads all of the template objects into a variable, for later manipulation.
     
.INPUTS
    None
     
.OUTPUTS
    System.Collections.Generic.List[YoutubeDlTemplate]
     
.NOTES
     
#>

function Read-Templates
{
    # Create an empty list.
    $templateList = New-Object -TypeName System.Collections.Generic.List[YoutubeDlTemplate]
    
    # If the file doesn't exist, skip any importing.
    if (Test-Path -Path $script:TemplateData -ErrorAction SilentlyContinue)
    {
        # Read the xml data in.
        $xmlData = Import-Clixml -Path $script:TemplateData
        
        # Iterate through all the objects.
        foreach ($item in $xmlData)
        {
            # Rather than extracting the deserialised objects, which would
            # create a mess of serialised and non-serialised objects, create
            # new identical copies from scratch.
            if ($item.pstypenames[0] -eq "Deserialized.YoutubeDlTemplate")
            {
                $template = [YoutubeDlTemplate]::new($item.Name, $item.Path)
                $templateList.Add($template)
            }
        }
    }
    
    # Return the list as a <List> object, rather than as an array,
    # (ps converts by default).
    Write-Output $templateList -NoEnumerate
}


<#
.SYNOPSIS
    Gets the specified youtube-dl item(s).
     
.DESCRIPTION
    The `Get-Item` cmdlet gets one or more youtube-dl templates or jobs,
    specified by their name(s).
     
.PARAMETER Template
    Indicates that this cmdlet will be retrieving youtube-dl template(s).
     
.PARAMETER Job
    Indicates that this cmdlet will be retrieving youtube-dl job(s).
     
.PARAMETER Names
    Specifies the name(s) of the items to get.
     
 [!]Once you specify the '-Template'/'-Job' switch, this parameter will
    autocomplete to valid names for the respective item type.
     
.PARAMETER All
    Specifies to get all items of the respective item type.
     
.INPUTS
    System.String[]
        You can pipe one or more strings containing the names of the items
        to get.
     
.OUTPUTS
    YoutubeDlTemplate
    YoutubeDlJob
     
.NOTES
    This cmdlet is aliased by default to 'gydl'.
     
.EXAMPLE
    PS C:\> Get-YoutubeDlItem -Template -Names "music","video"
     
    Gets the youtube-dl template definitions which are named "music" and
    "video", and pipes them out to the screen, by default formatted in a list.
     
.EXAMPLE
    PS C:\> Get-YoutubeDlItem -Job -All
     
    Gets all youtube-dl job definitions, and pipes them out to the screen,
    by default formatted in a list.
     
.EXAMPLE
    PS C:\> Get-YoutubeDlItem -Job "music" | Invoke-YoutubeDl -Job
     
    Gets the youtube-dl job named "music", and then invokes youtube-dl to
    run it automatically.
     
.LINK
    New-YoutubeDlItem
    Set-YoutubeDlItem
    Remove-YoutubeDlItem
    Invoke-YoutubeDl
    about_ytdlWrapper
     
#>

function Get-YoutubeDlItem
{
    [Alias("gydl")]
    
    [CmdletBinding()]
    param
    (
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Template-All")]
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Template-Specific")]
        [switch]
        $Template,
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job-All")]
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job-Specific")]
        [switch]
        $Job,
        
        [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "Template-Specific")]
        [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "Job-Specific")]
        [Alias("Name")]
        [string[]]
        $Names,
        
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Template-All")]
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Job-All")]
        [switch]
        $All
        
    )
    
    begin
    {
        # Store the retrieved items, to in one go at the end of execution.
        $outputList = if ($Template)
        {
            New-Object -TypeName System.Collections.Generic.List[YoutubeDlTemplate]
        }
        elseif ($Job)
        {
            New-Object -TypeName System.Collections.Generic.List[YoutubeDlJob]
        }
        
        # Read in the correct list of templates or jobs.
        $objectList = if ($Template)
        {
            Read-Templates
        }
        elseif ($Job)
        {
            Read-Jobs
        }
    }
    
    process
    {
        if (-not $All)
        {
            # Iterate through all the passed in names.
            foreach ($name in $Names)
            {
                # If the object doesn't exist, warn the user.
                $existingObject = $objectList | Where-Object { $_.Name -eq $name }
                if ($null -eq $existingObject)
                {
                    Write-Warning "There is no $(if($Template){`"template`"}else{`"job`"}) named: '$name'."
                    continue
                }
                
                # Add the object for outputting.
                $outputList.Add($existingObject) | Out-Null
            }
        }
        else
        {
            # Output every object.
            $outputList = $objectList
        }
    }
    
    end
    {
        # By default, this outputs in List formatting.
        $outputList | Sort-Object -Property Name
    }
}

<#
.SYNOPSIS
    Runs youtube-dl.
     
.DESCRIPTION
    The `Invoke-YoutubeDl` cmdlet runs youtube-dl.exe using the specified
    method.
     
    This cmdlet can be used to run youtube-dl, giving it a fully completed
    configuration file which matches the youtube-dl config specification.
     
    This cmdlet can be used to run a youtube-dl template, giving it the
    required input parameters.
     
    This cmdlet can be used to run a youtube-dl job, which happens without
    user input.
     
.PARAMETER Template
    Indicates that this cmdlet will be running a youtube-dl template.
     
.PARAMETER Job
    Indicates that this cmdlet will be running a youtube-dl job.
     
.PARAMETER Path
    Specifies the path of the location of the configuration file to use.
     
.PARAMETER Names
    Specifies the name(s) of the items to run.
     
 [!]Once you specify the '-Template'/'-Job' switch, this parameter will
    autocomplete to valid names for the respective item type.
     
    If specifying the '-Template' switch, you can only pass in one name.
     
    If specifying the '-Job' switch, you can pass in multiple names.
 
.PARAMETER WhatIf
    Shows what would happen if the cmdlet runs. The cmdlet does not run.
     
.PARAMETER Confirm
    Prompts you for confirmation before running any state-altering actions
    in this cmdlet.
     
.EXAMPLE
    PS C:\> Invoke-YoutubeDl -Path ~\download.conf
     
    Runs youtube-dl, giving it the "download.conf" configuration file to parse.
    The configuration file must fully align to the youtube-dl config
    specification.
     
.EXAMPLE
    Assuming the template 'music' has the input named "Url".
     
    PS C:\> Invoke-YoutubeDl -Template -Name "music" -Url "https:\\some\url"
     
    Runs the "music" template, which takes in the '-Url' parameter to complete
    the configuration file, before giving it to youtube-dl.
     
.EXAMPLE
    PS C:\> Invoke-YoutubeDl -Job -Name "archive"
     
    Runs the "archive" job, which uses the stored variables to complete the
    configuration file and pass it to youtube-dl. Afterwards, the scriptblocks
    responsible for each variable run to generate the new variable values to
    be used for the next run.
     
.INPUTS
    System.String[]
        You can pipe one or more strings containing the names of the items
        to run.
     
.OUTPUTS
    None
     
.NOTES
    When executing a template using the '-Template' switch, a dynamic parameter
    corresponding to each input definition, found within the configuration
    file, will be generated. The parameter sets the value of the input to make
    the template ready for execution.
     
    For detailed help regarding running a template, see the
    "INVOKING A TEMPLATE" section in the help at:
    'about_ytdlWrapper_templates'.
 
    This cmdlet is aliased by default to 'iydl'.
     
.LINK
    New-YoutubeDlItem
    Get-YoutubeDlItem
    Set-YoutubeDlItem
    Remove-YoutubeDlItem
    about_ytdlWrapper
     
#>

function Invoke-YoutubeDl
{
    [Alias("iydl")]
    
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Template")]
        [switch]
        $Template,
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job")]
        [switch]
        $Job,
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Config")]
        [Alias("ConfigurationFilePath")]
        [string]
        $Path,
        
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Template")]
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Job", ValueFromPipelineByPropertyName = $true)]
        [Alias("Name")]
        [string[]]
        $Names
        
    )
    
    dynamicparam
    {
        # Only run the input detection logic if a template is given, and only
        # one template is given, and the template exists, and the template
        # has a valid configuration file path.
        if (-not $Template) { return }
        if ($null -eq $Names) { return }
        $name = $Names[0]
        if ([system.string]::IsNullOrWhiteSpace($name)) { return }
        $templateList = Read-Templates
        $templateObject = $templateList | Where-Object { $_.Name -eq $name }
        if ($null -eq $templateObject) { return }
        if ($templateObject.GetState() -eq "InvalidPath") { return }
        
        # Retrieve all instances of input definitions in the config file.
        $inputNames = $templateObject.GetInputs()
        
        # Define the dynamic parameter dictionary to hold new parameters.
        $parameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        # Now that a list of all input definitions is found, create a
        # dynamic parameter for each one.
        foreach ($input in $inputNames)
        {
            # Set up the necessary objects for a parameter.
            $paramAttribute = New-Object System.Management.Automation.ParameterAttribute
            $paramAttribute.Mandatory = $true
            $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $attributeCollection.Add($paramAttribute)                
            $param = New-Object System.Management.Automation.RuntimeDefinedParameter($input, [String], $attributeCollection)
            
            $parameterDictionary.Add($input, $param)
        }
        
        return $parameterDictionary
    }
    
    process
    {
        if ($PSCmdlet.ParameterSetName -eq "Config")
        {
            # Validate that the path is valid.
            if (-not (Test-Path -Path $Path))
            {
                Write-Error "The configuration file path: '$Path' is invalid!"
                return
            }
            
            if ($PSCmdlet.ShouldProcess("Starting youtube-dl.exe.", "Are you sure you want to start youtube-dl.exe?", "Start Process Prompt"))
            {
                Invoke-Process -Path "$script:Folder\$hash.conf"
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Template")
        {
            # Only accept one template at a time.
            if ($Names.Length -gt 1)
            {
                Write-Error "Cannot specify more than one template per invocation of this cmdlet!"
                return
            }
            
            $name = $Names[0]
            
            # Retrieve the template and check that it exists.
            $templateList = Read-Templates
            $templateObject = $templateList | Where-Object { $_.Name -eq $name }
            Write-Verbose "Validating parameters and the configuration file."
            if ($null -eq $templateObject)
            {
                Write-Error "There is no template named: '$name'."
                return
            }
            
            # Validate that the template can be used.
            if ($templateObject.HasInvalidPath())
            {
                Write-Error "The template: '$name' has a configuration file path: '$($templateObject.Path)' which is invalid!"
                    return
            }
            if ($templateObject.HasNoInput())
            {
                Write-Error "The template: '$name' has a configuration file with no input definitions!`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_templates`'."
                    return
            }
            
            # Get the necessary inputs for this template, and assign each the
            # user provided value. Quit if the user has failed to give in a
            # certain value.
            $inputNames = $templateObject.GetInputs()
            $inputs = @{}
            foreach ($input in $inputNames)
            {
                if ($PSBoundParameters.ContainsKey($input))
                {
                    $inputs[$input] = $PSBoundParameters[$input]
                }
                else
                {
                    Write-Error "The template: '$name' requires the input: '$input' which has been not provided!"
                    return
                }
            }
            
            $completedTemplateContent = $templateObject.GetCompletedConfigFile($inputs)
            
            # Write modified config file (with substituted user inputs) to a
            # temporary file. This is done because it is easier to use the
            # --config-location flag for youtube-dl than to edit the whole
            # string to use proper escape sequences.
            $stream = [System.IO.MemoryStream]::new([byte[]][char[]]$completedTemplateContent)
            $hash = (Get-FileHash -InputStream $stream -Algorithm SHA256).hash
            if ($PSCmdlet.ShouldProcess("Creating temporary configuration file at: '$script:Folder\$hash.conf'.", "Are you sure you want to create a temporary configuration file at: '$script:Folder\$hash.conf'?", "Create File Prompt"))
            {
                Out-File -FilePath "$script:Folder\$hash.conf" -Force -InputObject $completedTemplateContent `
                    -ErrorAction Stop
            }
            
            if ($PSCmdlet.ShouldProcess("Starting youtube-dl.exe.", "Are you sure you want to start youtube-dl.exe?", "Start Process Prompt"))
            {
                Invoke-Process -Path "$script:Folder\$hash.conf"
            }
            
            # Clean up the temporary file.
            if ($PSCmdlet.ShouldProcess("Clean-up temporary configuration file from: '$script:Folder\$hash.conf'.", "Are you sure you want to clean-up the temporary configuration file from: '$script:Folder\$hash.conf'?", "Delete File Prompt"))
            {
                Remove-Item -Path "$script:Folder\$hash.conf" -Force
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Job")
        {
            foreach ($name in $Names)
            {
                # Retrieve the template and check that it exists.
                $jobList = Read-Jobs
                $jobObject = $jobList | Where-Object { $_.Name -eq $name }
                Write-Verbose "Validating parameters and the configuration file."
                if ($null -eq $jobObject)
                {
                    Write-Error "There is no job named: '$name'."
                    return
                }
                
                # Validate that the job can be used.
                if ($jobObject.HasInvalidPath())
                {
                    Write-Error "The configuration file path: '$Path' is invalid."
                    return
                }
                if ($jobObject.HasInputs())
                {
                    Write-Error "The configuration file at: '$Path' has input definitions, which a job cannot have.`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_jobs`'."
                    return
                }
                if ($jobObject.HasMismatchedVariables())
                {
                    Write-Error "The job: '$name' has a mismatch between the variables stored in the database and the variable definitions within the configuration file!`nRun the `Set-YoutubeDlItem` cmdlet with the '-Update' switch to fix the issue."
                    return
                }
                if ($jobObject.HasUninitialisedVariables())
                {
                    Write-Error "The job: '$name' has uninitialised variables and cannot run!`nRun the `Set-YoutubeDlItem` cmdlet with the '-Update' switch to fix the issue."
                    return
                }
                
                $completedJobContent = $jobObject.GetCompletedConfigFile()
                
                # Write modified config file (with substituted variable values) to a
                # temporary file. This is done because it is easier to use the
                # --config-location flag for youtube-dl than to edit the whole
                # string to use proper escape sequences.
                $stream = [System.IO.MemoryStream]::new([byte[]][char[]]$completedJobContent)
                $hash = (Get-FileHash -InputStream $stream -Algorithm SHA256).hash
                if ($PSCmdlet.ShouldProcess("Creating temporary configuration file at: '$script:Folder\$hash.conf'.", "Are you sure you want to create a temporary configuration file at: '$script:Folder\$hash.conf'?", "Create File Prompt"))
                {
                    Out-File -FilePath "$script:Folder\$hash.conf" -Force -InputObject $completedJobContent `
                        -ErrorAction Stop
                }
                
                if ($PSCmdlet.ShouldProcess("Starting youtube-dl.exe.", "Are you sure you want to start youtube-dl.exe?", "Start Process Prompt"))
                {
                    Invoke-Process -Path "$script:Folder\$hash.conf"
                }
                # Set the appropriate execution information.
                if ($LASTEXITCODE -eq 0)
                {
                    $jobObject._lastExecutionSuccess = $true
                }
                else
                {
                    $jobObject._lastExecutionSuccess = $false
                }
                $jobObject._lastExecutionTime = Get-Date
                
                # Clean up the temporary file.
                if ($PSCmdlet.ShouldProcess("Clean-up temporary configuration file from: '$script:Folder\$hash.conf'.", "Are you sure you want to clean-up the temporary configuration file from: '$script:Folder\$hash.conf'?", "Delete File Prompt"))
                {
                    Remove-Item -Path "$script:Folder\$hash.conf" -Force
                }
                
                # If a scriptblock didn't return a value, warn the user.
                Write-Verbose "Updating variable values for the job."
                $scriptblocks = $jobObject.GetScriptblocks()
                foreach ($key in $scriptblocks.Keys)
                {
                    $scriptblock = [scriptblock]::Create($scriptblocks[$key])
                    $returnResult = Invoke-Command -ScriptBlock $scriptblock
                    # If no value is returned, return the variable name to the invocation
                    # cmdlet to warn the user.
                    if ($null -eq $returnResult)
                    {
                        Write-Error "The job: '$name' has a scriptblock definition named: '$key' which did not return a value!`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_jobs`'."
                        $jobObject._lastExecutionSuccess = $false
                        $jobObject._Variables[$key] = $null
                        continue
                    }
                    
                    $jobObject._Variables[$key] = $returnResult
                }
                
                if ($PSCmdlet.ShouldProcess("Updating database at '$script:JobData' with the changes.", "Are you sure you want to update the database at '$script:JobData' with the changes?", "Save File Prompt"))
                {
                    Export-Clixml -Path $script:JobData -InputObject $jobList -WhatIf:$false -Confirm:$false `
                        | Out-Null
                }
            }
        }
    }
}

<#
.SYNOPSIS
    Creates a new youtube-dl item.
     
.DESCRIPTION
    The `New-YoutubeDlItem` cmdlet creates a new youtube-dl template or job,
    and sets its values in accordance to the given configuration
    file.
     
    This cmdlet can be used to create a youtube-dl template, which takes in
    a configuration file with input definitions. Alternatively, this cmdlet
    can be used to create a youtube-dl job, which takes in a configuration
    file with variable definitions.
     
    This cmdlet can optionally keep the configuration files in their original
    location if desired.
     
.PARAMETER Template
    Indicates that this cmdlet will be creating a youtube-dl template.
     
.PARAMETER Job
    Indicates that this cmdlet will be creating a youtube-dl job.
     
.PARAMETER Name
    Specifies the name of the item to be created; must be unique.
     
.PARAMETER Path
    Specifies the path of the location of the configuration file to use.
     
.PARAMETER DontMoveConfigurationFile
    Prevents the configuration file from being moved from its original location
    to a new location in the module appdata folder.
     
.PARAMETER WhatIf
    Shows what would happen if the cmdlet runs. The cmdlet does not run.
     
.PARAMETER Confirm
    Prompts you for confirmation before running any state-altering actions
    in this cmdlet.
     
.PARAMETER Force
    Forces this cmdlet to create an item that writes over an existing item.
    Even using this parameter, if the filesystem denies access to the
    necessary files, this cmdlet will fail.
     
.INPUTS
    System.String
        You can pipe a string containing a path to the location of the
        configuration file.
     
.OUTPUTS
    YoutubeDlTemplate
    YoutubeDlJob
     
.NOTES
    When creating a job using the '-Job' switch, a dynamic parameter
    corresponding to each variable definition, found within the configuration
    file, will be generated. The parameter sets the initial value of the
    variable to make the job ready for first-time execution.
     
    For detailed help regarding the configuration file, see the
    "SETTING UP A CONFIGURATION FILE" section in the help at:
    'about_ytdlWrapper_jobs'.
     
    This cmdlet is aliased by default to 'nydl'.
     
.EXAMPLE
    PS C:\> New-YoutubeDlItem -Template -Name "music" -Path ~\music.conf
     
    Creates a new youtube-dl template named "music", and moves the configuration
    file to the module appdata folder.
     
.EXAMPLE
    PS C:\> New-YoutubeDlItem -Template -Name "music" -Path ~\music.conf
                -DontMoveConfigurationFile
                 
    Creates a new youtube-dl template named "music", but doesn't move the
    configuration file from the existing location. If this file is ever moved
    manually, this template will cease working until the path is updated to
    the new location of the configuration file.
     
.EXAMPLE
    Assuming 'music.conf' has an input definition named "Url".
     
    PS C:\> New-YoutubeDlItem -Template -Name "music" -Path ~\music.conf |
                Invoke-YoutubeDl -Template -Url "https:\\some\youtube\url"
     
    Creates a new youtube-dl template named "music", and then invokes
    youtube-dl to run it, giving in the required inputs (Url) in the process.
     
.EXAMPLE
    Assuming 'archive.conf' has a variable definition named "Autonumber".
     
    PS C:\> New-YoutubeDlJob -Job -Name "archive" -Path ~\archive.conf
                -Autonumber "5"
                 
    Creates a new youtube-dl job named "archive", and moves the configuration
    file from the home directory to the module appdata foler. Also sets
    the 'Autonumber' variable within this configuration file to an initial
    value of "5".
     
.LINK
    Get-YoutubeDlItem
    Set-YoutubeDlItem
    Remove-YoutubeDlItem
    about_ytdlWrapper
     
#>

function New-YoutubeDlItem
{
    [Alias("nydl")]
    
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Template")]
        [switch]
        $Template,
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job")]
        [switch]
        $Job,
        
        [Parameter(Position = 1, Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Position = 2, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("ConfigurationFilePath")]
        [string]
        $Path,
        
        [Parameter(Position = 3)]
        [switch]
        $DontMoveConfigurationFile,
        
        [Parameter()]
        [switch]
        $Force
        
    )
    
    dynamicparam
    {
        # Only run the variable detection logic if creating a new job,
        # and a valid configuration file path has been given in.
        if ($Job -and ($null -ne $Path) -and (Test-Path -Path $Path))
        {
            # Retrieve all instances of variable definitions in the config file.
            $definitionList = Read-ConfigDefinitions -Path $Path -VariableDefinitions
            
            # Define the dynamic parameter dictionary to hold new parameters.
            $parameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            
            # Create a parameter for each variable definition found.
            foreach ($definition in $definitionList)
            {
                # Set up the necessary objects for a parameter.
                $paramAttribute = New-Object System.Management.Automation.ParameterAttribute
                $paramAttribute.Mandatory = $true
                $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                $attributeCollection.Add($paramAttribute)                
                $param = New-Object System.Management.Automation.RuntimeDefinedParameter($definition, [String], `
                    $attributeCollection)
                
                $parameterDictionary.Add($definition, $param)
            }
            
            return $parameterDictionary
        }
    }
    
    begin
    {
        # Validate that '-WhatIf'/'-Confirm' isn't used together with '-Force'.
        # This is ambiguous, so warn the user instead.
        Write-Debug "`$WhatIfPreference: $WhatIfPreference"
        Write-Debug "`$ConfirmPreference: $ConfirmPreference"
        if ($WhatIfPreference -and $Force)
        {
            Write-Error "You cannot specify both '-WhatIf' and '-Force' in the invocation for this cmdlet!"
            return
        }
        if (($ConfirmPreference -eq "Low") -and $Force)
        {
            Write-Error "You cannot specify both '-Confirm' and '-Force' in the invocation for this cmdlet!"
            return
        }
    }
    
    process
    {
        if ($Template)
        {
            # Validate that the name isn't already taken.
            $templateList = Read-Templates
            $existingTemplate = $templateList | Where-Object { $_.Name -eq $Name }
            Write-Verbose "Validating parameters and the configuration file."
            if ($null -ne $existingTemplate)
            {
                if ($Force)
                {
                    Write-Verbose "Existing template named: '$Name' exists, but since the '-Force' switch is present, the existing template will be deleted."
                    $existingTemplate | Remove-YoutubeDlItem -Template
                }
                else
                {
                    Write-Error "The name: '$Name' is already taken for a template."
                    return
                }
            }
            
            # Validate that the configuration file exists and can be used.
            if ([YoutubeDlTemplate]::HasInvalidPath($Path))
            {
                Write-Error "The configuration file path: '$Path' is invalid."
                return
            }
            if ([YoutubeDlTemplate]::HasNoInput($Path))
            {
                Write-Error "The configuration file located at: '$Path' has no input definitions.`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_templates`'."
                    return
            }
            
            if (-not $DontMoveConfigurationFile -and $PSCmdlet.ShouldProcess("Moving configuration file from '$(Split-Path -Path $Path -Parent)' to '$script:Folder\Templates'.", "Are you sure you want to move the configuration file from '$(Split-Path -Path $Path -Parent)' to '$script:Folder\Templates'?", "Move File Prompt")) 
            {
                # Move the file over to the module appdata folder, and rename it
                # to the unique name of the template to avoid any potential
                # filename collisions.
                $fileName = Split-Path -Path $Path -Leaf
                Move-Item -Path $Path -Destination "$script:Folder\Templates" -Force -WhatIf:$false `
                    -Confirm:$false | Out-Null
                Rename-Item -Path "$script:Folder\Templates\$fileName" -NewName "$Name.conf" -Force -WhatIf:$false `
                    -Confirm:$false | Out-Null
                $Path = "$script:Folder\Templates\$Name.conf"
            }
            
            # Create the object and save it to the database.
            Write-Verbose "Creating new youtube-dl template object."
            $newTemplate = [YoutubeDlTemplate]::new($Name, $Path)
            $templateList.Add($newTemplate)
            if ($PSCmdlet.ShouldProcess("Saving newly-created template to database at '$script:TemplateData'.", "Are you sure you want to save the newly-created template to the database at '$script:TemplateData'?", "Save File Prompt"))
            {
                Export-Clixml -Path $script:TemplateData -InputObject $templateList -Force -WhatIf:$false `
                    -Confirm:$false | Out-Null
            }
            
            Write-Output $newTemplate
        }
        elseif ($Job)
        {
            # Validate that the name isn't already taken.
            $jobList = Read-Jobs
            $existingJob = $jobList | Where-Object { $_.Name -eq $Name }
            Write-Verbose "Validating parameters and the configuration file."
            if ($null -ne $existingJob)
            {
                if ($Force)
                {
                    Write-Verbose "Existing job named: '$Name' exists, but since the '-Force' switch is present, the existing job will be deleted."
                    $existingJob | Remove-YoutubeDlItem -Job
                }
                else
                {
                    Write-Error "The name: '$Name' is already taken for a job."
                    return
                }
            }
            
            # Validate that each required variable in the configuration file
            # has been given an initial value.
            $variableDefinitions = Read-ConfigDefinitions -Path $Path -VariableDefinitions
            $initialVariableValues = @{}
            foreach ($definition in $variableDefinitions)
            {
                if ($PSBoundParameters.ContainsKey($definition))
                {
                    $initialVariableValues[$definition] = $PSBoundParameters[$definition]
                }
                else
                {
                    Write-Error "The variable: '$definition' has not been provided an initial value as a parameter!"
                    return
                }
            }
            # Validate that the configuration file exists and can be used.
            if ([YoutubeDlJob]::HasInvalidPath($Path))
            {
                Write-Error "The configuration file path: '$Path' is invalid."
                return
            }
            if ([YoutubeDlJob]::HasInputs($Path))
            {
                Write-Error "The configuration file at: '$Path' has input definitions, which a job cannot have.`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_jobs`'."
                return
            }
            
            if (-not $DontMoveConfigurationFile -and $PSCmdlet.ShouldProcess("Moving configuration file from '$(Split-Path -Path $Path -Parent)' to '$script:Folder\Jobs'.", "Are you sure you want to move the configuration file from '$(Split-Path -Path $Path -Parent)' to '$script:Folder\Jobs'?", "Move File Prompt"))
            {
                # Move the file over to the module appdata folder, and rename it
                # to the unique name of the template to avoid any potential
                # filename collisions.
                $fileName = Split-Path -Path $Path -Leaf
                Move-Item -Path $Path -Destination "$script:Folder\Jobs" -Force -WhatIf:$false -Confirm:$false `
                    | Out-Null
                Rename-Item -Path "$script:Folder\Jobs\$fileName" -NewName "$Name.conf" -Force -WhatIf:$false `
                    -Confirm:$false | Out-Null
                $Path = "$script:Folder\Jobs\$Name.conf"
            }
            
            # Create the object and save it to the database.
            Write-Verbose "Creating new youtube-dl job object."
            $newJob = [YoutubeDlJob]::new($Name, $Path, $initialVariableValues, $null, $null)
            $jobList.Add($newJob)
            if ($PSCmdlet.ShouldProcess("Saving newly-created template to database at '$script:JobData'.", "Are you sure you want to save the newly-created template to the database at '$script:JobData'?", "Save File Prompt"))
            {
                Export-Clixml -Path $script:JobData -InputObject $jobList -Force -WhatIf:$false -Confirm:$false `
                    | Out-Null
            }
            
            Write-Output $newJob
        }
    }
}

<#
.SYNOPSIS
    Deletes a specified youtube-dl item.
     
.DESCRIPTION
    The `Remove-YoutubeDlItem` cmdlet deletes one or more youtube-dl templates
    or jobs, specified by their name(s).
     
.PARAMETER Template
    Indicates that this cmdlet will be deleting youtube-dl template(s).
     
.PARAMETER Job
    Indicates that this cmdlet will be deleting youtube-dl job(s).
     
.PARAMETER Names
    Specifies the name(s) of the items to delete.
     
 [!]Once you specify a '-Template'/'-Job' switch, this parameter will
    autocomplete to valid names for the respective item type.
     
.PARAMETER WhatIf
    Shows what would happen if the cmdlet runs. The cmdlet does not run.
     
.PARAMETER Confirm
    Prompts you for confirmation before running any state-altering actions
    in this cmdlet.
     
.INPUTS
    System.String[]
        You can pipe one or more strings containing the names of the items
        to delete.
     
.OUTPUTS
    None
     
.NOTES
    This cmdlet is aliased by default to 'rydl'.
     
.EXAMPLE
    PS C:\> Remove-YoutubeDlItem -Template -Names "music","video"
     
    Deletes the youtube-dl templates named "music" and "video".
     
.EXAMPLE
    PS C:\> Remove-YoutubeDlItem -Job -Name "archive"
     
    Deletes a youtube-dl job named "archive".
     
.LINK
    New-YoutubeDlItem
    Get-YoutubeDlItem
    Set-YoutubeDlItem
    about_ytdlWrapper
     
#>

function Remove-YoutubeDlItem
{
    [Alias("rydl")]
    
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Template")]
        [switch]
        $Template,
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job")]
        [switch]
        $Job,
        
        [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias("Name")]
        [string[]]
        $Names
        
    )
    
    begin
    {
        # Read in the correct list of templates or jobs.
        $objectList = if ($Template)
        {
            Read-Templates
        }
        elseif ($Job)
        {
            Read-Jobs
        }
        
        # Get the correct database path.
        $databasePath = if ($Template)
        {
            $script:TemplateData
        }
        elseif ($Job)
        {
            $script:JobData
        }
    }
    
    process
    {
        # Iterate through all the passed in names.
        foreach ($name in $Names)
        {
            # If the object doesn't exist, warn the user.
            $object = $objectList | Where-Object { $_.Name -eq $name }
            if ($null -eq $object)
            {
                Write-Error "There is no $(if($Template){`"template`"}else{`"job`"}) named: '$name'."
                continue
            }
            
            # Remove the object from the list.
            Write-Verbose "Deleting the youtube-dl $(if($Template){`"template`"}else{`"job`"}) object."
            $objectList.Remove($object) | Out-Null
        }
    }
    
    end
    {
        # Save the modified database.
        if ($PSCmdlet.ShouldProcess("Updating database at '$databasePath' with the changes (deletions).", "Are you sure you want to update the database at '$databasePath' with the changes (deletions)?", "Save File Prompt"))
        {
            Export-Clixml -Path $databasePath -InputObject $objectList -Force -WhatIf:$false `
                -Confirm:$false | Out-Null
        }
    }
}

<#
.SYNOPSIS
    Changes a value of a youtube-dl item.
     
.DESCRIPTION
    The `Set-YoutubeDlItem` cmdlet changes the value of a youtube-dl template
    or job.
     
    This cmdlet can be used to change a template's/job's path of the location
    of the configuration file to use.
     
    This cmdlet can be used to change a value of a variable of a job.
     
    This cmdlet can be used to update a job if the configuration file changes,
    initialising any new variables which have been added since the last time,
    and removing any now-unnecessary variables.
     
.PARAMETER Template
    Indicates that this cmdlet will be changing a youtube-dl template.
     
.PARAMETER Job
    Indicates that this cmdlet will be changing a youtube-dl job.
     
.PARAMETER Name
    Specifies the name of the item to be changed.
     
 [!]Once you specify the '-Template'/'-Job' switch, this parameter will
    autocomplete to valid names for the respective item type.
     
.PARAMETER Path
    Specifies the new path of the location of the configuration file to use.
     
.PARAMETER Variable
    Specifies the name of the variable to change the value of for a job.
     
 [!]Once you specify a valid '-Name' when using the '-Job' switch, this
    parameter will autocomplete to valid names of variables within this job.
     
.PARAMETER Value
    Specifies the new value of the variable being changed.
     
.PARAMETER Update
    Updates the variables of a job to match with what the defined configuration
    file has defined.
     
.PARAMETER WhatIf
    Shows what would happen if the cmdlet runs. The cmdlet does not run.
     
.PARAMETER Confirm
    Prompts you for confirmation before running any state-altering actions
    in this cmdlet.
     
.INPUTS
    System.String
        You can pipe the name of the item to change.
     
.OUTPUTS
    YoutubeDlTemplate
    YoutubeDlJob
     
.NOTES
    When changing a job using the '-Job' switch, a dynamic parameter
    corresponding to each NEW variable definition, found within the
    configuration file, will be generated. The parameter sets the initial
    value of the variable to make the job ready for execution.
     
    For detailed help regarding updating a job, see the
    "CHANGING THE PROPERTIES OF A JOB" section in the help at:
    'about_ytdlWrapper_jobs'.
     
    This cmdlet is aliased by default to 'sydl'.
     
.EXAMPLE
    PS C:\> Set-YoutubeDlItem -Template -Name "music" -Path ~\new\music.conf
     
    Changes the path of the location of the configuration file, for the
    youtube-dl template named "music".
     
.EXAMPLE
    PS C:\> Set-YoutubeDlItem -Job -Name "archive" -Path ~\new\archive.conf
                 
    Changes the path of the location of the configuration file, for the
    youtube-dl job named "archive".
     
.EXAMPLE
    Assuming the job 'archive' has a variable "Autonumber"=5
     
    PS C:\> Set-YoutubeDlItem -Job -Name "archive" -Variable "Autonumber"
                -Value "100"
     
    Changes the "Autonumber" variable of the job named "archive" to the new
    value of "100". The next time the job will be run, this new value will
    be used.
     
.EXAMPLE
    Assuming the job 'archive' has the variables "Autonumber"=5 and
    "Format"=best.
     
    Assuming the configuration file has the variable definitions "Autonumber"
    and "Quality".
     
    PS C:\> Set-YoutubeDlItem -Job -Name "archive" -Update -Quality "normal"
     
    Updates the job named "archive" to reflect its modified configuration file.
    The configuration file has a new variable named "Quality", whose initial
    value is provided through the '-Quality' parameter. The configuration file
    lacks the "Format" variable now, so that is deleted from the job.
     
.LINK
    Get-YoutubeDlItem
    Set-YoutubeDlItem
    Remove-YoutubeDlItem
    about_ytdlWrapper
     
#>

function Set-YoutubeDlItem
{
    [Alias("sydl")]
    
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Template")]
        [switch]
        $Template,
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job-Path")]
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job-Update")]
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job-Property")]
        [switch]
        $Job,
        
        [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $Name,
        
        [Parameter(Position = 2, Mandatory = $true, ParameterSetName = "Template")]
        [Parameter(Position = 2, Mandatory = $true, ParameterSetName = "Job-Path")]
        [Alias("ConfigurationFilePath")]
        [string]
        $Path,
        
        [Parameter(Position = 2, Mandatory = $true, ParameterSetName = "Job-Update")]
        [switch]
        $Update,
        
        [Parameter(Position = 2, Mandatory = $true, ParameterSetName = "Job-Property")]
        [string]
        $Variable,
        
        [Parameter(Position = 3, Mandatory = $true, ParameterSetName = "Job-Property")]
        $Value        
        
    )
    
    dynamicparam
    {
        # Only run the variable detection logic if a job is given, and the job
        # exists, and it has a valid configuration file path, and the '-Update'
        # switch is on.
        if (-not $Job) { return }
        if ($null -eq $Name) { return }
        $jobList = Read-Jobs
        $jobObject = $jobList | Where-Object { $_.Name -eq $Name }
        if ($null -eq $jobObject) { return }
        if ($jobObject.GetState() -eq "InvalidPath") { return }
        if (-not $Update) { return }
        
        # Figure out which are the new variables in the configuration file
        # to add parameters for.
        $jobVariables = $jobObject.GetStoredVariables()
        $configVariables = $jobObject.GetVariables()
        $newVariables = $configVariables | Where-Object { $jobVariables -notcontains $_ }
        
        #Define the dynamic parameter dictionary to add all new parameters to
        $parameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        
        # Now that a list of all new variable definitions is found, create a dynamic parameter for each
        foreach ($variable in $newVariables)
        {
            $paramAttribute = New-Object System.Management.Automation.ParameterAttribute
            $paramAttribute.Mandatory = $true
            $paramAttribute.ParameterSetName = "Job-Update"
            
            $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $attributeCollection.Add($paramAttribute)                
            $param = New-Object System.Management.Automation.RuntimeDefinedParameter($variable, [String], `
                $attributeCollection)
            
            $parameterDictionary.Add($variable, $param)
        }
        
        # Create parameters for every uninitialised variable.
        foreach ($variable in $jobObject.GetNullVariables())
        {
            $paramAttribute = New-Object System.Management.Automation.ParameterAttribute
            $paramAttribute.Mandatory = $true
            $paramAttribute.ParameterSetName = "Job-Update"
            
            $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $attributeCollection.Add($paramAttribute)                
            $param = New-Object System.Management.Automation.RuntimeDefinedParameter($variable, [String], `
                $attributeCollection)
            
            $parameterDictionary.Add($variable, $param)
        }
        
        return $parameterDictionary
    }
    
    process
    {
        if ($Template)
        {
            # If the template doesn't exist, warn the user.
            $templateList = Read-Templates
            $templateObject = $templateList | Where-Object { $_.Name -eq $Name }
            Write-Verbose "Validating parameters and the configuration file."
            if ($null -eq $templateObject)
            {
                Write-Error "There is no template named: '$Name'."
                return
            }
            
            # Validate that the new configuration file exists and can be used.
            if ([YoutubeDlTemplate]::HasInvalidPath($Path))
            {
                Write-Error "The configuration file path: '$Path' is invalid."
                return
            }
            if ([YoutubeDlTemplate]::HasNoInput($Path))
            {
                Write-Error "The configuration file located at: '$Path' has no input definitions.`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_templates`'."
                    return
            }
            
            Write-Verbose "Changing the path property of the template object."
            $templateObject.Path = $Path
            
            if ($PSCmdlet.ShouldProcess("Updating database at '$script:TemplateData' with the changes.", "Are you sure you want to update the database at '$script:TemplateData' with the changes?", "Save File Prompt"))
            {
                Export-Clixml -Path $script:TemplateData -InputObject $templateList -Force -WhatIf:$false `
                    -Confirm:$false | Out-Null
            }
        }
        elseif ($Job -and -not $Update)
        {
            # If the job doesn't exist, warn the user.
            $jobList = Read-Jobs
            $jobObject = $jobList | Where-Object { $_.Name -eq $Name }
            Write-Verbose "Validating parameters and the configuration file."
            if ($null -eq $jobObject)
            {
                Write-Error "There is no job named: '$Name'."
                return
            }
            
            if ($Path)
            {
                if ([YoutubeDlJob]::HasInvalidPath($Path))
                {
                    Write-Error "The configuration file path: '$Path' is invalid."
                    return
                }
                
                Write-Verbose "Changing the path property of the job object."
                $jobObject.Path = $Path
            }
            else
            {
                # Validate that the job can be used.
                if ($jobObject.HasInvalidPath())
                {
                    Write-Error "The configuration file path: '$Path' is invalid."
                    return
                }
                if ($jobObject.HasInputs())
                {
                    Write-Error "The configuration file at: '$Path' has input definitions, which a job cannot have.`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_jobs`'."
                    return
                }
                
                # Validate that the variable-to-modify exists.
                if ($jobObject._Variables.Keys -notcontains $Variable)
                {
                    Write-Error "The job: '$name' does not contain the variable named: '$Variable'!"
                    return
                }
                
                if ([System.String]::IsNullOrWhiteSpace($Value)) 
                {
                    Write-Error "The new value for the variable: '$Variable' cannot be empty!"
                    return
                }
                
                Write-Verbose "Changing the variable property of the job object."
                $jobObject._Variables[$Variable] = $Value
            }
            
            if ($PSCmdlet.ShouldProcess("Updating database at '$script:JobData' with the changes.", "Are you sure you want to update the database at '$script:JobData' with the changes?", "Save File Prompt"))
            {
                Export-Clixml -Path $script:JobData -InputObject $jobList -WhatIf:$false -Confirm:$false `
                    | Out-Null
            }
            
        }
        elseif ($Job -and $Update)
        {
            # If the job doesn't exist, warn the user.
            $jobList = Read-Jobs
            $jobObject = $jobList | Where-Object { $_.Name -eq $Name }
            Write-Verbose "Validating parameters and the configuration file."
            if ($null -eq $jobObject)
            {
                Write-Error "There is no job named: '$Name'."
                return
            }
            
            # Validate that the job can be used.
            if ($jobObject.HasInvalidPath())
            {
                Write-Error "The configuration file path: '$Path' is invalid."
                return
            }
            if ($jobObject.HasInputs())
            {
                Write-Error "The configuration file at: '$Path' has input definitions, which a job cannot have.`nFor help regarding the configuration file, see the `"SETTING UP A CONFIGURATION FILE`" section in the help at: `'about_ytdlWrapper_jobs`'."
                return
            }
            
            # Figure out which are the new variables in the configuration file
            # and which variables in the job (may) need to be removed.
            $jobVariables = $jobObject.GetStoredVariables()
            $configVariables = $jobObject.GetVariables()
            $newVariables = $configVariables | Where-Object { $jobVariables -notcontains $_ }
            $oldVariables = $jobVariables | Where-Object { $configVariables -notcontains $_ }
            
            $variableList = $jobObject._Variables
            # First remove all of the not-needed-anymore variables from the
            # hashtable.
            foreach ($key in $oldVariables)
            {
                $variableList.Remove($key)
            }
            # Then add all of the new variables which need an initial value
            # before the job can be ran.
            foreach ($key in $newVariables)
            {
                if ($PSBoundParameters.ContainsKey($key))
                {
                    $variableList[$key] = $PSBoundParameters[$key]
                }
                else
                {
                    Write-Error "The new variable: '$key' has not been provided an initial value as a parameter!"
                    return
                }
            }
            # Then set the values of any uninitialised variables too.
            foreach ($key in $jobObject.GetNullVariables())
            {
                if ($PSBoundParameters.ContainsKey($key))
                {
                    $variableList[$key] = $PSBoundParameters[$key]
                }
                else
                {
                    Write-Error "The existing variable: '$key' has not been provided an initial value as a parameter!"
                    return
                }
            }
            
            # Set the modified variable hashtable.
            Write-Verbose "Updating the variables of the job object."
            $jobObject._Variables = $variableList
            if ($PSCmdlet.ShouldProcess("Updating database at '$script:JobData' with the changes.", "Are you sure you want to update the database at '$script:JobData' with the changes?", "Save File Prompt"))
            {
                Export-Clixml -Path $script:JobData -InputObject $jobList -WhatIf:$false -Confirm:$false `
                    | Out-Null
            }
        }
    }
}

# Scriptblocks used for tab expansion assignments
$argCompleter_ItemName = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
    # Import all objects from the database file depending on the switch.
    $list = if ($fakeBoundParameters.Template)
    {
        Read-Templates
    }
    elseif ($fakeBoundParameters.Job)
    {
        Read-Jobs
    }
    
    if ($list.Count -eq 0) {
        Write-Output ""
    }
    
    # Return the names which match the currently typed in pattern.
    # This first strips the string of any quotation marks, then matches it to
    # the valid names, and then inserts the quotation marks again.
    # This is necessary so that strings with spaces have quotes, otherwise
    # they will not be treated as one parameter.
    $list.Name | Where-Object { $_ -like "$($wordToComplete.Replace(`"`'`", `"`"))*" } | ForEach-Object { "'$_'" }
    
}

$argCompleter_JobVariable = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
    # Only proceed if specifying a job.
    if ($fakeBoundParameters.Template) { return }
    
    # Get the already typed in job name
    $jobName = $fakeBoundParameters.Name
    
    if ($null -ne $jobName) {
        # Import all youtube-dl.Job objects from the database file
        $jobList = Read-Jobs
        $job = $jobList | Where-Object { $_.Name -eq $jobName }
        
        if ($null -ne $job) {
            # Return the variables which match currently typed in pattern
            $job._Variables.Keys | Where-Object { $_ -like "$($wordToComplete.Replace(`"`'`", `"`"))*" } `
                | ForEach-Object { "'$_'" }
        }
    }
}

# Tab expansion assignements for commands
Register-ArgumentCompleter -CommandName Get-YoutubeDlItem -ParameterName Names -ScriptBlock $argCompleter_ItemName
Register-ArgumentCompleter -CommandName Set-YoutubeDlItem -ParameterName Name -ScriptBlock $argCompleter_ItemName
Register-ArgumentCompleter -CommandName Set-YoutubeDlItem -ParameterName Variable -ScriptBlock $argCompleter_JobVariable
Register-ArgumentCompleter -CommandName Remove-YoutubeDlItem -ParameterName Names -ScriptBlock $argCompleter_ItemName
Register-ArgumentCompleter -CommandName Invoke-YoutubeDL -ParameterName Names -ScriptBlock $argCompleter_ItemName
    #endregion Load compiled code
}

# TEMPLATE DATA MIGRATION
# -----------------------
Write-Debug "Checking for template databse migration"
$templateDatabaseVersion = [Regex]::Match((Get-Item -Path "$Folder\template-database.*.xml" -ErrorAction Ignore), ".*?ytdlWrapper\\template-database.(.*).xml").Groups[1].Value
if ($templateDatabaseVersion -eq "0.2.0")
{
    Write-Debug "`e[4mDetected database version 0.2.0!`e[0m"
    Rename-Item -Path "$Folder\template-database.0.2.0.xml" -NewName "template-database.0.2.1.xml" -Force -WhatIf:$false -Confirm:$false
}

# JOB DATA MIGRATION
# ------------------
Write-Debug "Checking for job database migration"
$jobDatabaseVersion = [Regex]::Match((Get-Item -Path "$Folder\job-database.*.xml" -ErrorAction Ignore), ".*?ytdlWrapper\\job-database.(.*).xml").Groups[1].Value
if ($jobDatabaseVersion -eq "0.2.0")
{
    Write-Debug "`e[4mDetected database version 0.2.0!`e[0m"
    $jobList = New-Object -TypeName System.Collections.Generic.List[YoutubeDlJob]
    $xmlData = Import-Clixml -Path "$Folder\job-database.$jobDatabaseVersion.xml"
    foreach ($item in $xmlData)
    {
        if ($item.pstypenames[0] -eq "Deserialized.YoutubeDlJob")
        {
            $job = [YoutubeDlJob]::new($item.Name, $item.Path, $item._Variables, $null, $null)
            $jobList.Add($job)
        }
    }
    
    Export-Clixml -Path "$Folder\job-database.0.2.1.xml" -InputObject $jobList -WhatIf:$false -Confirm:$false | Out-Null
    Remove-Item -Path "$Folder\job-database.$jobDatabaseVersion.xml" -Force -WhatIf:$false -Confirm:$false | Out-Null
}