Randomly copy 50 musicfiles from folder with subdirs to another folder

Posted on

QUESTION :

I have a folder structure that contains my music files in the formats mp3 and flac
The folder structure looks like this:

S:Music
     |___ABBA
     |___...
     |___ZZTop

I’m trying to create a Powershell script that selects randomly 50 files from the Music directory (including its subdirectories) and puts it in another folder.

I tried this script but it does not work.

$d = gci "S:Music*.mp3" | resolve-path  |  get-random -count 50
Copy-Item $d  -destination 'S:MusicRandomFIles'
 

It throws this error: Copy-Item : Cannot bind argument to parameter 'Path' because it is null.

ANSWER :

Why the need for | Resolve-Path |?
You need to append switch -File to Get-ChildItem so you are not retrieving DirectoryInfo objects together with FileInfo objects.

Then use the .FullName property on the -Path parameter (which is the first positional parameter that expects a string array)
See Copy-Item

Depending on how large the music collection is, this may take its time to complete

By using a destination folder INSIDE the folder you are recursively iterating for files, you may get exception:
“Copy-Item : Cannot overwrite the item blah.mp3 with itself.”
so to avoid that, you will need to append -ErrorAction SilentlyContinue to the Copy-Item command.

Below copies both .MP3 and .FLAC files:

# loop through the music folders and search for the file patterns you need to copy
$files = Get-ChildItem -Path 'S:Music' -File -Include '*.mp3', '*.flac' -Recurse | Get-Random -Count 50
Copy-Item -Path $files.FullName -Destination 'S:MusicRandomFiles' -Force -ErrorAction SilentlyContinue

However, if you only want to copy say .MP3 files, then note that parameter -Filter works faster than -Include but..

  • -Filter allows only ONE file name pattern like ‘*.mp3’
  • -Include allows for an array of name patterns, but can only be used if the path ends with *, OR when the -Recurse switch is also used
# loop through the music folders and search for the single file pattern you need to copy
$files = Get-ChildItem -Path 'S:Music' -File -Filter '*.mp3' -Recurse | Get-Random -Count 50
Copy-Item -Path $files.FullName -Destination 'S:MusicRandomFiles' -Force -ErrorAction SilentlyContinue

In both cases, make sure the destination path exists.


Edit

As you have discovered, running the first code block several times, can leave you with a total number less than 50 items on each run.
This is because Get-Random -Count 50 can return files that were already in the destination folder and the code above simply copies over them.
Also, for me it is not clear if Get-Random -Count 50 itself does not return duplicates.. So far I haven’t seen that, but it is possible that upon return the 50 items may not always be unique.

To overcome that, below makes sure every file copied is not already to be found in the destination folder and it will always return 50 files without duplicates.

A warning though:
When running over and over, you may end up with an endess loop if there are no more files in the source folder that haven’t already been copied.

$sourceFolder = 'S:Music'
$destination  = 'S:MusicRandomFiles'  # this folder MUST already exist 

# first create a Hashtable with file already in the destination folder (Names only)
$existing = @{}
Get-ChildItem -Path $destination  -File -Filter '*.mp3' | ForEach-Object { $existing[$_.Name] = $true }

# next loop through the music folders and search for the single file pattern you need to copy
# if the destination folder is NOT in the sourcefolder, you can leave out the Where-Object clause
$files = Get-ChildItem -Path $sourceFolder -File -Filter '*.mp3' -Recurse | Where-Object{ -not $_.DirectoryName.StartsWith($destination) }
# make sure you do not exceed the total number of files
$totalFiles = @($files).Count
$maxFiles = [math]::Min($totalFiles, 50)
# start a counted loop
for ($i = 0; $i -lt $maxFiles; $i++) {
    $rnd = Get-Random -Maximum $totalFiles                  # get a random array index from the complete $files collection
    if ($existing.ContainsKey($files[$rnd].Name)) { $i-- }  # file is already present; decrease the counter
    else {
        Write-Host "Copying file no. $($i + 1)/$maxFiles"
        $files[$rnd] | Copy-Item -Destination $destination  # copy the file
        $existing[$files[$rnd].Name] = $true                # and add its name to the Hashtable
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *