ytdlWrapper.psm1

# Create some global variables
$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\ytdlWrapper.psd1").ModuleVersion

$script:DataPath = "$env:APPDATA\Powershell\ytdlWrapper"

if ((Test-Path -Path $script:DataPath) -eq $false) {
    
    # Create the module data storage folders if they don't exist
    New-Item -ItemType Directory -Path "$env:APPDATA" -Name "Powershell" -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path "$env:APPDATA\Powershell" -Name "ytdlWrapper"
    
}

if ($null -eq (Get-Command "youtube-dl.exe" -ErrorAction SilentlyContinue)) {
    
    # Warn the user that youtube-dl.exe cannot be found since without the binary in PATH,
    # the module won't function correctly.
    Write-Warning "Could not find youtube-dl.exe in PATH. Install it or modify the PATH variable."
    
}

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = $global:ModuleDebugDotSource
$script:doDotSource = $true #! Needed to make code coverage tests work
# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = $global:ModuleDebugIndividualFiles

# Resolve-Path function which deals with non-existent paths
function Resolve-Path_i {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]
        $Path # Path to resolve
    )
    
    # Run the command silently
    $resolvedPath = Resolve-Path $Path -ErrorAction SilentlyContinue
    
    # Variable will be null if $Path doesn't exist
    # In that case set it to an empty string
    if ($null -eq $resolvedPath) {
        $resolvedPath = ""
    }
    
    $resolvedPath
}

# If script detects its running from original dev environment, import individually since module won't be compiled
if (Test-Path (Resolve-Path_i -Path "$($script:ModuleRoot)\..\.git")) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }

# Imports a module file, either through dot-sourcing or through invoking the script
function Import-ModuleFile {
    <#
    .SYNOPSIS
        Loads files into the module on module import.
     
    .DESCRIPTION
        This helper function is used during module initialization.
        It should always be dotsourced itself, in order to proper function.
         
        This provides a central location to react to files being imported, if later desired
     
    .PARAMETER Path
        The path to the file to load
     
    .EXAMPLE
        PS C:\> . Import-ModuleFile -File $function.FullName
         
        Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path # Path of module file
    )
    
    # Get the resolved path to avoid any cross-OS issues
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) {
        
        # Load the script through dot-sourcing
        . $resolvedPath
        
    }else {
        
        # Load the script through different method (unknown atm)
        $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) 
        
    }
}

# Load individual files if not compiled
if ($importIndividualFiles) {
    
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) {
                        
        . Import-ModuleFile -Path $function.FullName
        
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) {    
            
        . Import-ModuleFile -Path $function.FullName
        
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End execution here, do not load compiled code below
    return
}

#region Load compiled code
function Get-Jobs {
    <#
    .SYNOPSIS
        Get and return a list of jobs
         
    .DESCRIPTION
        Get and return a list of youtube-dl.Job objects from the database file.
         
    .PARAMETER Path
        The path of the database file.
         
    .EXAMPLE
        PS C:\> $jobList = Get-Job -Path "%appdata%/database.xml"
         
        Populates the array/list with all jobs in the specified database.
         
    .INPUTS
        None
         
    .OUTPUTS
        None
         
    .NOTES
         
         
    #>

    
    [CmdletBinding()]
    param (
        
        [Parameter(Mandatory = $true)]
        [string]
        $Path
        
    )
    
    $jobList = [System.Collections.Generic.List[psobject]]@()
    
    # If the file doesn't exist, then the import logic will error accordingly
    if ((Test-Path -Path $Path) -eq $true) {
        
        # Read the xml data in
        $xmlData = Import-Clixml -Path $Path 
        
        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.PSObject.TypeNames[0] -eq "Deserialized.youtube-dl.Job") {
                
                $job = New-Object -TypeName psobject
                $job.PSObject.TypeNames.Insert(0, "youtube-dl.Job")        
                
                # Copy the properties from the Deserialized object into the new one
                foreach ($property in $item.PSObject.Properties) {
                    
                    # Copy over the deserialised object properties over to new object
                    if ($property.Name -eq "Scriptblock") {
                        
                        # In the case of a scriptblock, create it as a proper scriptblock object so that it doesn't
                        # have to be converted later on, possibly more than once?
                        $job | Add-Member -Type NoteProperty -Name "Scriptblock" -Value ([Scriptblock]::Create($property.Value))
                        
                    }else {
                        
                        $job | Add-Member -Type NoteProperty -Name $property.Name -Value $property.Value
                        
                    }
                    
                }
                
                $jobList.Add($job)
                
            }
            
        }
        
    }
    
    # Return the list as a List object, rather than as an array (by default)
    Write-Output $jobList -NoEnumerate
    
}

