Tuesday, August 9, 2016

Renaming Photos Based on Date Taken

I have digital photos coming from multiple sources: my DSLR camera, my phone, my wife's phone, etc. Each comes with its own naming scheme. What I really want is a way to organize the photos based on when they are taken--especially if my wife and I were taking photos on the same day, so we can see the photos in the same order as an event happened.
Here's an example of some photos from various sources. Note the variation of file names. Also note the highlighted file that has no date taken set. Notice what happens later when I try to rename that file. What you would need to do is manually set the date taken on that file to avoid the issue, but I'm trying to illustrate the point.
So, I set out to write a script that would rename all photos based on the date taken.
It turns out that it's not hard to do via PowerShell. If you have .NET 4.0 you can use the System.Drawing assembly to inspect each photo to see its date taken property.
Here's how to call my script and its results. First, you can specify all the parameters if you like.
Notice the warning about the one image with no date taken property. The resulting files are:
 
You can also call the script and take the defaults. Naturally, the directory is required. If you don't provide, the script will prompt as it's a required parameter.
The images would then be named:

Here's the code that I came up with. The comments should explain everything that is happening and where you can/should modify.


#code written by Crazy-S Farm guy
#http://powershelltidbits.blogspot.com
#use at your own risk

Param
(
    [Parameter(Mandatory=$true)]
    [string] $Directory,
    [Parameter()]
    [string[]] $Extensions,
    [Parameter()]
    [string] $FileNameStart = "IMG"
)


#these are the valid file extensions to be considered as images
$validPhotoExtensions = @(".jpg",".gif",".jpeg",".png")

#use this assembly to look for date taken on photos
#you may need to change if your DLL is located somewhere else
[reflection.assembly]::LoadFile("C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Drawing.dll") | Out-Null

#here's the format of the files after renaming
#it starts with the $FileNameStart (optional parameter), which will be IMG unless you specify otherwise
#here's an example:
#IMG-2016-04-19-200013794.jpg
#note that the last 3 digits are milliseconds and not part of this date format variable, read below for explanation
$imageDateRenameFormat = "yyyy-MM-dd-HHmmss"


#if you don't specify a set of file exensions to look for, it will default to all image type extensions
if (($Extensions -eq $null) -or ($Extensions.Count -eq 0))
{
    $Extensions = @(".jpg",".gif",".jpeg",".png")
}

