-
Notifications
You must be signed in to change notification settings - Fork 2
/
format-tvshows.ps1
498 lines (422 loc) · 19.3 KB
/
format-tvshows.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
<#
.SYNOPSIS
Renames TV Show files to a specific naming scheme and moves the files
to their correct seasons folder.
.DESCRIPTION
The scrips calls The Movie Database, themoviedb.org, api to fetch
information aobut the TV Shows, Seasons, and Episodes.
It takes that data and creates Season folders for every season;
Renames the TV Shows episodes to the correct naming scheme; and
moves the episodes into the correct season folder.
After the script has finished processing the files, empty folders are removed.
.PARAMETER FolderPath
Specify the path to the TV Show folder where you want the files to be processed.
.PARAMETER TheMovieDB_API
Will need to have an themoviedb.org accouunt and setup to get a free api token.
This what allows the API calls to be authenticated in order to return data.
.PARAMETER TVShowID
If the search is not returning the correct TV Show or you just want to
manually specify one, you grab it off themoviedb.org website and the script
will use that ID when pulling information about the TV Show.
.PARAMETER Separator
Allows specifing the separator that is used when separating Season and Episode
numbers. Example S01.E02
Allows the following characters to be used as a separator 'xX.-_ ' and a one
character limit.
Defaults to use the period of nothing is defined.
.PARAMETER NoSeparator
Will cause the script to not use a separator between Season and Episode
numbers. Example S01E02
Will override anything defined in the -Separator parameter.
.INPUTS
None. You cannot pipe objects to format-tvshows.ps1.
.OUTPUTS
None. format-tvshows.ps1 does not generate any output.
.NOTES
Author: Bradley Herbst
Created: February 9th, 2023
PROBLEMS: Doesn't currently handle processing two Episodes in one File.
It will rename the file to the first episode and you will need to manually fix
the name and specify the second episode.
# Script to recreate TV Show folder structure to allow testing of the script
param(
[Parameter(Mandatory)][IO.DirectoryInfo] $SourceBackupFolder,
[string] $DestinationFolder = [Environment]::GetFolderPath('UserProfile')
)
$Path = (New-Item -ItemType Directory -Path $DestinationFolder -Name $(
Split-Path $SourceBackupFolder -Leaf) -ErrorAction Stop
).FullName
Get-ChildItem -Path $SourceBackupFolder -Recurse
| ForEach-Object {
if ($_.Gettype().Name -eq 'DirectoryInfo') {
New-Item -ItemType Directory -Path $(
Join-Path -Path $Path -ChildPath (
Split-Path $_.FullName
).Replace($SourceBackupFolder,'')
) -Name $_.BaseName
}
else {
New-Item -Path $([WildcardPattern]::Escape($(
Join-Path -Path $Path -ChildPath (Split-Path $_.FullName).Replace($SourceBackupFolder,'')
))) -Name $_.Name
}
}
.EXAMPLE
./format-tvshows.ps1 -FolderPath 'Friends -TheMovieDB_API $env:api -Separator "x"
Basic example specify a specific separator.
.LINK
Git Repository Location
https://github.com/bordwalk2000/format-tvshows
#>
#Requires -Version 7
[CmdletBinding()]
# Ignore VSCode warning saying that $count is not being used, because it's defined in the begin scope.
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'count',
Justification = 'variable is used in another scope')]
param (
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[IO.DirectoryInfo] $FolderPath,
[ValidateNotNullOrEmpty()]
[string] $TheMovieDB_API,
[string] $TVShowID,
[ValidateNotNullOrEmpty()]
[ValidatePattern('[xX\.\-_ ]')]
[ValidateLength(1, 1)]
[string] $Separator = ".",
[switch] $NoSeparator
)
BEGIN {
Function Find-TheMovieDBTVShowID {
param(
[Parameter(Mandatory)][string] $SearchString,
[string] $APIKey,
[int] $Year,
[int] $ResultsCounts = 1
)
# TheMovieDB API Address
$BaseURL = "https://api.themoviedb.org/3"
# Escape String to be used in URL Search
$EscapedString = [uri]::EscapeDataString($SearchString)
# Create Search Query URL
$SearchParams = [string]::Join('&',"query=$EscapedString","api_key=$APIKey","first_air_date_year=$Year")
$SearchQuery = "$BaseURL/search/tv?$SearchParams"
# Search for the TV Show and Pulls out top results
$APIData = Invoke-WebRequest -Uri $SearchQuery -ErrorAction Stop
$Results = ($APIData.Content | ConvertFrom-Json).results
| Select-Object -First $ResultsCounts id, name, first_air_date, overview
Return $Results
}
Function Get-TheMovieDBTVShowInfo {
param(
[Parameter(Mandatory)][int] $TVShowID,
[string] $APIKey
)
# TheMovieDB API Address
$BaseURL = "https://api.themoviedb.org/3"
# Create TV Show URI
$URI = "$BaseURL/tv/$($TVShowID)?api_key=$APIKey"
# Calls the API for TV Show Data
$APIData = Invoke-WebRequest -Uri $URI -ErrorAction Stop
$Results = $APIData.Content | ConvertFrom-Json
| Select-Object name, first_air_date, number_of_seasons, seasons
Return $Results
}
Function Get-TheMovieDBSeasonInfo {
param(
[Parameter(Mandatory)][int] $TVShowID,
[Parameter(Mandatory)][int] $SeasonNumber,
[string] $APIKey
)
# TheMovieDB API Address
$BaseURL = "https://api.themoviedb.org/3"
# Create TV Show Season URI
$URI = "$BaseURL/tv/$($TVShowID)/season/$($SeasonNumber)?api_key=$APIKey"
#Calls the API for TV Show Season Data
$APIData = Invoke-WebRequest -Uri $URI -ErrorAction Stop
$Results = $APIData.Content | ConvertFrom-Json
| Select-Object id, name, air_date, number_of_seasons, episodes
Return $Results
}
# TheMovieDB API Address
$BaseURL = "https://api.themoviedb.org/3"
# System Check for Invalid File Name Characters
$InvalidFileNameChars = [string]::join('',
([IO.Path]::GetInvalidFileNameChars())
) -replace '\\', '\\'
}
PROCESS {
# Verifiy Folder Path is Valid
if (-not (Test-Path $FolderPath)) {
Write-Error 'Folder Path File Not Valid' -ErrorAction Stop
}
# Check if Able to Successfully Call TheMovieDB API
try {
Write-Verbose "Test API Connection"
Write-Debug $(Invoke-RestMethod -Uri "$BaseURL/genre/movie/list?api_key=$TheMovieDB_API")
}
catch {
$StatusCode = $_.Exception.Response.StatusCode
Write-Debug API StatusCode: $StatusCode
if ($StatusCode -eq 401) {
$ErrorMessage = "Error Code: 401 Unauthroized. " +
"Unable to Connect to API."
Write-Error $ErrorMessage -ErrorAction Stop
}
else {
$ErrorMessage = "Expected 200, got $([int]$StatusCode) " +
"Unable to Connect to API."
Write-Error $ErrorMessage -ErrorAction Stop
}
}
# Find TheMovieDB TV Show ID if Not Specified
if (!$TVShowID) {
#Grab TV Show Name From Folder Name
$FolderName = Split-Path $FolderPath -leaf
# Check For Year Tag and Get Value Ready for Search Query if Found
if ($FolderName -match '\s\(\d{4}\)') {
$YearSearch = $FolderName -match '\s\(\d{4}\)' | Select-Object -First 1
| ForEach-Object {
# Returns Section of the String that the Regex Validated
$Matches.Values -Replace '[^\d]'
}
}
# Combine TV Show Name with Search Year Criteria
$SearchString = $FolderName -replace '\s\(\d{4}\)',''
# Splat Paramters Used for Find-TheMovieDBTVShowID Function
$Params = @{
APIKey = $TheMovieDB_API
SearchString = $SearchString
}
# Add YearSearch if one was found.
if ($YearSearch) { $Params.Year = $YearSearch }
# Call Get API to get TV Show ID
$Results = Find-TheMovieDBTVShowID @Params
# Verify Results were Returned
if ($Results.count -lt 1) {
$ErrorMessage = "Unable to find results based on tv show search query: $SearchString`r`n" +
"Either update folder name or supply TheMovieDB TV Show ID."
Write-Error $ErrorMessage -ErrorAction Stop
}
# Populate TVShowID Variable
$TVShowID = $Results.ID
Write-Verbose "TV Show TheMovieDB ID: $TVShowID"
}
# Get TV Show Information
$TVShowInfo = Get-TheMovieDBTVShowInfo -TVShowID $TVShowID -APIKey $TheMovieDB_API
# Move 'The/A/An' to the End of the Title
$FormatedTVShowName = $TVShowInfo.name -replace '^(the|a|an) (.*)$', '$2, $1'
# Remove Colon from the Name; Not a Supported File Name Character on Windows
$FormatedTVShowName = $FormatedTVShowName -Replace (': ', ' - ')
# Replace Open Parentheses with Dash
$FormatedTVShowName = $FormatedTVShowName -Replace (' \(', ' - ')
# Remove Special Characters Not Supported On Different Operating Systems As Valid File Name Characters.
$FormatedTVShowName = $FormatedTVShowName -Replace '[?(){}]'
# Remove Colon from the Name; Not a Supported Windows Filename Character.
$FormatedTVShowName = $FormatedTVShowName -replace "[$InvalidFileNameChars]", ''
# Grab Only the Year from the First Aired Date
$FirstedAiredYear = $TVShowInfo.first_air_date.Split('-')[0]
# Put TV Show Folder Name String Together.
$TVShowFolderName = "$($FormatedTVShowName) ($FirstedAiredYear)"
Write-Debug "TV Show Folder Name: $TVShowFolderName"
# Define Variable for Path to Newly Named Folder
$UpdatedFolderPath = (
Join-Path -Path (Split-Path -Path $FolderPath -Parent
) -ChildPath $TVShowFolderName)
# Checks Folder Name is Different From New Name
if ($(Split-Path -Path $FolderPath -Leaf) -ne $TVShowFolderName) {
# Verify New Name Folder Doesn't Already Exists
if (-not (Test-Path $UpdatedFolderPath)) {
# Rename TV Show Folder
Rename-Item -Path $FolderPath -NewName $TVShowFolderName
}
else {
$ErrorMessage = "Folder already exists named '$UpdatedFolderPath' `r`n" +
"Please remove or rename existing folder then rerun the script."
Write-Error $ErrorMessage -ErrorAction Stop
}
}
# Processes the Season Data in the TV Show Info Results
$TVShowInfo.seasons
| Sort-Object season_number
| ForEach-Object {
Write-Verbose "Processing Season $("{0:D2}" -f ([int]$_.season_number))"
# Create Season Folder if it Doesn't Exist with 2-Digit Season Number
if (-not(
Test-Path -Path $(
Join-Path -Path $UpdatedFolderPath `
-ChildPath $("\Season {0:D2}" -f ([int]$_.season_number))
)
)) {
New-Item -ItemType Directory -Path $(
Join-Path -Path $UpdatedFolderPath `
-ChildPath $("\Season {0:D2}" -f ([int]$_.season_number))
)
}
# Define Parameters to be Used in Get-TheMovieDBSeasonInfo Function
$Params = @{
TVShowID = $TVShowID
SeasonNumber = $_.season_number
APIKey = $TheMovieDB_API
}
# Call API to Get Season Info and Processes Data on the Episodes
(Get-TheMovieDBSeasonInfo @Params).episodes
| Sort-Object episode_number
| ForEach-Object {
# Creates String for Specifying 2-Digit Season and Episode Numbers
$FullEpisodeNumber =
$("S{0:D2}" -f ([int]$_.season_number)) +
$(if (-not($NoSeparator)) { $Separator }) +
$("E{0:D2}" -f ([int]$_.episode_number))
Write-Verbose "Processing Episode $FullEpisodeNumber"
# Assign Episode Name to Variable
$EpisodeTitle = $_.name
Write-Debug "Original Episode Title: $EpisodeTitle"
# Replace Colon at End of String with a Dashte
$EpisodeTitle = $EpisodeTitle -Replace (': ', ' - ')
# Replace Colon in Middle of String with Unicode Colon Character
$EpisodeTitle = $EpisodeTitle -Replace (':','꞉')
# Replace Open Parentheses with Dash
$EpisodeTitle = $EpisodeTitle -Replace (' \(', ' - ')
# Remove Special Characters Not Supported On Different Operating Systems As Valid File Name Characters.
$EpisodeTitle = $EpisodeTitle -Replace '[?(){}]'
# Verify No Invalid File Name Characters in Episode Name
$EpisodeTitle = $EpisodeTitle -replace "[$InvalidFileNameChars]", ''
Write-Debug "Filtered Episode Title: $EpisodeTitle"
# Finds the Correct Episode File by Matching Season & Episode Number
# Against the Currently Processed Episode and Renames the File
Get-ChildItem -Path $UpdatedFolderPath -File -Recurse
| Where-Object {
(
$_.Name -match $(
($FullEpisodeNumber -match '[sS]\d{2}')
| Select-Object -First 1
# Returns Section of the String that the Regex Validated
| ForEach-Object { $Matches.Values }
)
) -and
(
$_.Name -match $(
($FullEpisodeNumber -match '[eE]\d{2}')
| Select-Object -First 1
# Returns Section of the String that the Regex Validated
| ForEach-Object { $Matches.Values }
)
)
}
# Rename the File Found to Correct Name Format
| ForEach-Object {
$NewName = ($FormatedTVShowName,$FullEpisodeNumber,$EpisodeTitle -join ' ') + $($_.extension)
try {
Rename-Item -Path $_ -NewName $NewName -ErrorAction Stop -PassThru
Write-Debug "Renamed File Name: $NewName"
}
catch {
Write-Error -Message "Unable to Rename / Moving File to $($TVShowInfo.name,$FullEpisodeNumber,$EpisodeTitle -join ' ')"
Write-Error -Message "Error Reason: $($Error[0].CategoryInfo.Reason)"
}
}
# Moves File to Correct Season Folder
| Move-Item -Destination $(
Join-Path -Path $UpdatedFolderPath `
-ChildPath $(
"Season {0:D2}" -f ([int]$FullEpisodeNumber.Substring(1, 2))
)
) -ErrorAction Continue
# Does the same lookup process as above, this time looking for subtitle files.
Get-ChildItem -Path $UpdatedFolderPath -Recurse
# Filter Results to Ether Directories or Files with Subtitle Files Extensions
| Where-Object {
$_.PSIsContainer -eq $true -or
$_.Extension -in @('.srt','.smi', '.ssa', '.ass', '.vtt', '.VOBSUB', '.pgs')}
# Process Files First so it Doesn't Have Problems with the Folder Subtitle Files Rename Process
| Sort-Object PSIsContainer
# Grab Result that Matches the Season and Episode Number being Processed
| Where-Object {
(
$_.Name -match $(
($FullEpisodeNumber -match '[sS]\d{2}')
| Select-Object -First 1
# Returns Section of the String that the Regex Validated
| ForEach-Object { $Matches.Values }
)
) -and
(
$_.Name -match $(
($FullEpisodeNumber -match '[eE]\d{2}')
| Select-Object -First 1
# Returns Section of the String that the Regex Validated
| ForEach-Object { $Matches.Values }
)
)
}
| ForEach-Object {
# Check if Currently ProcessedS Object is a Directory or File
if ($_.PSIsContainer) {
Write-Verbose "Currently Pocessing $_.Name Subtitle Directory"
# Grab List of Files in that Directory.
$_ | Get-ChildItem
# Filter out Wrong or Broken Subtitle Files, but keeks 0KB for Testing
| Where-Object {$_.Length -eq 0kb -or $_.Length -gt 5kb}
| Sort-Object $_.Name
| ForEach-Object -Begin {$count=1} -Process {
# Remove all Characters in Name before First Alph character and Replace English with en
$SubtitleName = ($_.BaseName.Replace('English','en') -Replace('[^a-z]+','')) + $(if($count -gt 1){".$count"})
# Grabs Episode Name
$EpisodeName = ($TVShowInfo.name,$FullEpisodeNumber,$EpisodeTitle -join ' ')
# Combines the strings
$NewName = ($EpisodeName, $SubtitleName -join '.') + $($_.extension)
# Renames the Files Found
try {
Rename-Item -Path $_ -NewName $NewName -ErrorAction Stop -PassThru
Write-Debug "Renamed File Name: $NewName"
}
catch {
Write-Error -Message "Unable to Rename / Moving File to $($TVShowInfo.name,$FullEpisodeNumber,$EpisodeTitle -join ' ')"
Write-Error -Message "Error Reason: $($Error[0].CategoryInfo.Reason)"
}
# Increment Count Counter
$count++
}
}
else {
# Results is a file.
$SubtitleName = ($_.BaseName.Replace('English','en') -Replace('[^a-z]+',''))
# Grabs Episode Name
$EpisodeName = ($TVShowInfo.name,$FullEpisodeNumber,$EpisodeTitle -join ' ')
# Combines the strings
$NewName = ($EpisodeName, $SubtitleName -join '.') + $($_.extension)
# Renames the Files
try {
Rename-Item -Path $_ -NewName $NewName -ErrorAction Stop -PassThru
Write-Debug "Renamed File Name: $NewName"
}
catch {
Write-Error -Message "Unable to Rename / Moving File to $($TVShowInfo.name,$FullEpisodeNumber,$EpisodeTitle -join ' ')"
Write-Error -Message "Error Reason: $($Error[0].CategoryInfo.Reason)"
}
}
}
# Moves File to Correct Season Folder
| Move-Item -Destination $(
Join-Path -Path $UpdatedFolderPath `
-ChildPath $(
"Season {0:D2}" -f ([int]$FullEpisodeNumber.Substring(1, 2))
)
) -ErrorAction Continue
}
}
}
END {
# Removes Empty Folders
Write-Verbose "Remove Empty Folders"
Get-ChildItem -Path $UpdatedFolderPath -Recurse
| Where-Object {
$_.PSIsContainer -and
@(Get-ChildItem -LiteralPath $_.Fullname -Recurse
| Where-Object { -not($_.PSIsContainer) }).Length -eq 0
}
| Remove-Item -Recurse
# Return Successful Exit Code
Exit 0
}