classes/media.ps1
|
#================================================================================================================== #================================================================================================================== # Shared Classes (Movies & TV Shows) #================================================================================================================== #================================================================================================================== #------------------------------------------------------------------------------------------------------------------ # A Person's Role in a Movie or TV Show #------------------------------------------------------------------------------------------------------------------ Class Credit { [String] $Type [String] $Role [String] $Name [String] $OriginalName [String] $ID [String] $CreditID [Int] $CreditOrder [String] $ProfileImagePath [String] $ProfileImageURL [String] $ProfilePageURL #----------------------------------------------- # Constructors #----------------------------------------------- Credit ( ) { } Credit ( [String] $Type, [String] $Role, [String] $Name, [String] $ID ) { $this.Type = $Type $this.Role = $Role $this.Name = $Name $this.ID = $ID } Credit ( [String] $Type, [String] $Role, [String] $Name ) { $this.Type = $Type $this.Role = $Role $this.Name = $Name } Credit ( [String] $Role, [String] $Name ) { $this.Role = $Role $this.Name = $Name } Credit ( [String] $Name ) { $this.Name = $Name } Credit ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #------------------------------------------------------------------------------------------------------------------ # The Rating assigned by the Motion Picture or Broadcast Association (G, R, TV-G, TV-14, etc.) #------------------------------------------------------------------------------------------------------------------ Class ContentRating { [String] $Country [String] $Rating [String[]] $Descriptors #----------------------------------------------- # Constructors #----------------------------------------------- ContentRating ( ) { } ContentRating ( [String] $Country, [String] $Rating, [String[]] $Descriptors ) { $this.Country = $Country $this.Rating = $Rating $this.Descriptors = $Descriptors } ContentRating ( [String] $Country, [String] $Rating) { $this.Country = $Country $this.Rating = $Rating } ContentRating ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #------------------------------------------------------------------------------------------------------------------ # A Studio or Network involved in the production. (Disney, Hulu, HBO, FX, etc...) #------------------------------------------------------------------------------------------------------------------ Class Entity { [String] $Name [String] $ID [String] $Country [String] $LogoPath [String] $LogoURL #----------------------------------------------- # Constructors #----------------------------------------------- Entity ( ) { } Entity ( [String] $Name, [String] $ID, [String] $Country, [String] $LogoPath, [String] $LogoURL ) { $this.Name = $Name $this.ID = $ID $this.Country = $Country $this.LogoPath = $LogoPath $this.LogoPath = $LogoURL } Entity ( [String] $Name, [String] $ID ) { $this.Name = $Name $this.ID = $ID } Entity ( [String] $Name ) { $this.Name = $Name } Entity ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #------------------------------------------------------------------------------------------------------------------ # A generic item used for basic lists. #------------------------------------------------------------------------------------------------------------------ Class Item { [String] $Name [String] $ID #----------------------------------------------- # Constructors #----------------------------------------------- Item ( ) {} Item ( [String] $Name ) { $this.Name = $Name } Item ( [String] $Name, [String] $ID ) { $this.Name = $Name $this.ID = $ID } Item ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #------------------------------------------------------------------------------------------------------------------ # An image associated with the content (Poster, Background, Logo or Still Image) #------------------------------------------------------------------------------------------------------------------ Class Image { [String] $Type [Decimal] $AspectRatio [Int] $Height [Int] $Width [String] $Language [String] $Path [String] $URL #----------------------------------------------- # Constructors #----------------------------------------------- Image ( ) { } Image ( [String] $Type, [String] $URL ) { $this.Type = $Type $this.URL = $URL } Image ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #------------------------------------------------------------------------------------------------------------------ # An collection of media items. #------------------------------------------------------------------------------------------------------------------ Class Collection { [String] $ID [String] $Name [String] $BackdropPath [String] $PosterPath [String] $BackdropURL [String] $PosterURL #----------------------------------------------- # Constructors #----------------------------------------------- Collection ( ) { } Collection ( [String] $ID, [String] $Name ) { $this.ID = $ID $this.Name = $Name } Collection ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #================================================================================================================== #================================================================================================================== # TVShow Classes #================================================================================================================== #================================================================================================================== #------------------------------------------------------------------------------------------------------------------ # A TVShow/Series, which contains seasons, episodes, credits, etc... #------------------------------------------------------------------------------------------------------------------ Class TVShow { [String] $Source [String] $ID [String] $Name [String] $OriginalName [String] $Description [String] $Rating [String] $RatingReason [ContentRating[]] $Ratings [String] $Genre [Item[]] $Genres [Credit[]] $Creators [Credit[]] $Cast [Credit[]] $Crew [String] $Year [String] $FirstAirDate [String] $LastAirDate [int32] $TotalSeasons [int32] $TotalEpisodes [Boolean] $InProduction [String] $Status [String[]] $Country [Entity[]] $Studios [Entity[]] $Networks [String] $HomePage [String] $LogoPath [String] $BackdropPath [String] $PosterPath [String] $Poster16x9Path [String] $PosterSquarePath [String] $LogoURL [String] $BackdropURL [String] $PosterURL [String] $Poster16x9URL [String] $PosterSquareURL [TVSeason[]] $Seasons = @() [TVEpisode[]] $Episodes = @() [TVEpisode] $LastEpisode [TVEpisode] $NextEpisode [Item[]] $ExternalIDs [Image[]] $Images #----------------------------------------------- # Constructors #----------------------------------------------- TVShow ( ) { } TVShow ( [String] $Name ) { $this.Name = $Name } TVShow ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } #----------------------------------------------- # Static Overrides #----------------------------------------------- static TVShow ( ) { Update-TypeData -TypeName 'TVShow' -MemberType ScriptProperty ` -MemberName Episodes ` -Value { return $( $this.Seasons | Select-Object -ExpandProperty 'Episodes' ) } } #----------------------------------------------- # Methods #----------------------------------------------- # Export the current TVShow object to a specified file path as a typed XML object. [void] ExportToCache ( [String] $FilePath ) { $this | Export-Clixml -LiteralPath $FilePath -Depth 100 -Force } # Import a TVShow object from a typed XML definition. static [TVShow] ImportFromCache ( [String] $Path ) { if ( [System.IO.File]::Exists($Path) ) { [TVShow] $importedTVShow = Import-Clixml -LiteralPath $Path } else { Write-Msg -x -lc -m $('ImportFromCache: A valid file path was not provided. Provide a path ' + 'to a valid TVShow cache XML file.') break } return $importedTVShow } } #------------------------------------------------------------------------------------------------------------------ # A Single Season of a TV Show #------------------------------------------------------------------------------------------------------------------ Class TVSeason { [String] $Source [String] $ID [String] $URL [String] $ShowTitle [String] $ShowID [String] $ShowURL [int32] $Number [String] $Name [String] $Description [Credit[]] $Cast [Credit[]] $Crew [String] $Network [String] $Year [String] $FirstAirDate [String] $LastAirDate [int32] $TotalEpisodes [String] $BackdropPath [String] $PosterPath [String] $Poster16x9Path [String] $PosterSquarePath [String] $BackdropURL [String] $PosterURL [String] $Poster16x9URL [String] $PosterSquareURL [TVEpisode[]] $Episodes [Item[]] $ExternalIDs [Image[]] $Images #----------------------------------------------- # Constructors #----------------------------------------------- TVSeason ( ) { } TVSeason ( [int32] $Number ) { $this.Number = $Number } TVSeason ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #------------------------------------------------------------------------------------------------------------------ # A Single Episode of a TV Show #------------------------------------------------------------------------------------------------------------------ Class TVEpisode { [String] $Source [String] $ID [String] $URL [String] $SeasonID [String] $SeasonURL [String] $ShowID [String] $ShowURL [int32] $Season [int32] $Number [int32] $Index [Bool] $IsFirstEpisode [String] $Type [String] $ShowTitle [String] $Title [String] $Description [String] $LongDescription [String] $Genre [Item[]] $Genres [String] $Rating [Credit[]] $Cast [Credit[]] $Crew [Credit[]] $Guests [String] $AirDate [String] $ProductionCode [String] $Network [int32] $Runtime [String] $BackdropPath [String] $PosterPath [String] $Poster16x9Path [String] $PosterSquarePath [String] $StillPath [String] $BackdropURL [String] $PosterURL [String] $Poster16x9URL [String] $PosterSquareURL [String] $StillURL [String] $BaseFileName [String] $ShowFileName [Item[]] $ExternalIDs [Image[]] $Images #----------------------------------------------- # Constructors #----------------------------------------------- TVEpisode ( ) { } TVEpisode ( [int32] $SeasonNumber, [int32] $EpisodeNumber, [String] $Title ) { $this.Season = $SeasonNumber $this.Number = $EpisodeNumber $this.Title = $Title } TVEpisode ( [int32] $SeasonNumber, [int32] $EpisodeNumber) { $this.Season = $SeasonNumber $this.Number = $EpisodeNumber } TVEpisode ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } #----------------------------------------------- # Static Overrides #----------------------------------------------- static TVEpisode ( ) { Update-TypeData -TypeName 'TVEpisode' -MemberType ScriptProperty ` -MemberName 'BaseFileName' ` -Value { $n = $( "s{0:D2}e{1:D2} - {2}" -f $this.season,$this.number, $([media]::CleanInvalidFileSystemChars($this.Title))) return $n } Update-TypeData -TypeName 'TVEpisode' -MemberType ScriptProperty ` -MemberName 'ShowFileName' ` -Value { $n = $( "{0} - {1}" -f $([media]::CleanInvalidFileSystemChars($this.ShowTitle)),$this.BaseFileName) return $n } } } #================================================================================================================== #================================================================================================================== # Movie Classes #================================================================================================================== #================================================================================================================== Class Movie { [String] $Source [String] $ID [Bool] $Adult [String] $Title [String] $Tagline [String] $OriginalTitle [String] $OriginalLanguage [String] $Description [String] $LongDescription [String] $Genre [Item[]] $Genres [String] $ReleaseDate [String] $Year [String] $Rating [String] $RatingReason [ContentRating[]] $Ratings [String] $Status [Int] $Runtime [Entity[]] $Studios [Int] $Budget [Int] $Revenue [String] $HomePage [String] $LogoPath [String] $BackdropPath [String] $PosterPath [String] $Poster16x9Path [String] $LogoURL [String] $BackdropURL [String] $PosterURL [String] $Poster16x9URL [Credit[]] $Cast [Credit[]] $Crew [Collection[]] $Collections [Image[]] $Images [Item[]] $ExternalIDs #----------------------------------------------- # Constructors #----------------------------------------------- Movie ( ) {} Movie ( [String] $Title ) { $this.Title = $Title } Movie ( [Hashtable] $Info ) { $Info.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $Info.$($_) } } } } #================================================================================================================== #================================================================================================================== # Media File Classes #================================================================================================================== #================================================================================================================== Class MediaFile { #----------------------------------------------- # File Properties #----------------------------------------------- [String] $ID [String] $PathID [String] $Path [String] $Name [String] $Extension [bool] $Exists [bool] $TaggingComplete [bool] $TagsUpdated [String[]] $UpdatedTags = @() [String] $NewPath [String] $NewName [String] $ParentFolderName [String] $ParentFolderPath [String] $CreatedTime [String] $LastUpdatedTime [String] $PosterPath [String] $ThumbnailPath [Hashtable] $Tags [MediaFileNames] $Names [MediaFileMatches] $Matches [PSCustomObject] $Encoding #----------------------------------------------- # Constructors #----------------------------------------------- MediaFile ( ) { } MediaFile ( [string] $MediaFilePath ) { $this.SetFileProperties( $MediaFilePath ) } #----------------------------------------------- # Static Overrides #----------------------------------------------- static MediaFile ( ) { Update-TypeData -TypeName 'MediaFile' -MemberType ScriptProperty ` -MemberName 'Exists' ` -Value { return [System.IO.File]::Exists($this.Path) } Update-TypeData -TypeName 'MediaFile' -MemberType ScriptProperty ` -MemberName 'GeneratedName' ` -Value { $o = [media]::CleanInvalidFileSystemChars([Media]::CleanPunctuation($this.Tags.tvShowName)) $t = [media]::CleanInvalidFileSystemChars([Media]::CleanPunctuation($this.Tags.title)) $s = [int]$this.Tags.tvSeasonNumber $e = [int]$this.Tags.tvEpisodeNumber $y = $([DateTime]$this.Tags.releaseDate).Year if ( $this.Tags.mediaType -eq 'TV Show' -or $null -ne $this.Tags.tvShowName ) { $n = '{0} - s{1:d2}e{2:d2} - {3}' -f $o,$s,$e,$t } else { $n = '{0} ({1})' -f $t,$y } if ( -not [string]::IsNullOrEmpty($this.Encoding.ProfileTag) ) { $n += $(' [{0}]' -f $([media]::CleanInvalidFileSystemChars($this.Encoding.ProfileTag))) } elseif ( -not [string]::IsNullOrEmpty($this.Tags.comment) ) { $n += $(' [{0}]' -f $([media]::CleanInvalidFileSystemChars($this.Tags.comment))) } $n += '.m4v' return $n } } #----------------------------------------------- # Methods #----------------------------------------------- # Sets the MediaFile properties based on the physical file attributes. hidden [void] SetFileProperties ( [string] $MediaFilePath ) { $this.Path = $MediaFilePath if ( $this.Exists ) { $file = Get-Item -LiteralPath $MediaFilePath $this.Name = $file.Name $this.Extension = $file.Extension $this.ParentFolderName = $file.Directory.Name $this.ParentFolderPath = $file.Directory.FullName $this.CreatedTime = $file.CreationTime $this.LastUpdatedTime = $file.LastWriteTime $this.ID = [Media]::GetFileHash($this.Path) $this.PathID = [Media]::GetFilePathHash($this.Path,16) $this.Matches = [MediaFileMatches]::new($this.ID) $this.Names = [MediaFileNames]::new($this.Name) $this.SetPosterPath() } } # Test for the existence of and sets the poster path. hidden [void] SetPosterPath ( ) { $SupportedFileExtensions = @('.jpg','.jpeg','.png','.tbn') foreach ( $Extension in $SupportedFileExtensions ){ $PosterFilePath = ($this.Path -replace "\$($this.Extension)$",'') + $Extension if ( [System.IO.File]::Exists($PosterFilePath) ) { $this.PosterPath = $PosterFilePath break } } } # Set the tags/atoms. [void] SetTags ( [Hashtable] $TagData ) { # $this.Tags = [MediaFileTags]::new($TagData) $this.Tags = $TagData } # Set the encoding properties. [void] SetEncoding ( [Hashtable] $EncodingData ) { $this.Encoding = $EncodingData if ( $this.Names.EncodedVideoIsDenoised ) { $this.Encoding.Video.FormatTag += '-D' } if ( $this.Encoding.SourceTag -eq 'OTA' -and $this.Names.EncodedSource -eq 'DVD' ) { $this.Encoding.SourceTag = 'DVD' } $this.Encoding.ProfileTag = $( '{0} {1} {2} {3} {4}' -f $this.Encoding.Video.ResolutionTag, $this.Encoding.Video.AspectRatioTag, $this.Encoding.SourceTag, $this.Encoding.Video.FormatTag, $this.Encoding.Audio.FormatTag ) } } #------------------------------------------------------------------------------------------------------------------ # The online database ids of the entries matching a single file. #------------------------------------------------------------------------------------------------------------------ Class MediaFileMatches { [String] $FileId [String] $ATVpId [String] $TMdbId [String] $TVdbId [String] $IMdbId [Double] $ATVpMatchScore [Double] $TMdbMatchScore [Double] $TVdbMatchScore [Double] $IMdbMatchScore [Bool] $ATVpMatchConfirmed [Bool] $TMdbMatchConfirmed [Bool] $TVdbMatchConfirmed [Bool] $IMdbMatchConfirmed #----------------------------------------------- # Constructors #----------------------------------------------- MediaFileMatches ( ) { } MediaFileMatches ( [String] $FileId ) { $this.FileId = $FileId } } #------------------------------------------------------------------------------------------------------------------ # Detokenizes the different naming parts of a TV or Movie file name. #------------------------------------------------------------------------------------------------------------------ Class MediaFileNames { [String] $FullName [String] $StandardizedName [String] $BaseName [String] $BaseNameWithTag [String] $Extension [String] $FileTag [String] $FileTagValue [String] $TVShowName [String] $TVShowNameAndYear [String] $TVShowYear [String] $TVSeasonNumber [String] $TVEpisodeNumber [String] $TVEpisodeTitle [String] $TVEpisodeTitleHasPartNumber [String] $TVEpisodeTitlePartNumber [String] $TVEpisodeTitleNoPart [String] $TVEpisodeTitlePart [String] $TVEpisodeTitleStandardized [String] $TVEpisodeTitleWithPartFormat1 [String] $TVEpisodeTitleWithPartFormat2 [String] $TVEpisodeTitleWithPartFormat3 [String] $MovieName [String] $MovieYear [Bool] $UsesTVShowFormat [Bool] $UsesMovieFormat [String] $EncodedSummary [String] $EncodedResolution [String] $EncodedAspect [String] $EncodedSource [String] $EncodedVideoFormat [String] $EncodedAudioFormat [Bool] $EncodedVideoIsAnimated [Bool] $EncodedVideoIsDenoised #----------------------------------------------- # Constructors #----------------------------------------------- MediaFileNames ( ) { } MediaFileNames ( [String] $FileName ) { $this.Update($FileName) } #----------------------------------------------- # Functions #----------------------------------------------- [void] Update ( [String] $FileName ) { $naming = [Media]::GetFileNaming($FileName) $naming.Keys | ForEach-Object { if ( $this.PSObject.Properties.Match($_).count -eq 1 ) { $this.$($_) = $naming.$($_) } } } } #------------------------------------------------------------------------------------------------------------------ # A utility class for static, media-related functions. #------------------------------------------------------------------------------------------------------------------ Class Media { #----------------------------------------------------------------------------- # Calculates a Fast file hash by analyzing 3 parts of a file. #----------------------------------------------------------------------------- static [String] GetFileHash ( [string] $FilePath ) { $f = [System.IO.File]::OpenRead($FilePath) try { $size = [long]$f.Length $hasher = [System.Security.Cryptography.SHA256]::Create() $bufSize = 65536 $buf = New-Object byte[] $bufSize # Compute min without calling System.Math overloads $min = { param($a,$b) if ($a -lt $b) { return $a } else { return $b } } # Read File Head $count = [int](& $min $bufSize $size) if ($count -gt 0) { $read = $f.Read($buf,0,$count) $hasher.TransformBlock($buf,0,$read,$null,0) | Out-Null } # Read File Middle $midPos = [long](([long]($size - $bufSize)) / 2) if ($midPos -lt 0) { $midPos = 0 } $remaining = $size - $midPos $count = [int](& $min $bufSize $remaining) if ($count -gt 0) { $f.Position = $midPos $read = $f.Read($buf,0,$count) $hasher.TransformBlock($buf,0,$read,$null,0) | Out-Null } # Read File Tail $tailPos = $size - $bufSize if ($tailPos -lt 0) { $tailPos = 0 } $f.Position = $tailPos $remaining = $size - $f.Position $count = [int](& $min $bufSize $remaining) if ($count -gt 0) { $read = $f.Read($buf,0,$count) $hasher.TransformFinalBlock($buf,0,$read) | Out-Null } else { $hasher.TransformFinalBlock($buf,0,0) | Out-Null } $hash = ($hasher.Hash | ForEach-Object { $_.ToString('x2') }) -join '' return "${size}-${hash}" } finally { $f.Close() } } #----------------------------------------------------------------------------- # Calculates a shorter (file-system friendly) version of the full File Hash. #----------------------------------------------------------------------------- # # Collision risk (~1% chance of any collision) by truncated hash length: # # HexChars Bits Files needed for ~1% collision risk # -------- ---- ----------------------------------- # 8 32 ~9,300 # 12 48 ~2.4 million # 16 64 ~610 million # 20 80 ~156 billion # 24 96 ~40 trillion # 28 112 ~10 quadrillion # 32 128 ~2.6 quintillion # #----------------------------------------------------------------------------- # static [String] GetShortFileHash ( [string] $FileHash, [int] $DesiredLength ) { # $hash = $FileHash.Substring($FileHash.IndexOf('-') + 1) # return $hash.Substring(0, $DesiredLength) # } #----------------------------------------------------------------------------- # Calculates a Fast file hash based on the file path. #----------------------------------------------------------------------------- # Calculates an MD5 hash on the file path. # # Collision risk (~1% chance of any collision) by truncated hash length, using # the birthday-paradox approximation n ≈ √(0.0201 × 2^bits): # # HexChars Bits Files needed for ~1% collision risk # -------- ---- ----------------------------------- # 8 32 ~9,300 # 12 48 ~2.4 million # 16 64 ~610 million # 20 80 ~156 billion # 24 96 ~40 trillion # 28 112 ~10 quadrillion # 32 128 ~2.6 quintillion (full MD5) # #----------------------------------------------------------------------------- static [String] GetFilePathHash ( [string] $FilePath, [int] $DesiredLength ) { $bytes = [System.Text.Encoding]::UTF8.GetBytes($FilePath) $hasher = [System.Security.Cryptography.MD5]::Create() $hash = $hasher.ComputeHash($bytes) $hex = ($hash | ForEach-Object { $_.ToString('x2') }) -join '' return $hex.Substring(0, $DesiredLength) } #----------------------------------------------------------------------------- # De-tokenizes the file name. #----------------------------------------------------------------------------- static [PSCustomObject] GetFileNaming ( [string] $FileName ) { $fxp = '^(?<base>.+?)\s*(?<tag>\[[^\]]*\])?\s*(?<ext>\.[A-Za-z0-9]{1,6})?$' $mxp = '(?<title>.+?)\s*\((?<year>\d{4})\)' $num = 'one|two|three|four|five|six|seven|eight|nine|ten' $txf = '^(?<show>.+?)\s*-\s*[sS](?<season>\d{1,2})[eE](?<episode>\d{1,2})\s*-\s*(?<title>.+)$' $txp = ('(?i)^(?<show>.+?)\s*-\s*s(?<season>\d{1,2})e(?<episode>\d{1,2})\s*-\s*(?<title>.+?)' + '(?:[\s,:-]*?(?:\(?\s*(?:part|pt)\.?\s*(?<part>\d{1,2}|'+$num+')\s*\)?|\(?\s*(?<part>\d{1,2})\s*\)?))?\s*$') $p = [ordered]@{ FullName = $FileName; StandardizedName = $null } $fp = [regex]::Match($FileName,$fxp) if ( $fp.success ) { $p.FullName = $FileName $p.BaseName = $fp.Groups['base'].Value $p.FileTag = $fp.Groups['tag'].Value $p.FileTagValue = [regex]::Match($FileName,'(?<=\[)[^\]]+(?=\])').value $p.Extension = $fp.Groups['ext'].Value $p.BaseNameWithTag = $FileName.Replace($p.Extension,'') $tf = [regex]::Match($p.BaseName,$txf) $tp = [regex]::Match($p.BaseName,$txp) if ( $tp.success ) { $p.UsesTVShowFormat = $true $p.UsesMovieFormat = $false $p.TVShowName = $tp.Groups['show'].Value.Trim() $p.TVShowNameAndYear = $tf.Groups['show'].Value.Trim() $p.TVSeasonNumber = $tp.Groups['season'].Value $p.TVEpisodeNumber = $tp.Groups['episode'].Value $p.TVEpisodeTitle = $tf.Groups['title'].Value.Trim() $p.TVEpisodeTitleHasPartNumber = $tp.Groups['part'].value -eq '' ? $false : $true $p.TVEpisodeTitlePartNumber = $null $p.TVEpisodeTitleNoPart = $tp.Groups['title'].Value.Trim() $p.TVEpisodeTitlePartString = $tp.Groups['part'].value -eq '' ? $null : $p.TVEpisodeTitle.Replace($p.TVEpisodeTitleNoPart,'').Trim() $p.TVEpisodeTitlePart = $tp.Groups['part'].value -eq '' ? $null : $tp.Groups['part'].value.ToString().Trim() if ( $p.TVEpisodeTitleHasPartNumber ) { if ($p.TVEpisodeTitlePart -match '^\d+$') { $p.TVEpisodeTitlePartNumber = [int]$p.TVEpisodeTitlePart } else { $p.TVEpisodeTitlePartNumber = $( switch ($p.TVEpisodeTitlePart.ToLowerInvariant()) { 'one' { 1 } 'two' { 2 } 'three' { 3 } 'four' { 4 } 'five' { 5 } 'six' { 6 } 'seven' { 7 } 'eight' { 8 } 'nine' { 9 } 'ten' { 10 } default { $null } }) } $p.TVEpisodeTitleWithPartFormat1 = $('{0} (Part {1})' -f $p.TVEpisodeTitleNoPart, $p.TVEpisodeTitlePartNumber) $p.TVEpisodeTitleWithPartFormat2 = $('{0}, Part {1}' -f $p.TVEpisodeTitleNoPart, $p.TVEpisodeTitlePartNumber) $p.TVEpisodeTitleWithPartFormat3 = $('{0}, Pt. {1}' -f $p.TVEpisodeTitleNoPart, $p.TVEpisodeTitlePartNumber) } $p.TVEpisodeTitleStandardized = $p.TVEpisodeTitleWithPartFormat1 ?? $p.TVEpisodeTitleNoPart $p.StandardizedName = $("{0} - s{1:D2}e{2:D2} - {3} {4}{5}" -f $p.TVShowNameAndYear, $p.TVSeasonNumber, $p.TVEpisodeNumber, $p.TVEpisodeTitleStandardized, $p.FileTag, $p.Extension) } else { $mp = [regex]::Match($p.BaseName,$mxp) if ( $mp.success ) { $p.UsesTVShowFormat = $false $p.UsesTVShowYearFormat = $false $p.UsesMovieFormat = $true $p.MovieTitle = $mp.Groups['title'].Value.Trim() $p.MovieYear = $mp.Groups['year'].Value } else { $p.UsesTVShowFormat = $false $p.UsesTVShowYearFormat = $false $p.UsesMovieFormat = $false } $p.StandardizedName = $p.FullName } $ep = [Media]::GetEncodingProfileProperties($p.FileTagValue) if ( $ep.count -gt 0 ) { $ep.Keys | ForEach-Object { $p.$($_) = $ep.$($_) } } if ( $p.TVEpisodeTitle -like "*©*" ) { $p.EncodedAudioCommentary = $true } } else { $p.UsesTVShowFormat = $false $p.UsesMovieFormat = $false } return $p } #----------------------------------------------------------------------------- # De-tokenizes the MPEG profile summary. #----------------------------------------------------------------------------- # Samples: # r x s v a ac c # ----- --- ------ ----- --- --- --- # 4k CWS BR SHQ20 DD # 1080p WS BR SHQ20 DD # 1080p WS iTunes HD DD # 1080p WS BR SHQ20 AC3 5.1 # 1080p WS BR ATV3+ DD COM # 240p WS BR iPod DPL 2.0 # 240p WS BR iPod DPL 2.0 COM #----------------------------------------------------------------------------- static [PSCustomObject] GetEncodingProfileProperties ( [String] $EncodingProfile ) { $r = @{ } $x = @{ rez = '(?<resolution>(?:\d{3,4}p|4[kK]))' asp = '(?<aspect>FS|WS|CWS)' src = '(?<source>[A-Za-z0-9\+\-]+)' vid = '(?<videoformat>[A-Za-z0-9\+\-]+)' aud = '(?<audioformat>[A-Za-z0-9\+\-\.]+)' ach = '(?<audiochannels>[0-9\.]+)' com = '(?<commentary>[A-Za-z]+)' xxx = '\b' s = '\s+' } $p = @( $x.xxx, $x.rez, $x.s, $x.asp, $x.s, $x.src, $x.s, $x.vid ) if ( ($EncodingProfile -split ' ').count -eq 7 -and $EncodingProfile.EndsWith(' COM') ) { $p += @( $x.s, $x.aud, $x.s, $x.ach, $x.s, $x.com, $x.xxx ) } elseif ( ($EncodingProfile -split ' ').count -eq 6 -and $EncodingProfile.EndsWith(' COM') ) { $p += @( $x.s, $x.aud, $x.s, $x.com, $x.xxx ) } elseif ( ($EncodingProfile -split ' ').count -eq 6 ) { $p += @( $x.s, $x.aud, $x.s, $x.ach, $x.xxx ) } else { $p += @( $x.s, $x.aud, $x.xxx ) } $p = $p -join '' if ($EncodingProfile -match $p) { $r.EncodedSummary = $EncodingProfile $r.EncodedResolution = $matches['resolution'] $r.EncodedAspect = $matches['aspect'] $r.EncodedSource = $matches['source'] $r.EncodedVideoFormat = $matches['videoformat'] $r.EncodedAudioFormat = $matches['audioformat'] $r.EncodedAudioChannels = $matches['audiochannels'] $r.EncodedAudioCommentary = Test-IsSomething($matches['commentary']) $r.EncodedVideoIsAnimated = $r.EncodedVideoFormat -like "*2DA" $r.EncodedVideoIsDenoised = ( $r.EncodedVideoFormat -like "*ULD" -or $r.EncodedVideoFormat.EndsWith('-D') ) } return $r } #----------------------------------------------------------------------------- # Removes Special Characters from a string #----------------------------------------------------------------------------- static [String] ReplaceSpecialCharacters ( [String] $String ) { $sc = [System.Collections.Generic.Dictionary[string,string]]::new() $sc.Add([char]0x2018,"'") # ‘ LEFT SINGLE QUOTATION MARK $sc.Add([char]0x2019,"'") # ’ RIGHT SINGLE QUOTATION MARK $sc.Add([char]0x201A,"'") # ‚ SINGLE LOW-9 QUOTATION MARK $sc.Add([char]0x201B,"'") # ‛ SINGLE HIGH-REVERSED-9 QUOTATION MARK $sc.Add([char]0x2032,"'") # ′ PRIME $sc.Add([char]0x2035,"'") # ‵ REVERSED PRIME $sc.Add([char]0x275B,"'") # ❛ HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT $sc.Add([char]0x275C,"'") # ❜ $sc.Add([char]0x201C,'"') # “ LEFT DOUBLE QUOTATION MARK $sc.Add([char]0x201D,'"') # ” RIGHT DOUBLE QUOTATION MARK $sc.Add([char]0x201E,'"') # „ DOUBLE LOW-9 QUOTATION MARK $sc.Add([char]0x201F,'"') # ‟ DOUBLE HIGH-REVERSED-9 QUOTATION MARK $sc.Add([char]0x2033,'"') # ″ DOUBLE PRIME $sc.Add([char]0x2036,'"') # ‶ REVERSED DOUBLE PRIME $sc.Add([char]0x275D,'"') # ❝ HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT $sc.Add([char]0x275E,'"') # ❞ $sc.Add("é","é") $sc.Add("À","A") $sc.Add("Á","A") $sc.Add("Â","A") $sc.Add("Ã","A") $sc.Add("Ä","A") $sc.Add("Å","A") $sc.Add("Æ","A") $sc.Add("È","E") $sc.Add("É","E") $sc.Add("Ê","E") $sc.Add("Ë","E") $sc.Add("Ì","I") $sc.Add("Í","I") $sc.Add("Î","I") $sc.Add("Ï","I") $sc.Add("Ò","O") $sc.Add("Ó","O") $sc.Add("Ô","O") $sc.Add("Õ","O") $sc.Add("Ö","O") $sc.Add("Ù","U") $sc.Add("Ú","U") $sc.Add("Û","U") $sc.Add("Ü","U") $sc.Add("à","a") $sc.Add("á","a") $sc.Add("â","a") $sc.Add("ã","a") $sc.Add("ä","a") $sc.Add("å","a") $sc.Add("æ","a") $sc.Add("è","e") $sc.Add("ê","e") $sc.Add("ë","e") $sc.Add("ì","i") $sc.Add("í","i") $sc.Add("î","i") $sc.Add("ï","i") $sc.Add("ò","o") $sc.Add("ó","o") $sc.Add("ô","o") $sc.Add("õ","o") $sc.Add("ö","o") $sc.Add("ù","u") $sc.Add("ú","u") $sc.Add("û","u") $sc.Add("ü","u") $sc.Add("ý","y") $sc.Add("þ","y") $sc.Add("ÿ","y") foreach ( $k in $sc.keys ) { $String = $String.Replace( $k,$sc[$k] ) } return $String } #----------------------------------------------------------------------------- # Removes Special Punctuation from a string #----------------------------------------------------------------------------- static [String] CleanPunctuation ( [String] $String ) { $sc = [System.Collections.Generic.Dictionary[string,string]]::new() $sc.Add([char]0x2018,"'") # ‘ LEFT SINGLE QUOTATION MARK $sc.Add([char]0x2019,"'") # ’ RIGHT SINGLE QUOTATION MARK $sc.Add([char]0x201A,"'") # ‚ SINGLE LOW-9 QUOTATION MARK $sc.Add([char]0x201B,"'") # ‛ SINGLE HIGH-REVERSED-9 QUOTATION MARK $sc.Add([char]0x2032,"'") # ′ PRIME $sc.Add([char]0x2035,"'") # ‵ REVERSED PRIME $sc.Add([char]0x275B,"'") # ❛ HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT $sc.Add([char]0x275C,"'") # ❜ $sc.Add([char]0x201C,'"') # “ LEFT DOUBLE QUOTATION MARK $sc.Add([char]0x201D,'"') # ” RIGHT DOUBLE QUOTATION MARK $sc.Add([char]0x201E,'"') # „ DOUBLE LOW-9 QUOTATION MARK $sc.Add([char]0x201F,'"') # ‟ DOUBLE HIGH-REVERSED-9 QUOTATION MARK $sc.Add([char]0x2033,'"') # ″ DOUBLE PRIME $sc.Add([char]0x2036,'"') # ‶ REVERSED DOUBLE PRIME $sc.Add([char]0x275D,'"') # ❝ HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT $sc.Add([char]0x275E,'"') # ❞ foreach ( $k in $sc.keys ) { $String = $String.Replace( $k,$sc[$k] ) } return $String } #----------------------------------------------------------------------------- # Removes invalid files system characters for Windows, Mac OS and Linux #----------------------------------------------------------------------------- static [String] CleanInvalidFileSystemChars ( [String] $String ) { $invalidChars = @('<','>',';',':','"','/','\','|','?','*','$','%') + ([char[]](0..31)) $pattern = ("[{0}]" -f [RegEx]::Escape($invalidChars -Join '')) $result = $String -replace $pattern return $result } } |