#get all the files in the specified directory
$files = Get-ChildItem -Path $Directory
foreach ($file in $files)
{
    #make sure the file extension is part of what you asked to process
    if ($Extensions -contains ($file.Extension))
        {
            #figure out what mod date
            [datetime]$modDate = [datetime]$file.LastWriteTime
            Write-Verbose "file $($file.Name) mod date: $modDate"
            try  
            {
                #if not a photo type, will keep using modification date (lastwritetime)
                if ($validPhotoExtensions -contains ($file.Extension))
                {
                    Write-Verbose -Message "file is image type, trying to determing date taken"
                    $imgData = New-Object System.Drawing.Bitmap($file.FullName)
                    if ($imgData -ne $null)
                    {
                        [byte[]]$ImgBytes = $null
                        # Gets 'Date Taken' in bytes
                        try
                        {
                            $ImgBytes = $ImgData.GetPropertyItem(36867).Value
                        }
                        catch
                        {
                            #that property doesn't exist.
                            Write-Warning -Message ("file: " + $file.Name + " has no date taken property set")
                            #therefore, will continue to use last modified date
                        }

                        if ($ImgBytes -ne $null)
                        {
                            # Gets the date and time from bytes 
                            [string]$dateString = [System.Text.Encoding]::ASCII.GetString($ImgBytes)
                            Write-Verbose -Message "date string for image taken date: $dateString"
                            # Formats the date to the desired format
                            $modDate = [datetime]::ParseExact($dateString,"yyyy:MM:dd HH:mm:ss`0",$Null)
                        }
                    }
                }
                else
                {
                    Write-Verbose -Message "file is not image type. will use modified date"
                }
            }
            catch  
            {
                Write-Warning -Message ("ERROR reading file " + $file.Name + " -- " + $Error[0] ) 
            }
            finally
            {
                if ($imgData -ne $null)
                {
                    $ImgData.Dispose() 
                }
            }


            [string]$dateTaken = $modDate.ToString($imageDateRenameFormat)
            #okay... there's the dumb part... the cameras dont give me any milliseconds. and... occasionally I have pictures sometimes
            #taken at the same second. so... here's a trick. instead of having to analyze and add _1 or _2 to files,
            #let's just take the milliseconds from the creation time and stick them here. there is rarely a collision there
            $createDate = $file.CreationTime
            #here's where we are adding the 3 millisecond digits to the new file name.
            $dateTaken += ($createDate.ToString("fff"))
            #build the final new file name. start with the leading characters (IMG unless you change)
            #next the dateTaken formatted as above + 3 millisecond value from the creation date
            #lastly the same extension as the original file 
            $newFileName = ($FileNameStart + "-" + $dateTaken + $file.Extension)
            #now, make sure that the file doesn't already exist in the directory
            #you could easily make this write the files into a different directory and change the move to a copy
            #however, you should still ensure that the file doesn't exist there, in case you happen to have two images
            #that are going to collide
            if (Test-Path -Path ($Directory + "\" + $newFileName))
            {
                Write-Warning -Message ("Unable to rename " + $file.Name + " File $newFileName already exists in directory!")
            }
            else
            {
                Move-Item -Path ($Directory + "\" + $file) -Destination ($Directory + "\" + $newFileName)
            }
        }
        else
        {
            Write-Warning -Message ("File " + $file.Name + " not valid type")
        }
}

Sunday, June 5, 2016

Viewing MP4 Video Tags via Powershell

In my earlier post, I described my technique to easily tag TV Show MP4 files via a PowerShell script. But how do you validate that it worked? Most of this tagged information can't be seen when viewing the detailed file properties in Windows Explorer. (I think Year and Genre are all you can see).
So, I wanted a quick way to view the information was successfully set before importing to iTunes. Here's what I came up with.
There are 2 scripts, actually. One displays a table of information (but not description or long description) and the other displays a long list of all data for each file.
First, please note this leverages Taglib-Sharp. It's free, so don't worry. Simply follow the link to download "tarballs" and get the windows zip. Extract taglib-sharp.dll from the Libraries directory and place somewhere on your machine. Point the import statement in my code to that dll.
Each script when executed will prompt for the directory location of your MP4 files.

#modify the path below to point to the taglib-sharp dll file
Import-Module "D:\powershell\modules\MPTag\taglib-sharp.dll"

#code written by Crazy-S Farm guy
#http://powershelltidbits.blogspot.com
#use at your own risk

$BOXTYPE_TVEN = "tven"; # episode name
$BOXTYPE_TVES = "tves"; # episode number
$BOXTYPE_TVSH = "tvsh"; # TV Show or series
$BOXTYPE_PURD = "purd"; # purchase date
$BOXTYPE_DESC = "desc"; # short description
$BOXTYPE_TVSN = "tvsn"; # season
#now, we have to set up to set the broadcast (air) date
$c = [char]169

#you would think you could do this... but it doesn't work...
#$BOXTYPE_AIRDATE_STRING = $c + "day"; # broadcast date
#this is a little bit of "magic" since for some reason, it wants to stick "194" character in front of it.
# there may be a better way to do this, but I can't figure it out.
$BOXTYPE_BROADCASTDATE = [TagLib.ByteVector]::New()
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromUShort(169)[1])
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("d"))
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("a"))
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("y"))

$BOXTYPE_EPISODE_TITLE = [TagLib.ByteVector]::New()
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromUShort(169)[1])
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromString("n"))
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromString("a"))
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromString("m"))


$mediaDirectory = Read-Host -Prompt "Enter directory to scan"


$fileList = Get-ChildItem -Path $mediaDirectory -Filter "*.mp4"

$output = @()

foreach ($file in $fileList)
{
    $mediafile = [TagLib.File]::Create($file.FullName)
    [TagLib.Mpeg4.AppleTag]$customTag = $mediaFile.GetTag([TagLib.TagTypes]::Apple, 1)
    $rowData = New-Object -TypeName PSObject

    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "File" -Value ($file.Name)

    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "TV Show" -Value ($customTag.DataBoxes($BOXTYPE_TVSH).Text)
    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "Title" -Value ($customTag.DataBoxes($BOXTYPE_EPISODE_TITLE).Text)
    #the tven is used for sorting purposes
    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "Sort" -Value ($customTag.DataBoxes($BOXTYPE_TVEN).Text)

    $number = ""
    $array = $customTag.DataBoxes($BOXTYPE_TVSN)
    if (($array -ne $null) -and ($array.Data.count -ge 3))
    {
        $number = $array.Data[3].toString()
    }
    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "Season" -Value $number

    $number = ""
    $array = $customTag.DataBoxes($BOXTYPE_TVES)
    if (($array -ne $null) -and ($array.Data.count -ge 3))
    {
        $number = $array.Data[3].toString()
    }
    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "#" -Value $number

    $airDate = $customTag.DataBoxes($BOXTYPE_BROADCASTDATE).Text
    if (($airDate -ne $null) -and ($airDate.length -gt 10))
    {
        $airDate = $airDate.substring(0,10)
    }
    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "Air Date" -Value ($airDate)
    Add-Member -InputObject $rowData -MemberType NoteProperty -Name "Short Description" -Value ($customTag.DataBoxes($BOXTYPE_DESC).Text)

    $output += $rowData
}

$output | Format-Table -AutoSize

Here's a sample of the output:
sample table output


#modify the path below to point to the taglib-sharp dll file
Import-Module "D:\powershell\modules\MPTag\taglib-sharp.dll"

#code written by Crazy-S Farm guy
#http://powershelltidbits.blogspot.com
#use at your own risk

$BOXTYPE_LDES = "ldes"; # long description
$BOXTYPE_TVEN = "tven"; # episode name
$BOXTYPE_TVES = "tves"; # episode number
$BOXTYPE_TVSH = "tvsh"; # TV Show or series
$BOXTYPE_PURD = "purd"; # purchase date
$BOXTYPE_DESC = "desc"; # short description
$BOXTYPE_TVSN = "tvsn"; # season
#now, we have to set up to set the broadcast (air) date
$c = [char]169

#you would think you could do this... but it doesn't work...
#$BOXTYPE_AIRDATE_STRING = $c + "day"; # broadcast date
#this is a little bit of "magic" since for some reason, it wants to stick "194" character in front of it.
# there may be a better way to do this, but I can't figure it out.
$BOXTYPE_BROADCASTDATE = [TagLib.ByteVector]::New()
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromUShort(169)[1])
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("d"))
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("a"))
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("y"))

$BOXTYPE_EPISODE_TITLE = [TagLib.ByteVector]::New()
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromUShort(169)[1])
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromString("n"))
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromString("a"))
$BOXTYPE_EPISODE_TITLE.add([TagLib.ByteVector]::FromString("m"))


$mediaDirectory = Read-Host -Prompt "Enter directory to scan"


$fileList = Get-ChildItem -Path $mediaDirectory -Filter "*.mp4"

$output = @()

foreach ($file in $fileList)
{
        
    $mediafile = [TagLib.File]::Create($file.FullName)
    [TagLib.Mpeg4.AppleTag]$customTag = $mediaFile.GetTag([TagLib.TagTypes]::Apple, 1)

    Write-Output ("File: " + $file.Name)
    Write-Output ("TV Show: " + $customTag.DataBoxes($BOXTYPE_TVSH).Text)
    Write-Output ("Episode Title: " + $customTag.DataBoxes($BOXTYPE_EPISODE_TITLE).Text)
    Write-Output ("Sort Title: " + $customTag.DataBoxes($BOXTYPE_TVEN).Text)

    $number = ""
    $array = $customTag.DataBoxes($BOXTYPE_TVSN)
    if (($array -ne $null) -and ($array.Data.count -ge 3))
    {
        $number = $array.Data[3].toString()
    }
    Write-Output ("Season: " + $number)

    #Write-Output ("Season: " + (($customTag.DataBoxes($BOXTYPE_TVSN)).Data)[3].ToString())

    $number = ""
    $array = $customTag.DataBoxes($BOXTYPE_TVES)
    if (($array -ne $null) -and ($array.Data.count -ge 3))
    {
        $number = $array.Data[3].toString()
    }

    Write-Output ("Episode Number: " + $number)
    #Write-Output ("Episode Number: " + (($customTag.DataBoxes($BOXTYPE_TVES)).Data)[3].ToString())

    $airDate = $customTag.DataBoxes($BOXTYPE_BROADCASTDATE).Text
    if (($airDate -ne $null) -and ($airDate.length -gt 10))
    {
        $airDate = $airDate.substring(0,10)
    }

    Write-Output ("Air Date: " + $airDate)
    Write-Output ("Short Description: " + $customTag.DataBoxes($BOXTYPE_DESC).Text)
    Write-Output ("Long Description: " + $customTag.DataBoxes($BOXTYPE_LDES).Text)
    Write-Output "--------------------------------------------------"

}



Here's a sample output:
sample details output

MP4 TV Show file tagging with PowerShell

I grew frustrated with not being able to easily modify the properties of a my ripped TV Show MP4 file before importing to iTunes. So, I figured a way to use PowerShell to help me quickly tag MP4 files before importing. Let me share the solution.
First, if you've ever tried importing an MP4 file to iTunes, it always assumes it is a Home Movie, even if you want it treated as a TV Show.
Adding file to iTunes
file shown as Home Movie
You can alter the file properties and make it a TV Show, but that's just one more step. Also, for me, with all the files named in a structured format, it was just natural to try to bulk modify the file tags rather than do all the modifications in iTunes.
Now, once the file is imported, it is properly recognized as a TV Show.
file recognized as TV Show
iTunes will also recognize all the data set as well.
TV Show details recognized by iTunes
So, here's the solution.
First, it does leverage Taglib-Sharp. It's free, so don't worry. Simply follow the link to download "tarballs" and get the windows zip. Extract taglib-sharp.dll from the Libraries directory and place somewhere on your machine. Point the import statement in my code to that dll.
Second, it is assuming a very specific naming structure for the files (if you want to use this solution, either name your files to match, or tweak the script to match your naming). The format is:  TV Show-episode number-episode title.mp4. so, an example would be: Eureka-02-Many Happy Returns.mp4
Third, I group each files into a folder for each season. So, in for the TV Show Eureka, I have the folder for season 1 containing episodes 1-12, folder for season 2 containing episodes 13-25, etc. Once the files are all tagged, I put them into a single folder for the show and import to iTunes. The reason for doing the folders by episode is for the "sort title" used in iTunes. I want that to be Season Number + # in that Season - Episode Title. So, an example is that episode 70 (in the file Eureka-70-Worst Case Scenario.mp4) is the 6th episode for season 5. so the sort title becomes 506-Worst Case Scenario.

