2

I am trying to write an automation such that I can given an input as described below and get indicated output and I am using Powershell to do this.

INPUT : Song name in the format given below
    Artist Name - Song Name.mp3

OUTPUT : Rename the song file in the format given below
    Song Name - Artist Name.Mp3

I understand there are OTS tools for this, but I am trying to ddo this using PowerShell as part of bigger solution.

I have single line PowerShell cmdlet to Get items and then rename them. So I tried this:

Get-ChildItem *.mp3 | Rename-Item -NewName {$_.Name.Substring($_.Name.IndexOf("-")+2,  $_.Name.IndexOf(".")) +" - " + $_.Name.Substring(0, ($_.Name.IndexOf("-"))) + ".mp3"}

And here is the problem. The second IndexOf in the substring section gets a null value as it is being used twice in the same (substring) operation twice.

$_.Name.Substring($_.Name.IndexOf("-")+2,  $_.Name.IndexOf("."))

This seems to be a thing with PowerShell. Take below lines for example:

Get-ChildItem *.mp3 | Select-Object {$_.Name.Substring($_.Name.IndexOf("-")+2)}
Get-ChildItem *.mp3 | Select-Object {$_.Name.Substring($_.Name.IndexOf(".mp3"))}

When they are separately, Ps gives an output for each run. However, if I run both cmdlets together, only the first line gives an output, second one returns NULL.

How to get an output for the below logic (I am doing a Select here just to troubleshoot as I do not want to actually rename when troubleshooting)

Get-ChildItem *.mp3 | Select-Object {$_.Name.Substring($_.Name.IndexOf("-")+2, $_.Name.IndexOf(".mp3"))}

I can use variables and join strings, but trying to keep the process in one line if possible without writing custom code/module.

js2010
  • 23,033
  • 6
  • 64
  • 66
theKnight
  • 21
  • 1

4 Answers4

3

The root of your problem is your Substring expression. If you break it out of the pipeline for a moment you'll see there's an error which is being swallowed...

PS> $s = "my artist - my song.mp3"
PS> $s.Substring($s.IndexOf("-") + 2, $s.IndexOf("."))
Exception calling "Substring" with "2" argument(s): "Index and length must refer to a location within the string.
Parameter name: length"
At line:1 char:1
+ $s.Substring($s.IndexOf("-") + 2, $s.IndexOf("."))
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ArgumentOutOfRangeException

The issue is that Substring takes a startIndex and length parameter, but you're trying to give it a startIndex and endIndex. The result is that your new name is an empty string due to the exception being thrown inside the pipeline.

You could fix this with a regex instead - something like:

PS> $s = "my artist - my song.mp3"
PS> $matches = [regex]::Match($s, "(?<artist>.*) - (?<song>.*).mp3")
PS> $newname = $matches.Groups["song"].Value + " - " + $matches.Groups["artist"].Value + ".mp3"
PS> $newname
my song - my artist.mp3

And if you put that back into your pipeline you'd get:

Get-ChildItem *.mp3 | Rename-Item -NewName {
    $matches = [regex]::Match($_.Name, "(?<artist>.*) - (?<song>.*).mp3")
    $matches.Groups["song"].Value + " - " + $matches.Groups["artist"].Value + ".mp3"
}

But have a play with Select-Object before you commit to renaming your entire music library :-)

