Sunday, June 5, 2016

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()

}





No comments:

Post a Comment