The code below is a script that will:
  1. prompt for the directory containing the files to be tagged. see above how I group by season.
  2. prompt for the season number. I could have read it from the folder structure, but I wanted it to be more flexible.
  3. loop through all files in the specified folder. for each it will prompt for:
  • Fixed title which is optional (Windows doesn't let you have some characters in the title, like ? and ! and some show titles have those)
  • Episode synopsis
  • Broadcast (air) date
As you enter the information for each file, it will tag the file appropriately. I believe the code should be commented well enough to understand how it uses each piece of information. If not, post a comment and I will elaborate. Please make sure you look at the code which can display the information stored for each file.

Here's the code.

#modify the path below to point to the taglib-sharp dll file
Import-Module "D:\powershell\modules\MPTag\taglib-sharp.dll"

#code written by Crazy-S Farm guy
#http://powershelltidbits.blogspot.com
#use at your own risk

#here are some things to keep in mind as this is written:
#expect each season is in a separate directory
#here's what file name we expect: Eureka-02-Many Happy Returns.mp4
#that's:
#TV Show-episode number-episode title.mp4
#the episode number is expected to be an increasing number, not resetting each season
#so, in my example, in my season 4 folder, I have a file: Eureka-47-The Story of O2.mp4
#its episode number (tves) will be 47, but episode name (tven) will be set to 404-The Story of O2 (4th in season 4)


#hard coding the broadcast time... you could set if you like, but iTunes only shows the date portion anyway
$TIME_STRING = "T9:00:00 PMZ"

#here are the "magic codes" to know where apple tag stores the metadata for each file.
$BOXTYPE_TVEN = "tven"; # episode name
$BOXTYPE_TVES = "tves"; # episode number
$BOXTYPE_TVSH = "tvsh"; # TV Show or series
$BOXTYPE_PURD = "purd"; # purchase date
$BOXTYPE_DESC = "desc"; # short description - max is 255 characters
$BOXTYPE_TVSN = "tvsn"; # season
$BOXTYPE_STIK = "stik"; # "magic" to make it realize it's a TV show
$BOXTYPE_AIRDATE = "@day"; # season
$DATATYPE_DATA = 0
$DATATYPE_TEXT = 1
#the magic value to make iTunes recognize as a TV Show
$c_TvShowType = 10

#now, we have to set up to set the broadcast (air) date
$c = [char]169

#you would think you could do this... but it doesn't work...
#$BOXTYPE_AIRDATE_STRING = $c + "day"; # broadcast date
#this is a little bit of "magic" since for some reason, it wants to stick "194" character in front of it.
# there may be a better way to do this, but I can't figure it out.
$BOXTYPE_BROADCASTDATE = [TagLib.ByteVector]::New()
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromUShort(169)[1])
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("d"))
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("a"))
$BOXTYPE_BROADCASTDATE.add([TagLib.ByteVector]::FromString("y"))


#iTunes doesn't seem to make any use of this, but will set it anyway
$BOXTYPE_LDES = "ldes"; # long description


$mediaDirectory = Read-Host -Prompt "Enter directory to scan"
#if you don't have separated by series, you can move this in the foreach below and enter series number for each file
$season = Read-Host -Prompt "enter season number"


