Skip to content

Instantly share code, notes, and snippets.

@sharpninja
Last active May 3, 2023 02:24
Show Gist options
  • Save sharpninja/c4a1e2206f8cf733097a0f5cdb0ef9bc to your computer and use it in GitHub Desktop.
Save sharpninja/c4a1e2206f8cf733097a0f5cdb0ef9bc to your computer and use it in GitHub Desktop.
Moves all videos from a source folder to a destination folder.
#! pwsh
param (
[Parameter()]
[string[]]
$SourcePaths = @($PWD),
[Parameter()]
[string]
$DestinationPath = $null,
[Parameter()]
[string[]]
$MatchPatterns = @('\.(mp4|m4v|m4p)', `
'\.(mpg|mp2|mpeg|mpv|mpe|m2v)', `
'\.(3gp|3g2)', '\.mkv', '\.webm', `
'\.(flv|f4v|f4p|f4a|f4b)', `
'\.vob', `
'\.ogv', `
'\.avi', `
'\.(mts|m2ts)', '\.(mov|qt)', '\.wmv', '\.asf'),
[Parameter()]
[switch]$WhatIf = $false,
[switch]$Debugging = $false,
[switch]$Undo = $false
)
$SourcePathsString = $([string]::Join('`, `', $SourcePaths));
if ($Undo -eq $true) {
$sp = $DestinationPath;
$DestinationPath = $SourcePaths[0];
$SourcePaths = @($sp);
}
$Verbose = $true;
$Debug = $Debugging;
class Result {
[Match[]]$Matches;
[Int32]$Processed;
Result(
[Match[]]$m,
[Int32]$p
) {
$this.Matches = $m;
$this.Processed = $p;
}
}
class Match {
[string]$Pattern;
[string]$RelativePath;
[System.IO.FileInfo]$Item;
Match(
[System.IO.FileInfo]$i,
[string]$p,
[System.IO.DirectoryInfo]$s
) {
$this.Item = $i;
$this.Pattern = $p;
$this.RelativePath = $i.FullName.Replace("$($s.FullName)", '').TrimStart("\/".ToCharArray());
}
[string]AsJson() {
return "{ '$($this.Pattern)': '$($this.RelativePath)' }"
}
}
function Write-Hashtable {
param(
[Match[]]$matched
)
foreach ($result in $matched) {
if ($result -is [Match[]]) {
Write-Matches $result;
}
else {
Write-Information $result.AsJson();
}
}
}
$Source = Get-Item $SourcePaths[0] -ErrorAction SilentlyContinue;
if ($DestinationPath) {
if (-not(Test-Path $DestinationPath)) {
New-Item $DestinationPath -ItemType Directory
}
$Destination = Get-Item $DestinationPath -ErrorAction Stop;
}
$Patterns = $MatchPatterns;
Push-Location
Set-Location $Source
Write-Debug -Debug:$Debug "Debug output enabled."
Write-Verbose -Verbose:$Verbose "Verbose output enabled."
if (($Debug -eq $true) -and ($Verbose -eq $true)) {
Write-Verbose -Verbose:$Verbose "```$Source`` = $Source"
Write-Verbose -Verbose:$Verbose "```$Destination`` = $Destination"
Write-Verbose -Verbose:$Verbose "```$Patterns`` = [$([string]::Join('], [', $Patterns))]"
}
$activity = "Video-Consolidator";
[Int32]$index = 0.0;
[Int32]$completed = 0;
[System.IO.FileSystemInfo[]]$Paths = @();
foreach ($SourcePath in $SourcePaths) {
Set-Location $SourcePath
Write-Verbose -Verbose:$Verbose "Counting Children of ``$SourcePath``"
[System.IO.FileSystemInfo[]]$PathInfos = Get-ChildItem -Recurse -Exclude $DestinationPath -Verbose:$Verbose;
Write-Verbose -Verbose:$Verbose "Expecting $($PathInfos.Length) children in ``$SourcePath`` to be processed."
$Paths += $PathInfos;
}
[Int32]$count = $Paths.Length;
function Get-ByMatchPatterns {
[OutputType([Result])]
param (
[Parameter()]
[System.IO.FileSystemInfo[]]
$Paths = @($Source),
[Int32]$StartIndex = $index,
[switch]$WhatIf = $false
)
try {
[Int32]$CurrentIndex = $StartIndex;
[Result]$results = [Result]::new(@(), $StartIndex);
$Paths | ForEach-Object -Process {
[System.IO.FileSystemInfo]$Path = $_;
[string]$status = "$($Path ?? '<<null>>')";
try {
if ($null -ne $Path) {
$PathItem = $Path
[string]$fullName = ($PathItem?.FullName) ?? "$PathItem";
[string]$relPath = $fullName.Replace("$($Source.FullName)", '').TrimStart("\/".ToCharArray());
if ($PathItem -is [System.IO.DirectoryInfo]) {
if ($Verbose -eq $true -and ($Debug -eq $true)) {
$status = "[$completed % : $CurrentIndex] ``$relPath`` is a directory.";
Write-Debug -Debug:$Debug $status
}
}
elseif ($PathItem -is [System.IO.FileInfo] -and (-not $PathItem.LinkTarget)) {
# Is a file.
if ($Verbose -eq $true -and ($Debug -eq $true)) {
$status = "[$completed % : $CurrentIndex] ``$relPath`` is a file.";
Write-Debug -Debug:$Debug $status
}
foreach ($Pattern in $Patterns) {
$Pattern="$Pattern\b";
if ($PathItem.FullName -imatch $Pattern) {
$results.Matches += [Match]::new($PathItem, $Pattern, $Source);
if ($Verbose -eq $true -and ($Debug -eq $true)) {
Write-Verbose -Verbose:$Verbose "[$completed % : $CurrentIndex] ``$($PathItem.FullName)`` matches on ``$Pattern``"
}
}
}
}
else {
# Other FileSystemInfo
if ($Verbose -eq $true -and ($Debug -eq $true)) {
$status = "[$completed % : $CurrentIndex] ``$relPath`` is a ``$($PathItem.GetType().Name)``.";
Write-Debug -Debug:$Debug $status
}
}
}
}
catch {
$status = "[$completed % : $CurrentIndex] Error: $_"
if ($Verbose -eq $true -and ($Debug -eq $true)) {
Write-Debug -Debug:$Debug $status
}
}
finally {
[Int32]$CurrentIndex = [Int32]$CurrentIndex + [Int32]1.0;
[Int32]$completed = ([decimal]$CurrentIndex / [decimal]$count) * [decimal]100.0;
$status = "Processed $CurrentIndex of $count in ``$SourcePathsString``"
Write-Progress -CurrentOperation "Find Matches" `
-Status $status `
-Activity $activity `
-PercentComplete $completed;
}
}
$results.Processed = $CurrentIndex - $StartIndex;
if ($Debug -eq $true) {
if ($results.Matches.Length -eq 0) {
Write-Debug -Debug:$Debug "No matches in ``$SourcePathsString``"
}
else {
Write-Hashtable $results.Matches
}
}
if ($results -is [Result]) {
$results;
}
else {
$resultType = $result.GetType().FullName;
throw "Expected `$results to be [Result], but `$results is [$resultType]";
}
}
finally {
Write-Progress -CurrentOperation "Find Matches" `
-Status "Completed searching in $($PathInfos.Length) paths." `
-Activity $activity `
-PercentComplete 100;
}
}
function Move-Videos {
[OutputType([Match[]])]
param(
[Match[]]$AllResults = $null,
[switch]$WhatIf = $false
)
Write-Verbose -Verbose:$Verbose "Found $($AllResults.Length) Matches in ``$SourcePathsString``."
if ($Verbose -eq $true) {
$AllResults | Format-Table -AutoSize -GroupBy Pattern -Verbose:$Verbose
}
if ($Destination -and ($AllResults.Length -gt 0)) {
$SourceItems = $AllResults;
$CopiedFiles = @();
Write-Information "Copying $($SourceItems.Length) to ``$Destination``"
$copied = $SourceItems | ForEach-Object -Process {
$toCopy = $_.RelativePath;
$SourcePath=$_.Item.FullName.Replace($toCopy,'');
$sourceFile = $_.Item.FullName;
$destinationFile = "$DestinationPath/$toCopy";
$retryCount = 0;
Set-Location $SourcePath;
if (Test-Path $destinationFile) {
$existing = Get-Item $destinationFile;
if ($existing.LinkTarget) {
Remove-Item $existing -Force -Verbose:$Verbose;
Write-Verbose -Verbose:$Verbose "Removed existing link at $($existing.FullName)";
}
}
while (Test-Path $destinationFile) {
$destinationFile = "$DestinationPath/$toCopy (++$retryCount)";
}
$destinationFolder = $destinationFile.Replace((Split-Path $destinationFile -Leaf), "");
Write-Verbose -Verbose:$Verbose "`$destinationFolder: $destinationFolder";
if (-not(Test-Path $destinationFolder)) {
New-Item $destinationFolder -ItemType Directory -Verbose:$Verbose -WhatIf:$WhatIf;
}
Move-Item $sourceFile -Destination $destinationFile -Verbose:$Verbose -WhatIf:$WhatIf -ErrorAction Stop -Force;
$VideoPath = $sourceFile;
$TargetPath = $destinationFile;
New-Item -Path $VideoPath -ItemType SymbolicLink -Value $TargetPath -Verbose:$Verbose -WhatIf:$WhatIf
if ($WhatIf -ne $true) {
$copiedFile = Get-Item $destinationFile -Verbose:$Verbose -ErrorAction Stop;
if ($copiedFile) {
$copiedFile
$CopiedFiles += [Match]::new($copiedFile, $copiedFile.Extension, $copiedFile.Directory);
$status = "Moved ``$($copiedFile.Name)`` and created link at ``$TargetPath``"
Write-Progress -Activity $activity `
-Status $status `
-CurrentOperation "Create Links" `
-Completed $completed;
}
else {
Push-Location;
Set-Location $Destination
foreach ($match in $CopiedFiles) {
$sf = Get-Item $match.Item;
if ($sf.Exists -and ($sf -is [System.IO.SymbolicLink])) {
Remove-Item $sf -Force -Verbose:$Verbose;
Write-Debug -Debug:$Debug "Removed link at ``$($sf.FullName)``";
}
$sf = Get-Item $match.Item;
if (-not($sf.Exists)) {
Move-Item $match.RelativePath -Destination $sf.FullName -ErrorAction Stop -Verbose:$Verbose;
Write-Debug -Debug:$Debug "Moved ``$($match.RelativePath)`` back to ``$($sf.FullName)``";
}
else {
Write-Debug -Debug:$Debug "Skipping ``$($sf.FullName)`` because it is still present.";
}
}
Pop-Location;
throw "``$destinationFile`` was not created and moved files were restored."
}
}
else {
Get-Item $sourceFile;
}
}
if (-not $copied) {
throw "No videos were copied.";
}
Set-Location $Destination
$DestinationItems = ($SourceItems.RelativePath | Get-ChildItem -ErrorAction SilentlyContinue)?.FullName
$lengthsMatch = ($DestinationItems.Length -eq $SourceItems.Length);
if (-not($WhatIf) -and $lengthsMatch) {
Write-Information "Linked $($SourceItems.Length) Paths to Destination Videos."
}
else {
throw "Expected ``$DestinationPath`` to contain $($SourceItems.Length) videos, but $($DestinationItems.Length) were found.";
}
}
if ($Destination) {
$status = "Completed Moving and Linking $($AllResults.Length) Videos from ``$SourcePathsString`` to ``$DestinationPath``";
Write-Host $status
}
if ($Debug -eq $true) {
}
else {
$AllResults
}
}
try {
[Result]$r = (Get-ByMatchPatterns -Paths $Paths -StartIndex $index -Verbose:$Verbose -WhatIf:$WhatIf)[0];
Move-Videos -AllResults $r.Matches;
}
catch {
$err = $_;
Write-Error $err;
Write-Error $err.ScriptStackTrace;
}
finally {
Write-Progress -Activity $activity `
-Status "Finished" `
-CurrentOperation "Create Links" `
-Completed 100;
Pop-Location
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment