-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprofile.ps1
More file actions
executable file
·2053 lines (1826 loc) · 97.2 KB
/
profile.ps1
File metadata and controls
executable file
·2053 lines (1826 loc) · 97.2 KB
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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<# PowerShell Profile
Version: 1.7.2
Last Updated: 2025-03-30
Author: RuxUnderscore <https://github.com/ruxunderscore/>
License: MIT License
.SYNOPSIS
Advanced PowerShell profile with custom functions for file/media management,
Git integration, image processing, and system utilities.
.DESCRIPTION
This profile provides a rich set of functions and configurations aimed at
streamlining development workflows, media organization, and general PowerShell usage.
It includes features like:
- Robust helper functions for logging and admin checks (Note: Core helpers now reside in base profile).
- Advanced file/folder management: CBZ creation with metadata, PDF organization,
sequential image/video renaming (including Plex/Jellyfin standards),
permission management, numbered folder creation.
- Image processing via ImageMagick.
- Video metadata retrieval via ffprobe.
- Symbolic link creation for organizing 'favorite' folders.
- Wrappers for external tools (WinUtil, Hastebin).
- Conditional Git integration with helper functions and aliases.
- Extensive Comment-Based Help for functions.
- Dependency checking for required modules and external tools.
#>
<# Changelog:
- 2024-05-20: Initialized Changelog
- 2024-09-08: Disabled `Set-StrictMode -Version Latest` as it causes issues with some ps1 scripts outside of this profile.
- 2024-10-05: Added Rename-AnimeEpisodes function.
- 2025-03-29: Refactored output/error handling for consistency (Verbose, Information, LogMessage). Added Admin checks. Corrected linter warnings (UseNullComparison, UnusedVariable).
- 2025-03-29: Renamed `Rename-AnimeEpisodes` to `Rename-SeriesEpisodes` and `Rename-NewAnimeEpisode` to `Rename-NewSeriesEpisode` for broader use. Added Comment-Based Help examples/stubs.
- 2025-03-29: Added generic `New-NumberedFolders` function, refactored `New-ChapterFolders` and `New-SeasonFolders` to use it. Implemented `-WhatIf` support across modifying functions. Added full Comment-Based Help. Corrected logic error in Compress-ToCBZ.
- 2025-03-29: Centralized file extension lists into global variables ($global:Default*Extensions). Added `Reload-Profile` helper function (renamed from original concept) and alias (`reload`). Added aliases for common custom functions (`rimg`, `mpdf`, `cbz`, `cimg`). Resolved SuppressMessageAttribute issues by renaming.
- 2025-03-30: Parameterized metadata (Writer, Genre, AgeRating, Manga, Language) in `Compress-ToCBZ`. Corrected ComicInfo <Count> tag usage. Adjusted XML heredoc formatting for cleanliness.
- 2025-03-30: Added optional `-PublicationDate` parameter to `Compress-ToCBZ` with multi-format parsing to set Year/Month/Day in ComicInfo.xml.
- 2025-03-30: Updated header format, added License and Synopsis.
- 2025-03-30: Moved core helper functions (Write-LogMessage, Test-AdminRole) to Microsoft.Powershell_profile.ps1 (Base Profile). Moved Aliases region to end of script for better organization.
- 2025-04-07: Changed SeriesName handling in New-SeriesEpisodes and other fixes
- 2025-04-13: Make SeriesName lowercase in New-SeriesEpisodes.
#>
#region Configuration
using namespace System.Drawing
using namespace System.Drawing.Imaging
using namespace System.IO
# PowerShell Configuration
$ErrorActionPreference = 'Stop'
Set-PSReadLineOption -EditMode Windows
$ProgressPreference = 'Continue'
# Configure PowerShell history
$MaximumHistoryCount = 1000
Set-PSReadLineOption -HistorySearchCursorMovesToEnd
Set-PSReadLineOption -MaximumHistoryCount $MaximumHistoryCount
# Module imports and dependency checks
$requiredModules = @(
@{Name = 'PSReadLine'; MinimumVersion = '2.1.0' }
)
foreach ($module in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $module.Name |
Where-Object { $_.Version -ge $module.MinimumVersion })) {
Write-Warning "Required module $($module.Name) (>= $($module.MinimumVersion)) is not installed."
}
}
function Test-ExternalDependency {
<#
.SYNOPSIS
Checks if an external command-line tool is available in the PATH.
.DESCRIPTION
A helper function used during profile loading to verify if required external programs
(like git, ffprobe, magick, starship) can be found via Get-Command.
Outputs a warning message if the command is not found.
.PARAMETER Command
The name of the command to check (e.g., 'git', 'ffprobe').
.PARAMETER ErrorMessage
The warning message to display if the command is not found.
.OUTPUTS
System.Boolean - Returns $true if the command is found, $false otherwise.
.NOTES
Used internally by the profile setup region.
#>
param (
[string]$Command,
[string]$ErrorMessage
)
if (-not (Get-Command $Command -ErrorAction SilentlyContinue)) {
Write-Warning $ErrorMessage
return $false
}
return $true
}
$dependencies = @{
'ffprobe' = 'FFmpeg tools not found. Some video functions may not work.'
'starship' = 'Starship prompt not found. Default prompt will be used.'
'git' = 'Git not found. Version control functions will be disabled.'
'magick' = 'ImageMagick is not installed. Please install from https://imagemagick.org/'
}
$availableDependencies = @{}
foreach ($dep in $dependencies.GetEnumerator()) {
$availableDependencies[$dep.Key] = Test-ExternalDependency -Command $dep.Key -ErrorMessage $dep.Value
}
# -- START: Added Global Constants --
Write-Verbose "Defining global constants for file extensions..." # Optional verbose message
# Used for file processing/checks where only the extension is needed
$global:DefaultImageCheckExtensions = @('.webp', '.jpeg', '.jpg', '.png')
$global:DefaultVideoCheckExtensions = @('.mkv', '.mp4', '.avi', '.mov', '.wmv', '.m4v')
# Used specifically for Get-ChildItem -Filter parameter which often uses wildcards
$global:DefaultVideoFilterExtensions = @('*.mkv', '*.mp4', '*.avi', '*.mov', '*.wmv', '*.m4v')
# -- END: Added Global Constants --
#endregion
#region Helper Functions
# --- Load Shared Helper Functions ---
$HelperScriptPath = $null # Ensure variable is reset/scoped locally if needed
try {
# Construct path relative to the directory containing the currently executing profile script
$HelperScriptPath = Join-Path -Path $PSScriptRoot -ChildPath "HelperFunctions.ps1"
if (Test-Path $HelperScriptPath -PathType Leaf) {
Write-Verbose "Dot-sourcing helper functions from '$HelperScriptPath'..."
. $HelperScriptPath # Execute the helper script in the current scope
# Optional: Verify loading by checking if a key function exists now
if (Get-Command Write-LogMessage -ErrorAction SilentlyContinue) {
# Use the function now that it should be loaded (logs to Verbose stream)
Write-LogMessage -Message "Successfully loaded helper functions from '$HelperScriptPath'." -Level Information
} else {
# This warning indicates dot-sourcing ran but functions aren't defined - problem inside HelperFunctions.ps1?
Write-Warning "Dot-sourcing '$HelperScriptPath' seemed to complete, but key helper functions (like Write-LogMessage) are still not defined."
}
} else {
Write-Warning "Helper script not found at expected location: '$HelperScriptPath'. Some profile features may fail."
}
} catch {
Write-Error "FATAL: Failed to load critical helper script '$HelperScriptPath'. Profile loading aborted. Error: $_"
# Stop further profile execution if helpers are essential
throw "Critical helper functions failed to load."
}
# --- End Load ---
#endregion
#region Generic Functions
function New-NumberedFolders {
<#
.SYNOPSIS
Creates a sequence of numbered folders based on a specified naming format.
.DESCRIPTION
Generates folders for a range of numbers (MinNumber to MaxNumber), formatting the folder name
using the provided NameFormat string (which should include a format specifier like {0:D2} or {0:D3}).
.PARAMETER MinNumber
The starting number in the sequence. Mandatory.
.PARAMETER MaxNumber
The ending number in the sequence. Mandatory.
.PARAMETER NameFormat
A .NET format string used to generate the folder name. The number will be substituted for {0}.
Example: "Chapter {0:D3}", "season {0:D2}", "Item_{0:D4}". Mandatory.
.PARAMETER BasePath
The parent directory where the numbered folders should be created. Defaults to the current directory.
.EXAMPLE
PS C:\Manga\MySeries\Volume 1> New-NumberedFolders -MinNumber 1 -MaxNumber 10 -NameFormat "Chapter {0:D3}"
Creates folders 'Chapter 001' through 'Chapter 010'.
.EXAMPLE
PS C:\Shows\MySeries> New-NumberedFolders -MinNumber 1 -MaxNumber 5 -NameFormat "season {0:D2}" -BasePath C:\Shows\MySeries
Creates folders 'season 01' through 'season 05' inside 'C:\Shows\MySeries'.
.NOTES
- Uses New-Item -Force, so it won't error if a folder already exists.
- Supports -WhatIf to preview folder creation.
- Uses Write-Verbose for progress and Write-Information for summary.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $true)]
[int]$MinNumber,
[Parameter(Mandatory = $true)]
[int]$MaxNumber,
[Parameter(Mandatory = $true)]
[string]$NameFormat, # Example: "Chapter {0:D3}" or "season {0:D2}"
[Parameter(Mandatory = $false)]
[string]$BasePath = (Get-Location).Path
)
process {
Write-Verbose "Creating numbered folders from $MinNumber to $MaxNumber using format '$NameFormat' in '$BasePath'."
if ($MinNumber -gt $MaxNumber) {
Write-Warning "Minimum number ($MinNumber) is greater than maximum number ($MaxNumber). No folders will be created."
return
}
for ($i = $MinNumber; $i -le $MaxNumber; $i++) {
try {
# Format the folder name using the provided string and number padding
$folderName = $NameFormat -f $i
}
catch {
Write-LogMessage -Level Error -Message "Invalid NameFormat string provided: '$NameFormat'. Error: $_"
throw "Invalid NameFormat string." # Stop processing if format is bad
}
$BasePathClean = $BasePath.TrimEnd('\') # Ensure no trailing slash on base path
$folderPath = "$($BasePathClean)\$folderName"
if (-not (Test-Path -LiteralPath $folderPath -PathType Container)) {
# Check specifically for container
# Wrap New-Item
if ($PSCmdlet.ShouldProcess($folderPath, "Create Directory using format '$NameFormat'")) {
Write-Verbose "Creating folder: $folderPath"
try {
New-Item $folderPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
catch {
Write-LogMessage -Level Error -Message "Failed to create folder '$folderPath': $_"
# Decide whether to continue or stop; continuing seems reasonable for folder creation loop
}
}
}
else {
Write-Verbose "Folder already exists: $folderPath"
}
}
}
end {
Write-Information "Numbered folder creation completed for range $MinNumber-$MaxNumber in '$BasePath'."
}
}
#endregion
#region File Management Functions
function Compress-ToCBZ {
<#
.SYNOPSIS
Creates a Comic Book Zip (.cbz) archive from image files in the current directory,
automatically generating a ComicInfo.xml metadata file.
.DESCRIPTION
This function assumes a specific parent/grandparent folder structure to determine
Series Title, Volume Number, and Chapter Number for the metadata.
Structure 1: Grandparent (Series) \ Parent (Volume X) \ Current (Chapter Y)
Structure 2: Parent (Series) \ Current (Chapter Y) (Assumes Volume 1)
It counts the image files (excluding any existing ComicInfo.xml) for the PageCount,
creates a ComicInfo.xml file with derived metadata and allows overriding defaults/providing values
for Genre, AgeRating, Language, Manga format, Writer/Artist, and Publication Date via parameters.
It compresses all files in the current directory (including the temporary XML) into a .cbz file
named 'Series Vol.XXX Ch.XXX.cbz' (padded numbers), places it in the parent directory, and finally removes the temporary ComicInfo.xml.
.PARAMETER Path
The path to the chapter directory containing the images. Defaults to the current directory (".").
.PARAMETER Force
Switch parameter. If specified, allows overwriting an existing .cbz file with the same name.
.PARAMETER seriesWriter
[Optional] Specifies the writer of the series for the ComicInfo.xml. Defaults to the same value for Penciller, Inker, Colorist, and CoverArtist.
.PARAMETER Genre
[Optional] Specifies the Genre tag for the ComicInfo.xml.
.PARAMETER AgeRating
[Optional] Specifies the AgeRating tag for the ComicInfo.xml.
.PARAMETER PublicationDate
[Optional] Specifies the publication date for the ComicInfo.xml (Year, Month, Day tags).
Accepts "YYYY-MM-DD".
If omitted or unparseable, the Year/Month/Day tags will be empty.
.PARAMETER LanguageISO
[Optional] Specifies the LanguageISO tag for the ComicInfo.xml (e.g., 'en', 'ja'). Defaults to 'en'.
.PARAMETER Manga
[Optional] Specifies the Manga tag ('Yes' or 'No'). Defaults to 'Yes' ($true). Use -Manga:$false for 'No'.
.EXAMPLE
PS C:\Comics\My Indie Comic\Chapter 001> Compress-ToCBZ -seriesWriter "Creator Name" -PublicationDate "2024-11-15"
Creates CBZ using specified writer and sets Year=2024, Month=11, Day=15 in ComicInfo.xml.
.NOTES
- Relies heavily on the parent/grandparent folder names matching 'Volume #' and 'Chapter #'.
- Uses Write-LogMessage for logging progress and errors.
- Supports -WhatIf.
- Publication Date parsing accepts "yyyy-MM-dd" as the format.
- If PublicationDate cannot be parsed, Year/Month/Day tags will be empty.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $false)]
[string]$Path = (Get-Location).Path, # Corrected default from "." to specific Path property
[Parameter(Mandatory = $false)]
[switch]$Force,
[Parameter(Mandatory = $false)]
[string]$seriesWriter,
[Parameter(Mandatory = $false)]
[string]$Genre,
[Parameter(Mandatory = $false)]
[string]$AgeRating,
[Parameter(Mandatory = $false)]
[string]$PublicationDate,
[Parameter(Mandatory = $false)]
[string]$LanguageISO = 'en',
[Parameter(Mandatory = $false)]
[bool]$Manga = $true # Default value ($true means 'Yes')
)
begin {
Write-LogMessage -Message "Starting CBZ compression for path: $Path" -Level Information
if (-not (Test-Path -Path $Path)) {
Write-LogMessage -Message "Path not found: $Path" -Level Error
return
}
}
process {
# Define $comicInfoXmlPath early so it's available in catch block for cleanup
$comicInfoXmlPath = Join-Path -Path $Path -ChildPath "ComicInfo.xml"
try {
# Get the current directory name and its parent
$currentDir = Split-Path -Leaf (Resolve-Path $Path)
$parentDir = Split-Path -Leaf (Split-Path -Parent (Resolve-Path $Path))
$grandparentDir = Split-Path -Leaf (Split-Path -Parent (Split-Path -Parent (Resolve-Path $Path)))
# Extract chapter number
if ($currentDir -match "Chapter (\d+)") {
$chapterNumber = [int]$Matches[1]
}
else {
throw "Unable to extract chapter number from folder name '$currentDir'." # Added context
}
# Determine volume number and series title
if ($parentDir -match "Volume (\d+)") {
$volumeNumber = [int]$Matches[1]
$seriesTitle = $grandparentDir
}
else {
# If parent isn't Volume, assume parent is Series, Volume is 1
$volumeNumber = 1
$seriesTitle = $parentDir
}
# --- START: Date Parsing Logic ---
$parsedDate = $null
$xmlYear = ''
$xmlMonth = ''
$xmlDay = ''
# Check if the user provided the parameter and it's not just whitespace
if ($PSBoundParameters.ContainsKey('PublicationDate') -and -not [string]::IsNullOrWhiteSpace($PublicationDate)) {
Write-Verbose "Attempting to parse PublicationDate: '$PublicationDate'"
try {
# Use only the single format string and InvariantCulture:
$parsedDate = [DateTime]::ParseExact($PublicationDate.Trim(), "yyyy-MM-dd", [System.Globalization.CultureInfo]::InvariantCulture) # <-- Simplified ParseExact
Write-Verbose "Successfully parsed PublicationDate '$PublicationDate' to '$($parsedDate.ToString('yyyy-MM-dd'))'."
# Extract components if parsing succeeded
$xmlYear = $parsedDate.Year.ToString() # Ensure it's a string
$xmlMonth = $parsedDate.Month.ToString("00")
$xmlDay = $parsedDate.Day.ToString("00")
}
catch {
# --- MODIFIED INNER CATCH: Use Write-Warning ---
# Log a warning if parsing fails, leave Year/Month/Day blank
Write-Warning "Could not parse provided PublicationDate '$PublicationDate' using format 'yyyy-MM-dd'. Year/Month/Day tags will be empty. Error: $($_.Exception.Message)"
# Reset variables just in case (should already be empty)
$xmlYear = ''
$xmlMonth = ''
$xmlDay = ''
}
}
else {
# If -PublicationDate not provided, intentionally leave Y/M/D empty
Write-Verbose "No PublicationDate provided. Year/Month/Day tags will be empty."
}
# --- END: Date Parsing Logic ---
$pageCount = (Get-ChildItem -Path $Path -File | Where-Object { $_.Name -ne "ComicInfo.xml" } | Measure-Object).Count # Added Path
# Create ComicInfo.xml content
$comicInfoXml = @"
<?xml version='1.0' encoding='utf-8'?>
<ComicInfo>
<Series>$seriesTitle</Series>
<LocalizedSeries></LocalizedSeries>
<Count></Count>
<Writer>$seriesWriter</Writer>
<Penciller>$seriesWriter</Penciller>
<Inker>$seriesWriter</Inker>
<Colorist>$seriesWriter</Colorist>
<Letterer></Letterer>
<CoverArtist>$seriesWriter</CoverArtist>
<Genre>$Genre</Genre>
<AgeRating>$AgeRating</AgeRating>
<Title>$seriesTitle</Title>
<Summary></Summary>
<Tags></Tags>
<Web></Web>
<Number>$chapterNumber</Number>
<Volume>$volumeNumber</Volume>
<Format></Format>
<Manga>$(if($Manga){'Yes'}else{'No'})</Manga>
<Year>$xmlYear</Year>
<Month>$xmlMonth</Month>
<Day>$xmlDay</Day>
<LanguageISO>$LanguageISO</LanguageISO>
<Notes>ComicInfo.xml created with Compress-ToCBZ on $(Get-Date -Format "yyyy-MM-dd")</Notes>
<PageCount>$pageCount</PageCount>
</ComicInfo>
"@
# Define CBZ path info ($comicInfoXmlPath defined earlier)
$cbzFileName = "{0} Vol.{1:D3} Ch.{2:D3}.cbz" -f $seriesTitle, $volumeNumber, $chapterNumber
# Place CBZ in the PARENT directory (e.g., the Volume folder or Series folder)
$parentPath = Split-Path -Parent (Resolve-Path $Path) # Get the immediate parent path
$cbzDestinationDir = Split-Path -Parent $parentPath # Get the parent of the parent
$cbzFullPath = Join-Path -Path $cbzDestinationDir -ChildPath $cbzFileName
if ((Test-Path $cbzFullPath) -and -not $Force) {
throw "CBZ file '$cbzFullPath' already exists. Use -Force to overwrite."
}
# Action 1: Save ComicInfo.xml
if ($PSCmdlet.ShouldProcess($comicInfoXmlPath, "Save temporary ComicInfo.xml")) {
$comicInfoXml | Out-File -FilePath $comicInfoXmlPath -Encoding utf8 -ErrorAction Stop
}
else {
Write-Warning "WhatIf: Skipping CBZ creation as temporary ComicInfo.xml was not saved."
return
}
# Action 2: Compress files to CBZ
if ($PSCmdlet.ShouldProcess($cbzFullPath, "Create CBZ archive from contents of '$($Path)'")) {
# Ensure we are compressing items *inside* the target Path
Compress-Archive -Path (Join-Path -Path $Path -ChildPath '*') -DestinationPath $cbzFullPath -Force:$Force -ErrorAction Stop # Pass $Force switch
Write-LogMessage -Message "Created $cbzFileName (in $cbzDestinationDir) with $pageCount pages" -Level Information
Write-Verbose "Successfully created '$cbzFileName' in '$cbzDestinationDir'."
}
# Action 3: Clean up ComicInfo.xml (only if it was actually created)
if (Test-Path $comicInfoXmlPath) {
if ($PSCmdlet.ShouldProcess($comicInfoXmlPath, "Remove temporary ComicInfo.xml")) {
Remove-Item -Path $comicInfoXmlPath -Force -ErrorAction Stop
Write-Verbose "Removed temporary '$comicInfoXmlPath'."
}
}
} # End of main 'try' block
# --- MODIFIED OUTER CATCH BLOCK ---
catch {
Write-Warning "DEBUG: Outer catch triggered. Original error follows:"
# Attempt cleanup (moved before throw)
if ($PSCmdlet.ShouldProcess($comicInfoXmlPath, "Attempt cleanup of temporary ComicInfo.xml after error")) {
if (Test-Path $comicInfoXmlPath) { Remove-Item -Path $comicInfoXmlPath -Force -ErrorAction SilentlyContinue }
}
throw # Let PowerShell print the original error ($_)
}
# --- END OF MODIFIED OUTER CATCH BLOCK ---
} # End of 'process' block
end {
Write-Information "CBZ compression process completed for path '$Path'."
Write-Information "MangaManager is recommended to make any further adjustments to the created CBZ file's metadata (ComicInfo.xml)." # Suggestion for next steps
Write-Information "Get it here: https://github.com/MangaManagerORG/Manga-Manager"
}
} # End of function Compress-ToCBZ
function Move-PDFsToFolders {
<#
.SYNOPSIS
Moves PDF files found in a directory into new subfolders named after each PDF's base name.
.DESCRIPTION
Scans the specified directory for files with the '.pdf' extension. For each PDF found,
it creates a new subdirectory named identically to the PDF file (excluding the extension)
and then moves the PDF file into that newly created subdirectory.
.PARAMETER DirectoryPath
The path to the directory containing the PDF files. Defaults to the current directory.
Can accept input from the pipeline.
.PARAMETER Force
Switch parameter. If specified, allows overwriting a file if it somehow already exists
in the target subfolder (e.g., from a previous run). Also forces creation of the subfolder.
.EXAMPLE
PS C:\Downloads> Move-PDFsToFolders
Moves 'MyDocument.pdf' into 'C:\Downloads\MyDocument\MyDocument.pdf', 'Another.pdf' into 'C:\Downloads\Another\Another.pdf', etc.
.EXAMPLE
PS C:\Docs> Get-ChildItem -Filter *.pdf | Move-PDFsToFolders -Force -WhatIf
Shows which PDFs in C:\Docs would be moved into subfolders, overwriting if necessary, without actually moving them.
.INPUTS
System.String[] - Can accept directory paths via pipeline.
.NOTES
- Supports -WhatIf and -Confirm through CmdletBinding.
- Uses Write-LogMessage for logging progress, warnings (file exists), and errors.
#>
[CmdletBinding(SupportsShouldProcess = $true)] # Already present
param (
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[string]$DirectoryPath = (Get-Location).Path,
[switch]$Force
)
begin {
Write-LogMessage -Message "Starting PDF organization in: $DirectoryPath" -Level Information
}
process {
try {
if (-not (Test-Path -LiteralPath $DirectoryPath)) {
throw "Directory not found: $DirectoryPath"
}
$pdfFiles = Get-ChildItem -LiteralPath $DirectoryPath -Filter *.pdf -File # Ensure only files
foreach ($pdfFile in $pdfFiles) {
$folderPath = Join-Path -Path $DirectoryPath -ChildPath $pdfFile.BaseName
$destination = Join-Path -Path $folderPath -ChildPath $pdfFile.Name
if ((Test-Path -LiteralPath $destination) -and -not $Force) {
Write-LogMessage -Message "File already exists: $destination. Skipping." -Level Warning
continue
}
# Check if folder needs creating *before* processing the file move
$folderExists = Test-Path -LiteralPath $folderPath -PathType Container
if (-not $folderExists) {
# Wrap New-Item
if ($PSCmdlet.ShouldProcess($folderPath, "Create directory")) {
New-Item -ItemType Directory -Path $folderPath -Force | Out-Null
Write-Verbose "Created directory '$folderPath'."
$folderExists = $true # Assume success for subsequent move check
}
else {
# If WhatIf prevents folder creation, cannot move file
Write-Warning "WhatIf: Skipping move of '$($pdfFile.Name)' as directory '$folderPath' would not be created."
continue
}
}
# Wrap Move-Item (already correctly wrapped from previous script)
if ($folderExists -and $PSCmdlet.ShouldProcess($pdfFile.FullName, "Move to $destination")) {
Move-Item -LiteralPath $pdfFile.FullName -Destination $destination -Force:$Force # -Force used for overwrite if -Force switch is passed
Write-LogMessage -Message "Moved $($pdfFile.Name) to $folderPath" -Level Information
Write-Verbose "Successfully moved '$($pdfFile.Name)' to '$destination'."
}
}
}
catch {
Write-LogMessage -Message "Error moving PDFs: $_" -Level Error
throw
}
}
end {
Write-Information "PDF organization process completed for path '$DirectoryPath'."
}
}
function Rename-ImageFilesSequentially {
<#
.SYNOPSIS
Renames image files in a directory to a sequential, zero-padded numeric format (e.g., 001.jpg).
.DESCRIPTION
Finds image files (.webp, .jpeg, .jpg, .png) in the specified directory.
It sorts the files based first on any leading numbers (treating '10a' after '10'), then alphabetically for non-numeric names.
It uses a temporary subdirectory ('TempRename') to avoid naming collisions during the process.
Files are moved to the temp directory with sequential names (e.g., 001.ext, 002.ext) and then moved back to the original directory.
.PARAMETER Path
The directory containing the image files to rename. Defaults to the current directory.
.PARAMETER LeadingZeros
The number of digits to use for the sequential number, padded with leading zeros. Defaults to 3 (e.g., 001, 002 ... 010 ... 100).
.EXAMPLE
PS C:\Images> Rename-ImageFilesSequentially
Renames image files like 'cover.jpg', 'page1.png', 'page10.png', 'page2.png' into '001.jpg', '002.png', '003.png', '004.png' (order depends on sorting).
.EXAMPLE
PS C:\Scans> Rename-ImageFilesSequentially -LeadingZeros 4
Renames images sequentially starting from '0001.webp', '0002.jpg', etc.
.NOTES
- Supports -WhatIf and -Confirm through CmdletBinding.
- Handles common image extensions: .webp, .jpeg, .jpg, .png.
- Sorting logic prioritizes numeric names, then number+letter names, then others.
- Uses a temporary subfolder named 'TempRename' which is created and removed automatically.
- Uses Write-Verbose for detailed steps, Write-Information for summary, and Write-LogMessage for errors.
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param (
[Parameter(Mandatory = $false, Position = 0)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$Path = ".",
[Parameter(Mandatory = $false)]
[int]$LeadingZeros = 3 # Default to three leading zeros
)
$imageExtensions = $global:DefaultImageCheckExtensions
$tempDirName = "TempRename"
$tempPath = Join-Path -Path $Path -ChildPath $tempDirName
$nameFormat = "{0:D$LeadingZeros}"
$counter = 1
# Create a temporary directory if it doesn't exist
if (-not (Test-Path -LiteralPath $tempPath -PathType Container)) {
# Minor change: Wrap New-Item for temp dir creation (low impact)
if ($PSCmdlet.ShouldProcess($tempPath, "Create temporary directory")) {
Write-Verbose "Creating temporary directory: '$tempPath'"
New-Item -Path $tempPath -ItemType Directory -Force | Out-Null
}
else {
Write-Warning "WhatIf: Cannot proceed without creating temporary directory '$tempPath'."
return
}
}
# Get all image files
$imageFiles = Get-ChildItem -LiteralPath $Path | Where-Object { -not $_.PSIsContainer -and $_.Extension -in $imageExtensions }
# Sort the image files based on the described naming conventions
$sortedImageFiles = $imageFiles | Sort-Object {
$baseName = $_.BaseName
if ($baseName -match '^\d+$') {
# Pure number: sort as integer
[int]$baseName
}
elseif ($baseName -match '^(\d+)([a-zA-Z]+)$') {
# Number with sub-letter: sort by number then letter
[int]$Matches[1], $Matches[2]
}
else {
# Non-numeric or other patterns: push to the end with original name as tie-breaker
[int]::MaxValue, $baseName
}
}, Name
Write-Verbose "Found $($sortedImageFiles.Count) image files to process."
Write-Verbose "Moving sorted image files to temporary directory and renaming sequentially."
foreach ($file in $sortedImageFiles) {
$extension = $file.Extension
$newNameBase = $nameFormat -f $counter
$newTempPath = Join-Path -Path $tempPath -ChildPath "$newNameBase$extension"
Write-Verbose "Moving and renaming '$($file.Name)' to '$newNameBase$extension' in temporary directory."
if ($PSCmdlet.ShouldProcess($file.Name, "Rename to '$newNameBase$extension' in temporary directory '$tempPath'")) {
try {
Move-Item -Path $file.FullName -Destination $newTempPath -Force -ErrorAction Stop
Write-Verbose "Successfully moved and renamed '$($file.Name)' to '$newNameBase$extension' in temporary directory."
$counter++
}
catch { Write-LogMessage -Level Error -Message "Error moving and renaming '$($file.Name)': $($_.Exception.Message)" }
}
}
Write-Verbose "Moving sequentially renamed files back to the original directory."
# Get the sequentially named files from the temporary directory
$renamedFiles = Get-ChildItem -LiteralPath $tempPath | Where-Object { -not $_.PSIsContainer -and $_.Extension -in $imageExtensions }
foreach ($file in $renamedFiles) {
$destinationPath = Join-Path -Path $Path -ChildPath $file.Name
Write-Verbose "Moving '$($file.Name)' from temporary directory to '$destinationPath'."
if ($PSCmdlet.ShouldProcess($file.Name, "Move from temporary directory to original location '$destinationPath'")) {
try {
Move-Item -Path $file.FullName -Destination $destinationPath -Force -ErrorAction Stop
Write-Verbose "Successfully moved '$($file.Name)' back to original directory."
}
catch { Write-LogMessage -Level Error -Message "Error moving '$($file.Name)' back from temporary directory: $($_.Exception.Message)" }
}
}
# Clean up the temporary directory
Write-Verbose "Cleaning up temporary directory: '$tempPath'"
# Wrap Remove-Item
if (Test-Path -LiteralPath $tempPath) {
# Check if it exists (it might not if -WhatIf stopped creation)
if ($PSCmdlet.ShouldProcess($tempPath, "Remove temporary directory (Recursive)")) {
Remove-Item -Path $tempPath -Recurse -Force | Out-Null
Write-Verbose "Successfully removed temporary directory '$tempPath'."
}
}
# CONSISTENCY: Use Write-Information for final summary/status
Write-Information "Sequential renaming process completed for path '$Path'."
}
function Rename-NumberedFiles {
<#
.SYNOPSIS
Renames files with purely numeric basenames in a directory to have consistent zero-padding.
.DESCRIPTION
Finds all files in the specified path whose base names consist only of digits (e.g., '1.txt', '10.jpg', '05.png').
It calculates the maximum number of digits found across all such files (e.g., 2 if '10.jpg' is the highest)
and renames each numeric file to pad its name with leading zeros up to that maximum length (e.g., '1.txt' -> '01.txt', '10.jpg' -> '10.jpg').
.PARAMETER Path
The directory path containing the files to rename. Defaults to the current directory.
.EXAMPLE
PS C:\Data> Rename-NumberedFiles
If files '1.dat', '5.dat', '12.dat' exist, they will be renamed to '01.dat', '05.dat', '12.dat'.
.NOTES
- Supports -WhatIf (if added via CmdletBinding/ShouldProcess).
- Only affects files whose base name contains *only* digits. 'File1.txt' or '1a.jpg' are ignored.
- Uses Write-Verbose for detailed steps and Write-Information for summary. Logs warnings/errors via Write-LogMessage.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[string]$Path = "."
)
# Get all files in the specified path
$files = Get-ChildItem -Path $Path -File -ErrorAction SilentlyContinue
# Filter files with numeric names
$numericFiles = $files | Where-Object { $_.BaseName -match '^\d+$' }
# Sort files numerically
$sortedFiles = $numericFiles | Sort-Object { [int]($_.BaseName) }
if ($sortedFiles.Count -eq 0) {
Write-Verbose "No files with purely numeric names found in '$Path'."
return
}
# Get the maximum number of digits
$maxDigits = ($sortedFiles | Measure-Object -Property BaseName -Maximum).Maximum.ToString().Length
Write-Verbose "Found $($sortedFiles.Count) numeric files. Padding to $maxDigits digits."
# Rename files
foreach ($file in $sortedFiles) {
$newName = "{0:D$maxDigits}$($file.Extension)" -f [int]($file.BaseName)
# Avoid renaming if the name is already correct
if ($file.Name -eq $newName) {
Write-Verbose "Skipping '$($file.Name)' as it already has the correct padding."
continue
}
try {
# Wrap Rename-Item
if ($PSCmdlet.ShouldProcess($file.FullName, "Rename to '$newName'")) {
Rename-Item -Path $file.FullName -NewName $newName -Force -ErrorAction Stop
Write-Verbose "Renamed: $($file.Name) -> $newName"
}
}
catch {
Write-LogMessage -Level Warning -Message "Failed to rename $($file.Name): $_"
}
}
Write-Information "Numeric file renaming process completed for path '$Path'."
}
function Set-StandardSeasonFolderNames {
<#
.SYNOPSIS
Renames season folders in the current directory to a standard 'season XX' format.
.DESCRIPTION
Searches the current directory for folders whose names match patterns like 'season 1', 'Season_02', etc.
It extracts the season number and renames the folder to the standard 'season XX' format (e.g., 'season 01', 'season 02'), ensuring a two-digit padded number.
.EXAMPLE
PS C:\MySeries> Set-StandardSeasonFolderNames
Looks for folders like 'season 1', 'season_2', 'Season 03' in C:\MySeries and renames them to
'season 01', 'season 02', 'season 03' respectively.
.NOTES
- Only operates in the current directory.
- Looks for folders starting with 'season' (case-insensitive), potentially followed by whitespace or underscore, then digits.
- Renames are done using Rename-Item -Force.
- Uses Write-Verbose for detailed output and Write-Information for summary.
- Errors are logged using Write-LogMessage.
- Supports -WhatIf (if added via CmdletBinding/ShouldProcess).
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param ()
# Get the current directory
$Path = (Get-Location).Path; Write-Verbose "Standardizing season folder names in: $Path"
# Get all directories in the current path that match the pattern "season <number>"
$seasonFolders = Get-ChildItem -LiteralPath $Path -Directory | Where-Object { $_.Name -match '(?i)^season[_\s]*\d+$' }
if ($seasonFolders.Count -eq 0) { Write-Verbose "No folders matching the pattern 'season [number]' found."; return }
Write-Verbose "Found potential season folders:"; $seasonFolders | Format-Table -Property Name, FullName | Out-String | Write-Verbose
foreach ($folder in $seasonFolders) {
# Extract the season number from the folder name
if ($folder.Name -match '(?i)season[_\s]*(\d+)') {
$seasonNumber = "{0:D2}" -f [int]$Matches[1] # Format the season number with leading zero if necessary
$newName = "season $seasonNumber" # Construct the new standardized folder name
$parentPath = Split-Path -Path $folder.FullName -Parent # Get the parent path of the folder
# Ensure the parent path is not empty
if (-not [string]::IsNullOrEmpty($parentPath)) {
$newPath = Join-Path -Path $parentPath -ChildPath $newName # Construct the full path for the new folder name
# Rename the folder if the new name is different from the current name
if ($folder.Name -ne $newName) {
try {
# Wrap Rename-Item
# Note: $newPath contains the full destination path including the new name
if ($PSCmdlet.ShouldProcess($folder.FullName, "Rename folder to '$newName' (full path: '$newPath')")) {
Rename-Item -LiteralPath $folder.FullName -NewName $newPath -Force -ErrorAction Stop
Write-Verbose "Renamed: $($folder.Name) -> $newName"
}
}
catch { Write-LogMessage -Level Error -Message "Failed to rename $($folder.Name): $_" }
}
else {
# CONSISTENCY: Use Write-Verbose for info
Write-Verbose "No renaming needed for: $($folder.Name)"
}
}
else {
# CONSISTENCY: Use Write-LogMessage for errors
Write-LogMessage -Level Error -Message "Failed to get parent path for folder: $($folder.FullName)"
}
}
else {
# This case should ideally not be hit due to the Where-Object filter, but good for safety
Write-LogMessage -Level Warning -Message "Could not extract season number from folder name (unexpected): $($folder.Name)"
}
}
# CONSISTENCY: Use Write-Information for final summary
Write-Information "Season folder name standardization complete!"
}
function Rename-SeriesEpisodes {
<#
.SYNOPSIS
Renames video files within season folders (or the current folder) to a standard series episode format.
.DESCRIPTION
This function processes video files (mkv, mp4, avi, etc.) located within 'season XX' subfolders of the current directory,
or directly within the current directory if no season folders are found.
It renames the files sequentially to the standard Plex/Jellyfin format: 'series_name_sXXeYY.ext'.
The series name is derived from the first file found unless provided via the SeriesName parameter.
Season numbers (sXX) are derived from the 'season XX' folder names or use the DefaultSeason parameter.
Episode numbers (eYY) are assigned sequentially based on the sorted file list within each season.
The function attempts to clean common group tags (e.g., '[Group]') from the beginning of original filenames
before attempting to derive the series name.
.PARAMETER SeriesName
An optional string specifying the series name to use in the renamed files. Spaces will be replaced with underscores.
If not provided, the function attempts to derive the name from the first video file found in the first season folder (or current directory).
.PARAMETER DefaultSeason
The season number (integer) to use if processing files outside of a 'season XX' folder structure, or if a folder name doesn't match the pattern. Defaults to 1.
.EXAMPLE
PS C:\Path\To\My Show> Rename-SeriesEpisodes
Processes 'season 01', 'season 02' subfolders, derives the series name, and renames episodes like 'my_show_s01e01.mkv', 'my_show_s02e01.mkv', etc.
.EXAMPLE
PS C:\Path\To\My Show\season 03> Rename-SeriesEpisodes -SeriesName "My Awesome Show"
Processes only the current 'season 03' folder, using the provided series name, resulting in files like 'my_awesome_show_s03e01.mkv', etc.
.EXAMPLE
PS C:\Path\To\My Show\Specials> Rename-SeriesEpisodes -DefaultSeason 0
Processes files in the current 'Specials' directory, using season number 0 (s00) and deriving the series name, e.g., 'my_show_s00e01.mkv'.
.NOTES
- Supported video extensions: .mkv, .mp4, .avi, .mov, .wmv, .m4v.
- Deriving the series name from filenames can be unreliable if filenames are inconsistent. Providing -SeriesName is recommended for accuracy.
- Files are sorted by name before assigning episode numbers. Ensure original files sort correctly for proper numbering.
- Uses Write-Verbose for detailed steps, Write-Information for summary, Write-Warning for non-critical issues, and Write-LogMessage for errors.
- Supports -WhatIf (if added via CmdletBinding/ShouldProcess).
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $false)]
[string]$SeriesName,
[Parameter(Mandatory = $false)]
[int]$DefaultSeason = 1
)
# Define supported video file extensions
$videoExtensions = $global:DefaultVideoFilterExtensions
# Format SeriesName if provided (replace spaces with underscores)
if ($SeriesName) {
$SeriesName = $SeriesName.ToLower() -replace '\s+', '_'
Write-Verbose "Using provided SeriesName: $SeriesName"
}
# Get the current directory
$BaseDirectory = (Get-Location).Path
Write-Verbose "Base directory: $BaseDirectory"
# Check if the current directory is a season folder
if ($BaseDirectory -match '(?i)(\\season\s*\d+)$') {
# Simplified regex
Write-Verbose "Running within a season folder: $(Split-Path -Leaf $BaseDirectory)"
$seasonFolders = @((New-Object -TypeName PSObject -Property @{ Name = (Split-Path -Leaf $BaseDirectory); FullName = $BaseDirectory }))
}
else {
# Get all season folders
Write-Verbose "Searching for season folders in $BaseDirectory"
$seasonFolders = Get-ChildItem -Path $BaseDirectory -Directory | Where-Object { $_.Name -match '(?i)^season\s*\d+$' }
if ($seasonFolders.Count -eq 0) {
Write-LogMessage -Level Warning -Message "No season folders found in $BaseDirectory. Processing files in base directory using default season number: $DefaultSeason"
# Create a dummy object to represent the base directory as the folder to process
$seasonFolders = @((New-Object -TypeName PSObject -Property @{ Name = "DefaultSeason"; FullName = $BaseDirectory }))
}
else {
Write-Verbose "Found $($seasonFolders.Count) season folders to process."
}
}
# Determine SeriesName from first file if not provided (do this once)
$derivedSeriesName = $null
if (-not $SeriesName -and $seasonFolders.Count -gt 0) {
Write-Verbose "SeriesName not provided, attempting to derive from first video file..."
$firstFolderFiles = @()
foreach ($ext in $videoExtensions) {
$firstFolderFiles += Get-ChildItem -LiteralPath $seasonFolders[0].FullName -Filter $ext -File -ErrorAction SilentlyContinue
}
if ($firstFolderFiles.Count -gt 0) {
$firstFileSorted = $firstFolderFiles | Sort-Object Name | Select-Object -First 1
$cleanFirstName = $firstFileSorted.Name -replace '(?i)^\[.*?\]\s*', ''
if ($cleanFirstName -match '(?i)^(.*?)\s*(-|\[|\.)') {
# Added period as separator
$derivedSeriesName = ($Matches[1].Trim() -replace '\s+', '_').ToLower()
Write-Verbose "Derived SeriesName: $derivedSeriesName"
$effectiveSeriesName = $derivedSeriesName
}
else {
Write-LogMessage -Level Warning -Message "Could not derive series name from first file: $($firstFileSorted.Name)"
}
}
else {
Write-LogMessage -Level Warning -Message "No video files found in first folder ($($seasonFolders[0].Name)) to derive series name."
}
}
else {
$effectiveSeriesName = $SeriesName
}
# Use provided SeriesName or the derived one
# $effectiveSeriesName = $SeriesName -or $derivedSeriesName
if (-not $effectiveSeriesName) {
Write-LogMessage -Level Error -Message "Cannot proceed without a SeriesName (either provide one or ensure files allow derivation)."
return
}
foreach ($seasonFolder in $seasonFolders) {
Write-Verbose "Processing folder: $($seasonFolder.FullName)"
# Extract the season number from the folder name or use the default season number
$folderSeason = $DefaultSeason
if ($seasonFolder.Name -match '(?i)season\s*(\d+)') {
# Match specific 'season' prefix
$folderSeason = [int]$Matches[1]
}
elseif ($seasonFolder.Name -eq "DefaultSeason") {
# Keep default season number if it's the dummy object
Write-Verbose "Using default season number: $DefaultSeason"
}
else {
Write-LogMessage -Level Warning -Message "Folder '$($seasonFolder.Name)' doesn't match 'season XX' format, using default season $DefaultSeason."
}
Write-Verbose "Using Season Number: $folderSeason"
# Get all video files in the current season folder
$files = @()
foreach ($ext in $videoExtensions) {
$files += Get-ChildItem -LiteralPath $seasonFolder.FullName -Filter $ext -File -ErrorAction SilentlyContinue # Added -File and SilentlyContinue
}
if ($files.Count -eq 0) {
Write-Verbose "No video files found in folder: $($seasonFolder.FullName)"
continue # Move to the next season folder
}
# Sort files by name to attempt correct episode numbering
$sortedFiles = $files | Sort-Object Name
# Initialize episode counter
$episodeCounter = 1
foreach ($file in $sortedFiles) {
# Get the file extension from the original file
$extension = $file.Extension.ToLower()
# Construct new file name