function Read-ConfigDefinitions {
    <#
    .SYNOPSIS
        Read definitions from a config file
         
    .DESCRIPTION
        Read in either input definitions, variable definitions, or variable scriptblock declerations
        from a youtube-dl configuration file.
         
    .EXAMPLE
        PS C:\> Read-ConfigDefinitions -Path "~/conf.txt" -InputDefinitions
         
        Reads in and generates a list of all input definition names.
         
    .INPUTS
        None
         
    .OUTPUTS
        System.Collections.Generic.List[System.String]
         
    .NOTES
         
         
    #>

    
    [CmdletBinding()]
    param (
        
        [Parameter(Mandatory = $true)]
        [String]
        $Path,
        
        [Parameter()]
        [switch]
        $InputDefinitions,
        
        [Parameter()]
        [switch]
        $VariableDefinitions,
        
        [Parameter()]
        [switch]
        $VariableScriptblocks
        
    )
    
    # Read in the config file as a single string
    $configFilestream = Get-Content -Path $Path -Raw
    
    $definitionList = [System.Collections.Generic.List[System.String]]@()
    
    if ($InputDefinitions -eq $true) {
        
        # Find all matches to:
        # 1. --some-parameter i@{name} : normal parameter definition
        # 1. -s i@{name} : shorthand parameter definition
        # 2. 'i@{name}' : special case for url, since it doesn't have a flag
        # Also matches even if multiple parameter definitions are on the same line
        $regex1 = [regex]::Matches($configFilestream, "(-(\S+)\s'?i@{(\w+)}'?)\s*")
        $regex2 = [regex]::Matches($configFilestream, "('i@{(\w+)}')")
        
        # Add the descriptor fields to the list
        foreach ($match in $regex1) {
            
            # .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)
            
        }
        
        foreach ($match in $regex2) {
            
            # .Group[1] is the whole match
            # .Group[2] is the 'name' match
            
            $definitionList.Add($match.Groups[2].Value)
            
        }
        
    }else {
        
        # Find all matches to:
        # 1. --some-parameter v@{name}{start{scriptblock}end} : normal 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) {
                
                $definitionList.Add($match.Groups[4].Value)
                
            }
            
        }
        
    }
    
    # Return the list as a List object, rather than as an array (by default)
    Write-Output $definitionList -NoEnumerate
    
}

function Write-Message {
    <#
    .SYNOPSIS
        Writes a message to the screen.
         
    .DESCRIPTION
        Writes a message to the screen, as text, a warning, or an error.
         
    .PARAMETER Message
        The message to print to screen.
         
    .PARAMETER DisplayText
        Writes the message as standard text.
         
    .PARAMETER DisplayWarning
        Writes the message as a warning.
         
    .PARAMETER DisplayError
        Writes the message as an error.
         
    .EXAMPLE
        PS C:\> Write-Message -Message "invalid argument" -DisplayError
         
        Prints the error message to screen by invoking Write-Error.
         
    #>

    [CmdletBinding()]
    Param(
        
        [Parameter(ParameterSetName = "DisplayText", Mandatory = $true)]
        [Parameter(ParameterSetName = "DisplayWarning", Mandatory = $true)]
        [Parameter(ParameterSetName = "DisplayError", Mandatory = $true)]
        [string]
        $Message,
        
        [Parameter(ParameterSetName = "DisplayText", Mandatory = $true)]
        [switch]
        $DisplayText,
        
        [Parameter(ParameterSetName = "DisplayWarning", Mandatory = $true)]
        [switch]
        $DisplayWarning,
        
        [Parameter(ParameterSetName = "DisplayError", Mandatory = $true)]
        [switch]
        $DisplayError
    )
    
    if ($DisplayText -eq $true) {
        
        Write-Host -Message $Message
        
    }elseif ($DisplayWarning -eq $true) {
        
        Write-Warning -Message $Message
        
    }elseif ($DisplayError -eq $true) {
        
        Write-Error -Message $Message
        
    }
    
    
}