$fileList = Get-ChildItem -Path $mediaDirectory -Filter "*.mp4"
$episodeNumberInSeason = 0
foreach ($file in $fileList)
{
    $episodeNumberInSeason = $episodeNumberInSeason + 1
    $fileName = $file.Name
    $fileNameParts = $fileName.Split("-")
    $showName = $fileNameParts[0]
    $episodeNumber = $fileNameParts[1]
    if ($fileNameParts.count -gt 2)
    {
        #there were extra "-" in the title. get everything...
        $episodeTitle = $fileNameParts[2]
        for ($i = 3; $i -lt $fileNameParts.count;$i++)
        {
            $episodeTitle = $episodeTitle + "-" + $fileNameParts[$i]
        }
    }
    else
    {
        $episodeTitle = $fileNameParts[2]
    }
    #it should end with .mp4... but just check
    if ($episodeTitle.EndsWith(".mp4"))
    {
        $episodeTitle = $episodeTitle.Substring(0,($episodeTitle.Length - 4))
    }
    #need to find out the synopsis and broadcast date
    Write-Output "Preparing to tag file $($file.Name)"
    $alternateTitle = Read-Host -Prompt "enter fixed title (optional)"
    if (-not [string]::IsNullOrEmpty($alternateTitle))
    {
        $episodeTitle = $alternateTitle
    }
    $synopsis = Read-Host -Prompt "enter synopsis"
    if ($synopsis.Length -gt 255)
    {
        #the description field can only have 255 characters. just trim.
        #you could trim it shorter and add "..." at the end if you like.
        $shortDescription = $synopsis.Substring(0,255)
    }
    else
    {
        $shortDescription = $synopsis
    }
     
    $longDescription = $synopsis
    $airDateString = Read-Host -Prompt "enter air date"
    $airDateConverted = Get-Date $airDateString

    #here's where the actual work on the file's tags happen
    $mediaFile = [TagLib.File]::Create($file.FullName)
    [TagLib.Mpeg4.AppleTag]$customTag = $mediaFile.GetTag([TagLib.TagTypes]::Apple, 1)

    #Show Name
    $customTag.ClearData($BOXTYPE_TVSH)
    $customTag.SetText($BOXTYPE_TVSH, $showName)

    # Season Number
    $customTag.ClearData($BOXTYPE_TVSN)
    [TagLib.ByteVector] $seasonNumberVector = [TagLib.ByteVector]::FromUInt($season,$true)
    $customTag.SetData($BOXTYPE_TVSN, $seasonNumberVector, $DATATYPE_DATA);

    # Episode Number
    $customTag.ClearData($BOXTYPE_TVES);
    [TagLib.ByteVector] $episodeNumberVector = [TagLib.ByteVector]::FromUInt($episodeNumber,$true);
    $customTag.SetData($BOXTYPE_TVES, $episodeNumberVector, $DATATYPE_DATA);

    $customTag.Track = [uint32]$episodeNumber;

    # Episode Name
    $customTag.ClearData($BOXTYPE_TVEN)

    #this will reset the episode number for each season (season 2, episode 1)
    #use concat of season + episode number
    #so season 2, episode 1 will be: 201-Episode Title
    $customTag.SetText($BOXTYPE_TVEN, $season + (("{0:D2}" -f $episodeNumberInSeason)) + "-" + $episodeTitle)
    #one option is to just use the episode numbers to continue in order... don't reset to 1 each season,
    #use one of the below options:
    #$customTag.SetText($BOXTYPE_TVEN, $episodeNumber); #this would just have the number in it
    #$customTag.SetText($BOXTYPE_TVEN, $episodeNumber + $episodeTitle); #this would have number + title
    $customTag.Title = $episodeTitle

    # Short Description
    $customTag.ClearData($BOXTYPE_DESC)
    $customTag.SetText($BOXTYPE_DESC, $shortDescription)

    # Long Description
    $customTag.ClearData($BOXTYPE_LDES)
    $customTag.SetText($BOXTYPE_LDES, $longDescription)

    #set as tv show
    $vector = New-Object TagLib.ByteVector;
    $vector.Add([byte]$c_TvShowType)
    $customTag.SetData($BOXTYPE_STIK, $vector, $DATATYPE_DATA);

    $customTag.Genres = @("TV Shows");

    #air date
    $customTag.ClearData($BOXTYPE_BROADCASTDATE)
    $customTag.SetText($BOXTYPE_BROADCASTDATE,($airDateConverted.toString("yyyy-MM-dd") + $TIME_STRING))
    #it should be settting in a format like this:
    #$customTag.SetText($BOXTYPE_BROADCASTDATE,"2010-09-11T9:00:00 PMZ")
    
    #save the file now that it's been updated.
    #be aware it will update the last modified date for the file          
    $mediaFile.Save()

}