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 } |