function Add-YoutubeDLJob {
    <#
    .SYNOPSIS
        Create a new job definition
         
    .DESCRIPTION
        Add a new youtube-dl job definition to the database, which can be used with the Invoke-YoutubeDL command.
         
    .PARAMETER Name
        The name to call the job.
         
    .PARAMETER ConfigPath
        The filepath pointing to the configuration file.
         
    .PARAMETER Scriptblock
        A scriptblock which will be executed as part of the job once youtube-dl finishes running.
         
    .EXAMPLE
        PS C:\> Add-YoutubeDLJob -Name "test" -ConfigPath ~/conf.txt -Number "123"
         
        Adds a new job under the name "test", pointing to the configuration file specified,
        which contains a variable "Number" and initialises it with the value "123".
         
    .INPUTS
        None
         
    .OUTPUTS
        None
         
    .NOTES
        Once you supply a valid configuration filepath, the function will create parameters at runtime for each
        variable found in the file, so if for example the configuration file has the variables: "number", "url",
        the parameters -Number and -Url will be exposed. To see all the parameters, pressing Ctrl+Tab will show
        the variable parameters at the top of the list.
         
    #>

    
    [CmdletBinding()]
    param (
        
        [Parameter(Position = 0, Mandatory = $true)]
        [string]
        $Name,
        
        [Parameter(Position = 1, Mandatory = $true)]
        [Alias("Path")]
        [string]
        $ConfigPath,
        
        [Parameter(Position = 2, Mandatory = $false)]
        [scriptblock]
        $Scriptblock
        
    )
    
    dynamicparam {
        
        # Only run the logic if the file exists
        if ((Test-Path -Path $ConfigPath) -eq $true) {
            
            # Retrieve all instances of variable definitions in the config file
            $definitionList = Read-ConfigDefinitions -Path $ConfigPath -VariableDefinitions
            
            #Define the dynamic parameter dictionary to add all new parameters to
            $parameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            
            # Now that a list of all variable definitions is found, create a dynamic parameter for each
            foreach ($definition in $definitionList) {
                
                $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
            
        }
        
    }
    
    process {
        
        # Read in the list of job objects
        $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
        
        # Check that the job name isn't already taken
        $job = $jobList | Where-Object { $_.Name -eq $Name }
        if ($null -ne $job) {
            
            Write-Message -Message "There already exists a job called: $Name" -DisplayWarning
            return
            
        }
        
        # Ensure the config file actually exists
        if ((Test-Path -Path $ConfigPath) -eq $false) {
                
            Write-Message -Message "There is no file located at: $ConfigPath" -DisplayWarning
            return
            
        }
        
        # Retrieve all instances of variable definitions in the config file
        $definitionList = Read-ConfigDefinitions -Path $ConfigPath -VariableDefinitions
        
        # Set up the job object
        $job = New-Object -TypeName psobject
        $job.PSObject.TypeNames.Insert(0, "youtube-dl.Job")        
        $job | Add-Member -NotePropertyName "Name" -NotePropertyValue $Name
        $job | Add-Member -NotePropertyName "ConfigPath" -NotePropertyValue $ConfigPath
        
        # Add the user-provided variable initial-values to the job object
        [hashtable]$variableList = [ordered]@{}        
        foreach ($definition in $definitionList) {
            
            $variableList.Add($definition, $PSBoundParameters[$definition])
            
        }
        
        # If a scriptblock has been given in, add it to the job object
        if ($null -ne $Scriptblock) {
            
            $job | Add-Member -NotePropertyName "Scriptblock" -NotePropertyValue $Scriptblock.ToString()
            
        }
        
        $job | Add-Member -NotePropertyName "Variables" -NotePropertyValue $variableList
        $jobList.Add($job)
        
        # Save the newly created job to the database file
        Export-Clixml -Path "$script:DataPath\database.xml" -InputObject $jobList | Out-Null
        
    }
    
    
}


function Get-YoutubeDLJob {
    <#
    .SYNOPSIS
        Get a job definition
         
    .DESCRIPTION
        Return a youtube-dl job definition object. If run as a standalone command, it will write the job
        details to the screen.
         
    .PARAMETER JobName
        The name of the job to retrieve. Accepts multiple names in an array.
         
    .EXAMPLE
        PS C:\> Get-YoutubeDLJob -JobName "test"
         
        Returns the youtube-dl job object for the job named "test".
         
    .EXAMPLE
        PS C:\> "test","test2" | Get-YoutubeDLJob
         
        Returns the youtube-dl job objects for the jobs named "test" and "test2" one after another.
         
    .INPUTS
        System.String[]
         
    .OUTPUTS
        youtube-dl.Job[]
         
    .NOTES
         
         
    #>

    
    [CmdletBinding()]
    param (
        
        # Tab completion
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline)]
        [Alias("Job","Name")]
        [string[]]
        $JobName
        
    )
    
    process {
        
        foreach ($name in $JobName) {
            
            # Read in the list of job objects
            $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
                
            # Check that the job exists
            $job = $jobList | Where-Object { $_.Name -eq $name }
            if ($null -eq $job) {
                
                Write-Message -Message "There is no job called: $name" -DisplayWarning
                return
                
            }
            
            Write-Output $job
            
        }
        
    }
    
}