mclayton
  • 8,025
  • 2
  • 21
  • 26
  • 1
    Great answer. Passing common parameter [`-WhatIf`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters#whatif) to `Rename-Item` would allow previewing the operation directly. – mklement0 Nov 10 '19 at 11:19
  • P.S.: It's only a quibble, because your explanation still applies, but note that delay-bind script blocks (such as the one being passed to `-NewName` here) do _not_ swallow exceptions, they report them and move on to the next input object; it is only script blocks in calculated properties that quietly ignore all errors. – mklement0 Nov 10 '19 at 13:54
1

Let me complement mclayton's helpful answer, which explains the problem with your attempt well and offers an elegant regular-expression-based solution.

As for the title of this question and this statement:

The second IndexOf in the substring section gets a null value as it is being used twice in the same (substring) operation twice.

There is no restriction on how many times you can use $_ in a script block, as the following example demonstrates:

PS> 'one' | ForEach-Object { $_, $_.Substring(1), $_.SubString(2) -join '-' }
one-ne-e

It is technically possibly to modify and therefore also to discard the value of $_, but that should be avoided - as should be modification of all automatic variables.

String methods such as .Substring() fundamentally return a (modified) copy of the input string - they never modify it in place.


To offer a concise, alternative solution based on the -replace operator:

Get-ChildItem *.mp3 | Rename-Item -NewName {
  ($_.BaseName -replace '^(.+?) - (.+)', '$2 - $1') + $_.Extension
} -WhatIf

Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.

mklement0
  • 382,024
  • 64
  • 607
  • 775
0

I was also fruitless when it comes to renaming these files in the manner you are trying.

I was able to get this working with a bit more code. (You can jam it into a single line if you really want to):

Expanded code for ease of reading:
#Set location of your mp3's 
Set-Location "\\FileServer\TestShare"

#Create your array pf MP3s
$mp3s = Get-ChildItem *.mp3

Clear-Host
#loop through the tracks and rename them
foreach($mp3 in $mp3s)
{
   #Display original name
   Write-Host "`nOldName: " $mp3.Name -ForegroundColor Yellow

   #Split the old name into individual pieces
   $split = $mp3.Name.Split("-.")

   #Remove excess spaces from string
   $split = $split -replace (' ','')

   #Create the new name
   $unSplit = "$($split[1]) - $($split[0]).$($split[2])"

   #Display the new name
   Write-Host "NewName: "$unSplit -ForegroundColor Green   

   #Actually Rename the file
   Rename-Item -Path $mp3.Name -NewName $unSplit
}

Output:

OldName:  Farbrikam - MajorRelease1.mp3
NewName:  MajorRelease1 - Farbrikam.mp3

OldName:  Farbrikam - MajorRelease2.mp3
NewName:  MajorRelease2 - Farbrikam.mp3

Jammed 1 liner:

$mp3s = Get-ChildItem *.mp3;foreach($mp3 in $mp3s) {Write-Host "`nOldName: " $mp3.Name -ForegroundColor Yellow;$split = $mp3.Name.Split("-.");$split = $split -replace (' ','');$unSplit = "$($split[1]) - $($split[0]).$($split[2])";Write-Host "NewName: "$unSplit -ForegroundColor Green;Rename-Item -Path $mp3.Name -NewName $unSplit}

You can obviously remove the Write-Host's from this code as I was using them to check the name structure to make sure it was how I wanted it to be.

This code assumes that ALL files will be named in this format: "Artist Name - Song Name.mp3"

Examples:

Korn - Blind.mp3

Rob Zombie - Dragula.mp3

Luinz - I got 5 on it.mp3

Hope this helps

Scab
  • 26
  • 4
  • The OP's only problem was a misconception about how `.Substring()` works. A solution that uses the pipeline with `Rename-Item -NewName { ... }` is not only much more concise, but will also perform better than multiple `Rename-Item` calls in a loop. – mklement0 Nov 10 '19 at 13:31
0

.substring()'s second argument is length, not another index. But I could see the confusion:

'012345'.substring(0,2)
01

But this won't bear out with higher indexes. The error message refers to the length parameter.

'01234'.substring(2,3)
234

'012345'.substring(2,5)
Exception calling "Substring" with "2" argument(s): "Index and length must refer to a location within the string.
Parameter name: length"

You can see the function definition like this, without the parentheses, showing length as the second argument when two arguments are given:

'012345'.substring

OverloadDefinitions

string Substring(int startIndex)
string Substring(int startIndex, int length)

So you could get the length by subtracting the first index from the second index. I also put in IndexOf(" -") at the end to get rid of a space.

Get-ChildItem *.mp3 |
Rename-Item -NewName {$_.Name.Substring($_.Name.IndexOf("-")+2, $_.Name.IndexOf(".") - ($_.Name.IndexOf("-")+2)) +
  " - " +
  $_.Name.Substring(0, ($_.Name.IndexOf(" -"))) + ".mp3"} -whatif


What if: Performing the operation "Rename File" on target "Item: /Users/js/foo/artist - song.mp3 Destination: /Users/js/foo/song - artist.mp3".
What if: Performing the operation "Rename File" on target "Item: /Users/js/foo/my artist - my song.mp3 Destination: /Users/js/foo/my song - my artist.mp3".

Here's another approach, splitting on the ' - ':

Get-ChildItem *.mp3 |
rename-item -newname { $artist,$song = $_.basename -split ' - ';
                       "$song - $artist.mp3" } -whatif

Either solution can be on one line. Take off the -whatif, if it looks good.

js2010
  • 23,033
  • 6
  • 64
  • 66