function Invoke-YoutubeDL {
    <#
    .SYNOPSIS
        Invoke youtube-dl
         
    .DESCRIPTION
        Invoke the youtube-dl process, specifying either an already defined job or a configuration file.
         
    .PARAMETER ConfigPath
        The filepath pointing to the configuration file to use.
         
    .PARAMETER JobName
        The name of the job to run.
         
    .EXAMPLE
        PS C:\> Invoke-YoutubeDL -ConfigPath "~/conf.txt" -Url "//some/url/"
         
        Invokes youtube-dl using the specified configuration path, with has an input definition "Url" that is
        passed in as a parameter.
         
    .EXAMPLE
        PS C:\> Invoke-YoutubeDL -JobName "test"
         
        Invokes youtube-dl using the configuration path specified by the job, and any variables which may be
        defined for this job.
         
    .INPUTS
        None
         
    .OUTPUTS
        None
         
    .NOTES
         
         
    #>

    
    [CmdletBinding()]
    param (
        
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Config")]
        [Alias("Path")]
        [string]
        $ConfigPath,
        
        # Tab completion
        [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Job")]
        [Alias("Job", "Name")]
        [string]
        $JobName
        
    )
    
    dynamicparam {
        
        # Only run the logic if the file exists
        if ($null -ne $ConfigPath -and (Test-Path $ConfigPath) -eq $true) {
            
            # Retrieve all instances of input definitions in the config file
            $definitionList = Read-ConfigDefinitions -Path $ConfigPath -InputDefinitions
            
            #Define the dynamic parameter dictionary to add all new parameters to
            $parameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            
            # Now that a list of all input definitions is found, create a dynamic parameter for each
            foreach ($definition in $definitionList) {
                
                $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
            
        }
        
    }
    
    process {
        
        if ($PSCmdlet.ParameterSetName -eq "Config") {
            
            # Ensure the config file actually exists
            if ((Test-Path -Path $ConfigPath) -eq $false) {
                
                Write-Message -Message "There is no file located at: $ConfigPath" -DisplayWarning
                return
                
            }
            
            $configFileContent = Get-Content -Path $ConfigPath -Raw
            
            # Retrieve all instances of input definitions in the config file
            $definitionList = Read-ConfigDefinitions -Path $ConfigPath -InputDefinitions
            
            foreach ($definition in $definitionList) {
                
                if ($PSBoundParameters.ContainsKey($definition) -eq $true) {
                    
                    # Replace the occurence of the input definition with the user provided value
                    $configFileContent = $configFileContent -replace "i@{$definition}", $PSBoundParameters[$definition]
                    
                }else {
                    
                    # Warn the user and exit if they've not specified one of the input definition parameters
                    Write-Message -Message "You have not supplied the following user input: $definition" -DisplayWarning
                    return
                    
                }
                
            }
            
            # Write modified config file (with user inputs) to a temp file
            # It is easier to read in the config file than edit the existing string to work properly, by surrounding stuff in "" quotes etc
            Out-File -FilePath "$script:DataPath\temp.conf" -Force -InputObject $configFileContent
        
        }
        
        if ($PSCmdlet.ParameterSetName -eq "Job") {
            
            # Retrieve the job and heck that it exists
            $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
            $job = $jobList | Where-Object { $_.Name -eq $JobName }
            
            if ($null -eq $job) {
                
                Write-Message -Message "There is no job called: $JobName" -DisplayWarning
                return
                
            }
            
            # Read in the contents of the job config file
            $configFileContent = Get-Content -Path $job.ConfigPath -Raw
            
            # Retrieve all instances of variable definitions in the config file
            $definitionList = Read-ConfigDefinitions -Path $job.ConfigPath -VariableDefinitions
            
            # Check that the job variables match the configuration file definitions, otherwise there would be errors
            $jobDefinitionList = $job.Variables.Keys
            $difference1 = $jobDefinitionList | Where-Object { $definitionList -notcontains $_ }
            $difference2 = $definitionList | Where-Object { $jobDefinitionList -notcontains $_ }
            if (($null -ne $difference1) -or ($null -ne $difference2)) {
                
                Write-Message -Message "The job variables in the database do not match the variable definitions in the configuration file.
                                        `rRun Set-YoutubeDLJob with the -Update switch to fix the issue. See docs for help."
 -DisplayWarning
                return
                
            }
            
            foreach ($definition in $definitionList) {
                
                # Replace the occurence of the variable definition with the variable value from the database
                $configFileContent = $configFileContent -replace "v@{$definition}{start{(?s)(.*?)}end}", $job.Variables[$definition]
                
            }
            
            # Retrieve all instances of variable scriptblocks in the config file
            $scriptblockDefinitionList = Read-ConfigDefinitions -Path $job.ConfigPath -VariableScriptblocks
            
            # Create a table linking each scriptblock to its respective definition name
            [hashtable]$scriptblockList = [ordered]@{}    
            for ($i = 0; $i -lt $definitionList.Count; $i++) {
                
                $scriptblockList.Add($definitionList[$i], [scriptblock]::Create($scriptblockDefinitionList[$i]))
                
            }
            
            # Write modified config file (with user inputs) to a temp file
            # It is easier to read in the config file than edit the existing string to work properly, by surrounding stuff in "" quotes etc
            Out-File -FilePath "$script:DataPath\temp.conf" -Force -InputObject $configFileContent
            
        }
        
        # Define youtube-dl process information
        $processStartupInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
            FileName = "youtube-dl"
            Arguments = "--config-location `"$script:DataPath\temp.conf`""
            UseShellExecute = $false
        }
        
        # Start and wait for youtube-dl to finish
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $processStartupInfo
        $process.Start()
        
        $process.WaitForExit()
        $process.Dispose()
        
        # Delete the temp config file since its no longer needed
        Remove-Item -Path "$script:DataPath\temp.conf" -Force
        
        # Execute any scriptblocks for variables
        if ($PSCmdlet.ParameterSetName -eq "Job") {
            
            # Run every scriptblock, and store the result back into the databasee
            foreach ($key in $scriptblockList.Keys) {
                
                $returnResult = Invoke-Command -ScriptBlock $scriptblockList[$key]
                
                if ($null -eq $returnResult) {
                    
                    Write-Message -Message "The scriptblock for the $key variable definition didn't return a value. It must return a value." -DisplayError
                    return
                    
                }
                
                $job.Variables[$key] = $returnResult
                
            }
            
            # If the job has a scriptblock, run it
            if ($null -ne $job.Scriptblock) {
                
                Invoke-Command -ScriptBlock $job.Scriptblock -ArgumentList $job
                
            }
            
            # Save the modified job (if any scriptblocks ran) to the database file
            Export-Clixml -Path "$script:DataPath\database.xml" -InputObject $jobList | Out-Null
            
        }
        
        
    }
    
    
}

function Remove-YoutubeDLJob {
    <#
    .SYNOPSIS
        Remove a job definition
         
    .DESCRIPTION
        Remove a youtube-dl job definition from the database.
         
    .PARAMETER JobName
        The name of the job to remove. Accepts multiple names in an array.
         
    .EXAMPLE
        PS C:\> Remove-YoutubeDLJob -JobName "test"
         
        Removes a job called "test" from the database.
         
    .EXAMPLE
        PS C:\> "test","test2" | Remove-YoutubeDLJob
         
        Removes the jobs called "test" and "test2" from the database.
         
    .INPUTS
        System.String[]
         
    .OUTPUTS
        None
         
    .NOTES
         
         
    #>

    
    [CmdletBinding()]
    param (
        
        # Tab completion
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline)]
        [Alias("Job")]
        [string[]]
        $JobName
        
    )
    
    process {
        
        foreach ($name in $JobName) {
            
            # Read in the list of job objects
            $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
                
            # Check that the job exists
            $job = $jobList | Where-Object { $_.Name -eq $name }
            if ($null -eq $job) {
                
                Write-Message -Message "There is no job called: $name" -DisplayWarning
                return
                
            }
            
            $jobList.Remove($job)
            
            # Save the modified database file with the job removed changes
            Export-Clixml -Path "$script:DataPath\database.xml" -InputObject $jobList | Out-Null
            
        }
        
    }
    
}

function Set-YoutubeDLJob {
    <#
    .SYNOPSIS
        Set a property of a job
         
    .DESCRIPTION
        Set a property of a youtube-dl job definition, such as the configurataion filepath, or a variable value.
        This command also allows to sync up the job variables to the definitions in the config file, if the
        configuration file has been modified, i.e. variables added/removed.
         
    .PARAMETER JobName
        The name of the job to configure. Accepts multiple names in an array.
         
    .PARAMETER Variable
        The variable to edit.
         
    .PARAMETER Value
        The new value for the variable.
         
    .PARAMETER ConfigPath
        The new filepath pointing to the configuration file.
         
    .PARAMETER Scriptblock
        The new scriptblock to use post-execution.
         
    .PARAMETER Update
        Sync the job variable definitions to the definitions found in the configuration file. Use this switch
        if the variables have been added/removed from the configuration file.
         
    .EXAMPLE
        PS C:\> Set-YoutubeDLJob -JobName "test" -Variable "number" -Value "123"
         
        Sets the variable "number" to the new value of "123" for the job named "test".
         
    .EXAMPLE
        PS C:\> Set-YoutubeDLJob -JobName "test" -ConfigPath "~/new-config.txt"
         
        Sets the configuration filepath for the job named "test".
         
    .EXAMPLE
        PS C:\> Set-YoutubeDLJob -JobName "test" -Scriptblock $script
         
        Sets the scriptblock for the job named "test" to the scriptblock $script.
         
    .EXAMPLE
        PS C:\> Set-YoutubeDLJob -JobName "test" -Update -NewVariable "value"
         
        Sets the value for the non-initialised variable "NewVariable" to "value", for the job "test".
         
    .INPUTS
        System.String[]
         
    .OUTPUTS
        None
         
    .NOTES
        When using the -Update switch, once a valid job name has been supplied, the function will create
        parameters at runtime for each new (non-initialised) variable found in the configuration file, so if
        for example the configuration file has the new variable: "NewVariable" added to it, the parameter
        -NewVariable will be exposed.To see all the parameters, pressing Ctrl+Tab will show
        the variable parameters at the top of the list. The function will also automatically delete any records
        of variables that are no longer present in the configuration file.
         
    #>

    
    [CmdletBinding()]
    param (
        
        # Tab completion
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = "Variable")]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = "Config")]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = "Scriptblock")]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName, ParameterSetName = "Update")]
        [Alias("Job", "Name")]
        [string]
        $JobName,
        
        # Tab completion once jobname is given
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Variable")]
        [string]
        $Variable,
        
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Config")]
        [string]
        $ConfigPath,
        
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Scriptblock")]
        [scriptblock]
        $Scriptblock,
        
        [Parameter(Position = 1, Mandatory = $true, ParameterSetName = "Update")]
        [switch]
        $Update,
        
        [Parameter(Position = 2, Mandatory = $true, ParameterSetName = "Variable")]
        [string]
        $Value
        
    )
    
    dynamicparam {
        
        # Read in the list of job objects and try to get the job
        $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
        $job = $jobList | Where-Object { $_.Name -eq $JobName }
        
        # Only run if a valid job name has been given and the -Update switch is on
        if (($null -ne $job) -and ($Update -eq $true)) {
            
            # Get a list of definitions stored in the job in the config file
            $jobDefinitions = $job.Variables.Keys
            $configDefinitions = Read-ConfigDefinitions -Path $job.ConfigPath -VariableDefinitions
            
            # Find a list of variables which need to be added
            $variablesToAdd = $configDefinitions | Where-Object { $jobDefinitions -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 $variablesToAdd) {
                
                $paramAttribute = New-Object System.Management.Automation.ParameterAttribute
                $paramAttribute.Mandatory = $true
                $paramAttribute.ParameterSetName = "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 {
        
        # Read in the list of job objects
        $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
            
        # Check that the job exists
        $job = $jobList | Where-Object { $_.Name -eq $JobName }
        if ($null -eq $job) {
            
            Write-Message -Message "There is no job called: $JobName" -DisplayWarning
            return
            
        }
        
        if ($PSCmdlet.ParameterSetName -eq "Variable") {
            
            # Check that the variable is valid and exists
            if ($job.Variables.ContainsKey($Variable) -eq $false) {
                
                Write-Message -Message "There is no variable called: $Variable for the job: $JobName" -DisplayWarning
                return
                
            }
            
            # Set the variable value to the newly specified value
            $job.Variables[$Variable] = $Value
            
        }elseif ($PSCmdlet.ParameterSetName -eq "Config") {
            
            # Set the configuration filepath to the new value
            $job.ConfigPath = $ConfigPath
            
        }elseif ($PSCmdlet.ParameterSetName -eq "Scriptblock") {
            
            # Set the scriptblock to the new one, if there is no previously assigned scriptblock create a new one
            if ($null -ne $job.Scriptblock) {
                
                $job.Scriptblock = $Scriptblock.ToString()
                
            }else {
                
                $job | Add-Member -NotePropertyName "Scriptblock" -NotePropertyValue $Scriptblock.ToString()
                
            }
            
        }elseif ($PSCmdlet.ParameterSetName -eq "Update") {
            
            # Get a list of definitions stored in the job in the config file
            $jobDefinitions = $job.Variables.Keys
            $configDefinitions = Read-ConfigDefinitions -Path $job.ConfigPath -VariableDefinitions
            
            # Find a list of variables which need to be removed and remove them all
            $variablesToRemove = $jobDefinitions | Where-Object { $configDefinitions -notcontains $_ }
            foreach ($variable in $variablesToRemove) {
                
                $job.Variables.Remove($variable)
                
            }
            
            # Find a list of variables which need to be added and add them from the user passed in parameters
            $variablesToAdd = $configDefinitions | Where-Object { $jobDefinitions -notcontains $_ }
            foreach ($variable in $variablesToAdd) {
                
                $job.Variables.Add($variable, $PSBoundParameters[$variable])
                
            }
            
        }
        
        # Save the modified database file with the job changes
        Export-Clixml -Path "$script:DataPath\database.xml" -InputObject $jobList | Out-Null
        
    }
    
}

# Scriptblocks used for tab expansion assignments
$argCompleter_JobName = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
    # Import all youtube-dl.Job objects from the database file
    $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
    
    if ($jobList.Count -eq 0) {
        Write-Output ""
    }
    
    # Return the names which match the currently typed in pattern
    $jobList.Name | Where-Object { $_ -like "$wordToComplete*" }
    
}

$argCompleter_JobVariable = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
    # Get the already typed in job name
    $jobName = $fakeBoundParameters.JobName
    
    if ($null -ne $jobName) {
        
        # Import all youtube-dl.Job objects from the database file
        $jobList = Get-Jobs -Path "$script:DataPath\database.xml"
        
        
        $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*" }
            
        }
        
    }
    
}

# Tab expansion assignements for commands
Register-ArgumentCompleter -CommandName Invoke-YoutubeDL -ParameterName JobName -ScriptBlock $argCompleter_JobName
Register-ArgumentCompleter -CommandName Remove-YoutubeDLJob -ParameterName JobName -ScriptBlock $argCompleter_JobName
Register-ArgumentCompleter -CommandName Get-YoutubeDLJob -ParameterName JobName -ScriptBlock $argCompleter_JobName
Register-ArgumentCompleter -CommandName Set-YoutubeDLJob -ParameterName JobName -ScriptBlock $argCompleter_JobName
Register-ArgumentCompleter -CommandName Set-YoutubeDLJob -ParameterName Variable -ScriptBlock $argCompleter_JobVariable


#endregion Load compiled code