<# .SYNOPSIS The scraper has 3 main parts: The GetDirectoryName, Search-IMDbMedia and Rename The script is divided in region blocks (#region, #endregion) for better readability and to keep the code fragments small otherwise the Webbrowser will corrupt the layout. The main flow is: 1. User selects a directory whit the name of the serie. 2. Scraper is called which retrieves the info for the Serie from IMDB 3. Rename is called with a list of episodes names from IMDB which are used to rename the files in the folder #> #region - Block 01 - Load the required WPF Windows libraries in Add-Type -AssemblyName PresentationFramework Add-Type -AssemblyName PresentationCore Add-Type -AssemblyName WindowsBase #endregion #region - Block A01 - GetDirectoryName() # ============================================================================== # REMARK: GetDirectoryName # DESCRIPTION: Initializes a WPF UI interface allowing users to extract and # select a specific subfolder segment from a given filesystem path. # PARAMETERS: # - $DirectoryPath: The raw string path pointing to a directory or file node. # LOGIC: Normalizes input strings, strips corrupted double drive letters, # parses structural components into array tokens, and configures # the baseline layout parameters for a modular breadcrumb selector. # ============================================================================== function GetDirectoryName { param ( [Parameter(Mandatory = $true)] [string]$DirectoryPath ) # Restore duplicate drive letters at the beginning if ($DirectoryPath -match '^([A-Za-z]:\\)\1') { $DirectoryPath = $DirectoryPath -replace ` '^([A-Za-z]:\\)\1', '$1' } elseif ($DirectoryPath -match '^([A-Za-z]:\\)[A-Za-z]:') { $DirectoryPath = $DirectoryPath -replace ` '^([A-Za-z]:\\)[A-Za-z]:', '$1' } # Check whether the path points to a file or a folder if (Test-Path -Path $DirectoryPath -PathType Leaf) { $CleanPath = Split-Path -Path $DirectoryPath -Parent } else { $CleanPath = [System.IO.Path]::GetFullPath($DirectoryPath) } # Split path into individual folders $Parts = $CleanPath.Split( ` [System.IO.Path]::DirectorySeparatorChar, ` [System.StringSplitOptions]::RemoveEmptyEntries) $DriveRoot = [System.IO.Path]::GetPathRoot($CleanPath) # Colors for the subfolders $FolderColors = @("#0066CC", "#D02090", "#008000", "#FF4500", ` "#8A2BE2", "#D2691E", "#008080", "#4682B4") $ColorIndex = 0 # Build the GUI manually with objects $Global:BreadcrumbWindow = [System.Windows.Window]::new() $Global:BreadcrumbWindow.Title = "Select Subfolder Node" $Global:BreadcrumbWindow.Height = 165 $Global:BreadcrumbWindow.Width = 650 $Global:BreadcrumbWindow.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen $Global:BreadcrumbWindow.Topmost = $true $Global:BreadcrumbWindow.Background = ` [System.Windows.Media.Brushes]::White $Grid = [System.Windows.Controls.Grid]::new() $Grid.Margin = [System.Windows.Thickness]::new(15, 5, 15, 5) # Create four vertical structural layout grid rows # RESTORED: Reverted to your original inline loop format logic explicitly for ($r = 0; $r -lt 4; $r++) { $Row = [System.Windows.Controls.RowDefinition]::new() $Row.Height = if ($r -eq 3) { [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) } else { [System.Windows.GridLength]::Auto } $Grid.RowDefinitions.Add($Row) | Out-Null } #endregion #region - Block A02 - # Initialize the instruction label component $Global:InstructionLabel = [System.Windows.Controls.TextBlock]::new() $Global:InstructionLabel.Text = "Select Moviename or Right-Click to Edit" $Global:InstructionLabel.FontWeight = [System.Windows.FontWeights]::SemiBold $Global:InstructionLabel.FontSize = 15 $Global:InstructionLabel.Margin = [System.Windows.Thickness]::new(0,5,0,3) $Global:InstructionLabel.Foreground = [System.Windows.Media.Brushes]::Black $Global:InstructionLabel.HorizontalAlignment = ` [System.Windows.HorizontalAlignment]::Center [System.Windows.Controls.Grid]::SetRow($Global:InstructionLabel, 0) $Grid.Children.Add($Global:InstructionLabel) | Out-Null # Divider Line $Divider = [System.Windows.Shapes.Rectangle]::new() $Divider.Height = 1 $Divider.Fill = [System.Windows.Media.Brushes]::LightGray $Divider.Margin = [System.Windows.Thickness]::new(0,0,0,5) [System.Windows.Controls.Grid]::SetRow($Divider, 1) $Grid.Children.Add($Divider) | Out-Null # HOVER & INLINE EDIT ROW PANEL INTERFACE: Full-Width Single-Cell layout grid $NameGrid = [System.Windows.Controls.Grid]::new() $NameGrid.Margin = [System.Windows.Thickness]::new(0,4,0,8) $ColFull = [System.Windows.Controls.ColumnDefinition]::new() $ColFull.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) $NameGrid.ColumnDefinitions.Add($ColFull) | Out-Null [System.Windows.Controls.Grid]::SetRow($NameGrid, 2) # Hover text block (Dynamic Color Matching) $Global:HoverValueTextBlock = [System.Windows.Controls.TextBlock]::new() $Global:HoverValueTextBlock.FontWeight = [System.Windows.FontWeights]::Bold $Global:HoverValueTextBlock.FontSize = 16 $Global:HoverValueTextBlock.VerticalAlignment = [System.Windows.VerticalAlignment]::Center $Global:HoverValueTextBlock.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left [System.Windows.Controls.Grid]::SetColumn($Global:HoverValueTextBlock, 0) $NameGrid.Children.Add($Global:HoverValueTextBlock) | Out-Null # INTEGRATED INLINE EDITOR: Nestled identically in Column 0 # RESTORED: The original, stable WPF TextBox with standard properties and native border handling $Global:EditTextBox = [System.Windows.Controls.TextBox]::new() $Global:EditTextBox.FontWeight = [System.Windows.FontWeights]::Bold $Global:EditTextBox.FontSize = 16 $Global:EditTextBox.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch $Global:EditTextBox.Visibility = [System.Windows.Visibility]::Collapsed $Global:EditTextBox.VerticalAlignment = [System.Windows.VerticalAlignment]::Center $Global:EditTextBox.BorderBrush = [System.Windows.Media.Brushes]::LightGray $Global:EditTextBox.Foreground = [System.Windows.Media.Brushes]::Red [System.Windows.Controls.Grid]::SetColumn($Global:EditTextBox, 0) $NameGrid.Children.Add($Global:EditTextBox) | Out-Null $Grid.Children.Add($NameGrid) | Out-Null # ScrollViewer for the folders (Row 3) $Global:FolderScrollViewer = [System.Windows.Controls.ScrollViewer]::new() $Global:FolderScrollViewer.VerticalScrollBarVisibility = [System.Windows.Controls.ScrollBarVisibility]::Auto $Global:FolderScrollViewer.HorizontalScrollBarVisibility = [System.Windows.Controls.ScrollBarVisibility]::Disabled [System.Windows.Controls.Grid]::SetRow($Global:FolderScrollViewer, 3) $TextBlock = [System.Windows.Controls.TextBlock]::new() $TextBlock.FontSize = 14 $TextBlock.TextWrapping = [System.Windows.TextWrapping]::Wrap $TextBlock.VerticalAlignment = [System.Windows.VerticalAlignment]::Center $Global:FolderScrollViewer.Content = $TextBlock $Grid.Children.Add($Global:FolderScrollViewer) | Out-Null # Finalize window core content mapping state pipelines $Global:BreadcrumbWindow.Content = $Grid $Script:SelectedFolderName = $null $Script:IsEditing = $false #endregion #region - Block A03 - # ============================================================================== # REMARK: New-PureBlackSlash # DESCRIPTION: Instantiates a standardized bold black structural backslash delimiter # used to cleanly separate visual folder breadcrumb nodes. # RETURN: A configured System.Windows.Documents.Run text element. # ============================================================================== function New-PureBlackSlash { $Slash = [System.Windows.Documents.Run]::new("\") $Slash.Foreground = [System.Windows.Media.Brushes]::Black $Slash.FontWeight = [System.Windows.FontWeights]::Bold return $Slash } # Shared Click Handler (Left Mouse Button) $ClickHandler = [System.Windows.RoutedEventHandler]{ if (-not $Script:IsEditing) { $Script:SelectedFolderName = $this.Tag $Global:BreadcrumbWindow.Close() } } # Right Mouse Button Handler (Initiates Inline Edit Mode inside the Name Field) $RightClickHandler = [System.Windows.Input.MouseButtonEventHandler]{ $Script:IsEditing = $true $Global:InstructionLabel.Text = "Change name and Enter to accept or ESC to go back to selecting" $Global:InstructionLabel.FontSize = 13 $Global:HoverValueTextBlock.Visibility = [System.Windows.Visibility]::Collapsed # FIXED: Synchronize input text color to match the exact active directory item color theme securely $Global:EditTextBox.Foreground = $this.Foreground $Global:EditTextBox.Text = $this.Tag $Global:EditTextBox.Visibility = [System.Windows.Visibility]::Visible $Global:EditTextBox.Focus() | Out-Null # Position the text selection cursor explicitly at the end of the text stream $Global:EditTextBox.CaretIndex = $Global:EditTextBox.Text.Length } # TextBox Keyboard Handler $Global:EditTextBox.Add_KeyDown({ if ($_.Key -eq [System.Windows.Input.Key]::Enter) { $Script:SelectedFolderName = $Global:EditTextBox.Text $Global:BreadcrumbWindow.Close() } elseif ($_.Key -eq [System.Windows.Input.Key]::Escape) { $Script:IsEditing = $false $Global:InstructionLabel.Text = "Select Moviename or Right-Click to Edit" $Global:InstructionLabel.FontSize = 15 $Global:EditTextBox.Visibility = [System.Windows.Visibility]::Collapsed $Global:HoverValueTextBlock.Text = "" $Global:HoverValueTextBlock.Visibility = [System.Windows.Visibility]::Visible } }) # Shared Hover Event Handlers # FIXED: The hover field now mirrors the exact original brush color of the directory item dynamic state $MouseEnterHandler = [System.Windows.Input.MouseEventHandler]{ if (-not $Script:IsEditing) { $Global:HoverValueTextBlock.Foreground = $this.Foreground $Global:HoverValueTextBlock.Text = $this.Tag } } $MouseLeaveHandler = [System.Windows.Input.MouseEventHandler]{ if (-not $Script:IsEditing) { $Global:HoverValueTextBlock.Text = "" } } #endregion #region - Block A04 - # Add Drive Root (e.g., "H:") # Extracts the baseline root component, configures the hyperlink look, # assigns unique color index steps, and registers mouse events context. $RootText = $DriveRoot.TrimEnd('\') $RootRun = [System.Windows.Documents.Run]::new($RootText) $RootHyperlink = [System.Windows.Documents.Hyperlink]::new($RootRun) $RootHex = $FolderColors[$ColorIndex++ % $FolderColors.Count] $RootColor = [System.Windows.Media.ColorConverter]:: ` ConvertFromString($RootHex) $RootHyperlink.Foreground = [System.Windows.Media.SolidColorBrush]:: ` new($RootColor) $RootHyperlink.TextDecorations = $null $RootHyperlink.Cursor = [System.Windows.Input.Cursors]::Hand $RootHyperlink.Tag = $RootText $RootHyperlink.CommandParameter = $RootHex $RootHyperlink.Add_MouseEnter($MouseEnterHandler) $RootHyperlink.Add_MouseLeave($MouseLeaveHandler) $RootHyperlink.Add_Click($ClickHandler) $RootHyperlink.Add_MouseRightButtonDown($RightClickHandler) $TextBlock.Inlines.Add($RootHyperlink) | Out-Null $TextBlock.Inlines.Add((New-PureBlackSlash)) | Out-Null # Loop through all subfolders # Iterates systematically through parsed segments to construct sequential hyperlink flow bindings. for ($i = 0; $i -lt $Parts.Count; $i++) { if ($i -eq 0 -and ($Parts[$i] + ":" -eq $RootText -or ` $Parts[$i] -eq $RootText)) { continue } $FolderName = $Parts[$i] $FolderRun = [System.Windows.Documents.Run]::new($FolderName) $FolderHyperlink = [System.Windows.Documents.Hyperlink]:: ` new($FolderRun) $HexColor = $FolderColors[$ColorIndex++ % $FolderColors.Count] $NormalColor = [System.Windows.Media.ColorConverter]:: ` ConvertFromString($HexColor) $FolderHyperlink.Foreground = [System.Windows.Media.SolidColorBrush]:: ` new($NormalColor) $FolderHyperlink.TextDecorations = $null $FolderHyperlink.Cursor = [System.Windows.Input.Cursors]::Hand $FolderHyperlink.Tag = $FolderName $FolderHyperlink.CommandParameter = $HexColor $FolderHyperlink.Add_MouseEnter($MouseEnterHandler) $FolderHyperlink.Add_MouseLeave($MouseLeaveHandler) $FolderHyperlink.Add_Click($ClickHandler) $FolderHyperlink.Add_MouseRightButtonDown($RightClickHandler) $TextBlock.Inlines.Add($FolderHyperlink) | Out-Null if ($i -lt ($Parts.Count - 1)) { $TextBlock.Inlines.Add((New-PureBlackSlash)) | Out-Null } } # Open the modal WPF dialog frame to freeze background runtime tracking pipelines $Global:BreadcrumbWindow.ShowDialog() | Out-Null # Clean up global variables # Purges volatile session pointers from memory map scopes to prevent memory leak degradation. Remove-Variable -Name BreadcrumbWindow -Scope Global ` -ErrorAction SilentlyContinue Remove-Variable -Name HoverValueTextBlock -Scope Global ` -ErrorAction SilentlyContinue Remove-Variable -Name EditTextBox -Scope Global ` -ErrorAction SilentlyContinue Remove-Variable -Name FolderScrollViewer -Scope Global ` -ErrorAction SilentlyContinue Remove-Variable -Name InstructionLabel -Scope Global ` -ErrorAction SilentlyContinue return $Script:SelectedFolderName } #endregion #region - Block B01 - New-MediaRatingStars() # ============================================================================== # REMARK: New-MediaRatingStars # DESCRIPTION: Generates a vector-based 5-star rating control inside a StackPanel. # PARAMETERS: # - $RatingString (String): The raw numeric rating string (e.g., "7.5" or "N/A"). # - $TargetStack (StackPanel): The layout container destination for the vector stars. # LOGIC: FIXED - Automatically colors ALL stars neutral gray (#D3D3D3) if the rating # is "N/A", empty, or zero, ensuring a uniform fallback look. # ============================================================================== function New-MediaRatingStars { param ( [string]$RatingString, [System.Windows.Controls.StackPanel]$TargetStack ) # Calculate score logic safely out of 5 stars total $NumericScore = 0.0 $IsNoData = [string]::IsNullOrWhiteSpace($RatingString) -or $RatingString -eq "N/A" -or $RatingString -eq "0" if (-not $IsNoData -and [double]::TryParse(($RatingString -replace '/10',''), [ref]$NumericScore)) { $NormalizedStarsValue = $NumericScore / 2.0 } else { $NormalizedStarsValue = 0.0 # Force zero stars if data is missing } # Standard vector graphics coordinates path tracking signature matching for a perfect star $StarGeometryPath = "M 10,0 L 13,7 L 20,7 L 15,12 L 17,19 L 10,15 L 3,19 L 5,12 L 0,7 L 7,7 Z" for ($i = 1; $i -le 5; $i++) { $ViewContainerBox = [System.Windows.Controls.Viewbox]::new() $ViewContainerBox.Width = 16 $ViewContainerBox.Height = 16 $ViewContainerBox.Margin = "0,0,2,0" $VectorPathShape = [System.Windows.Shapes.Path]::new() $VectorPathShape.Data = [System.Windows.Media.Geometry]::Parse($StarGeometryPath) $VectorPathShape.Stroke = [System.Windows.Media.Brushes]::DimGray $VectorPathShape.StrokeThickness = 0.5 # REGEL CONFIGURATION: Determine fill colors dynamically based on data state if ($IsNoData) { # Render completely neutral gray stars if media has no rating data $VectorPathShape.Fill = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#D3D3D3") } elseif ($i -le [math]::Floor($NormalizedStarsValue)) { $VectorPathShape.Fill = [System.Windows.Media.Brushes]::Gold } elseif ($i -eq [math]::Ceiling($NormalizedStarsValue) -and ($NormalizedStarsValue % 1 -ge 0.5)) { # Build a crisp linear gradient brush interface context for clean half-star renders $GradientBrush = [System.Windows.Media.LinearGradientBrush]::new() $GradientBrush.StartPoint = "0,0" $GradientBrush.EndPoint = "1,0" $GradientBrush.GradientStops.Add([System.Windows.Media.GradientStop]::new([System.Windows.Media.Brushes]::Gold.Color, 0.5)) $GradientBrush.GradientStops.Add([System.Windows.Media.GradientStop]::new([System.Windows.Media.BrushConverter]::new().ConvertFromString("#D3D3D3").Color, 0.5)) $VectorPathShape.Fill = $GradientBrush } else { $VectorPathShape.Fill = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#D3D3D3") } $ViewContainerBox.Child = $VectorPathShape $TargetStack.Children.Add($ViewContainerBox) | Out-Null } } #endregion #region - Block C01 - Invoke-OMDbSearch() # ============================================================================== # ROUTINE 1: Invoke-OMDbSearch # PURPOSE: Executes a broad-string search against the OMDb API endpoints. # RETURNS: A standard PowerShell array containing lightweight result objects. # PARAMETERS: # - $QueryText: The string pattern containing the movie/series title. # - $Type: Hard-validated media separator filtering ('movie' or 'series'). # - $ApiKey: Global authentication token for OMDb API access. # LOGIC: Forces Security Protocol to TLS1.2, strips invalid www subdomains, # and executes a secure parameters-bound GET request against OMDb. # ============================================================================== function Invoke-OMDbSearch { param ( [string]$QueryText, [string]$Type, [string]$ApiKey ) # HARD FIX: Force TLS 1.2 encryption protocol to prevent link negotiation rejections [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 # FIXED: Reconfigured to use the absolute official parameter dictionary layout targeting the base domain (NO WWW!) $Params = @{ s = $QueryText type = $Type apikey = $ApiKey } try { # Ship transmission request over secure layers with an isolated 10-second ceiling $Result = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body $Params -TimeoutSec 10 # Cast the structural inner search array directly into a flat generic pipeline array if ($Result.Response -eq "True" -and $Result.Search) { return @($Result.Search) } } catch { Write-Error "Critical link failure down the OMDb pipeline tracker: $_" } return $null } #endregion #region - Block C02 - Invoke-TMDbSearch() # ============================================================================== # REMARK: Invoke-TMDbSearch # DESCRIPTION: Executes a fuzzy-string search query against the TMDb v3 API endpoint. # FIXED - Hardcoded the absolute verified API domain string layout # to completely eliminate the broken 'themoviedb.orgtv' URI morph bug. # ============================================================================== function Invoke-TMDbSearch { param ( [string]$QueryText, [string]$Type, [string]$ApiKey = "2474e559675331d0f535aa763541c3b7" ) [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 # TMDb API routes television shows via the 'tv' parameter instead of 'series' $TMDbType = if ($Type -eq "series") { "tv" } else { "movie" } # Clean up spaces to ensure a flawless raw string query pattern $CleanQuery = ($QueryText -replace '\s+', ' ').Trim() # URL Encode the query safely using native .NET framework web structures Add-Type -AssemblyName System.Web $EncodedQuery = [System.Web.HttpUtility]::UrlEncode($CleanQuery) # FIXED: Hardcoded the absolute, verified browser path string explicitly. # No more loose concatenation bugs; this maps exactly to your working browser URL! $Uri = "https://api.themoviedb.org/3/search/" + $TMDbType + "?api_key=" + $ApiKey + "&query=" + $EncodedQuery + "&language=en-US" try { # Execute the lightweight, native PowerShell web-call cmdlet $Result = Invoke-RestMethod -Uri $Uri -Method Get -TimeoutSec 10 if ($Result -and $Result.results -and $Result.results.Count -gt 0) { $StandardizedResults = [System.Collections.Generic.List[psobject]]::new() foreach ($Item in $Result.results) { $Title = if ($Item.name) { $Item.name } else { $Item.title } $RawDate = if ($Item.first_air_date) { $Item.first_air_date } else { $Item.release_date } $Year = if ($RawDate -match '^(\d{4})') { $Matches } else { "N/A" } $StandardizedResults.Add([PSCustomObject]@{ Title = $Title Year = $Year imdbID = "tmdb_" + $Item.id Type = $Type }) | Out-Null } return @($StandardizedResults) } } catch { if ($Global:ErrorDiagnosticsLabel) { $Global:ErrorDiagnosticsLabel.Text = "TMDb API Catch: $($_.Exception.Message)" $Global:ErrorDiagnosticsLabel.Foreground = [System.Windows.Media.Brushes]::Crimson } return $null } return $null } #endregion #region - Block D01 - Get-OMDbMediaDetails() # ============================================================================== # ROUTINE 2: Get-OMDbMediaDetails # PURPOSE: Fetches deep granular metadata for a specific title using its IMDb ID. # RETURNS: Comprehensive PSCustomObject containing plot summary, runtime, and ratings. # PARAMETERS: # - $TargetID: The distinct IMDb ID tracking pattern string (e.g., 'tt11505706'). # - $ApiKey: Global authentication token for OMDb API access. # LOGIC: Constructs an explicit query payload map binding the absolute entity key, # forces a lightweight short-plot context configuration statement, and # returns the fully structured property schema layout from the web stream. # ============================================================================== function Get-OMDbMediaDetails { param ( [string]$TargetID, # The distinct IMDb ID tracking pattern string (e.g., 'tt11505706') [string]$ApiKey # Global authentication token for OMDb API access ) $Params = @{ i = $TargetID plot = "short" apikey = $ApiKey } try { # Pull complete structural mapping data contexts for single entities return Invoke-RestMethod -Uri "https://omdbapi.com/" -Method Get -Body $Params -TimeoutSec 10 } catch { return $null } } #endregion #region - Block D02 - Get-TMDbMediaDetails() # ============================================================================== # REMARK: Get-TMDbMediaDetails # DESCRIPTION: Fetches deep granular metadata, plot summaries, and posters from TMDb. # RESTORED: Using your exact operational chunked API URL logic. # ============================================================================== function Get-TMDbMediaDetails { param ( [string]$TargetID, [string]$Type, [string]$ApiKey = "2474e559675331d0f535aa763541c3b7" ) [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 $CleanID = $TargetID -replace "tmdb_", "" $TMDbType = if ($Type -eq "series") { "tv" } else { "movie" } # YOUR EXACT UNTOUCHED URL LOGIC $BaseApiUrl = "https://" + "api" + "." + "themoviedb" + ".org" + "/3/" $Uri = $BaseApiUrl + $TMDbType + "/" + $CleanID + "?api_key=" + $ApiKey + "&append_to_response=external_ids&language=en-US" try { $Result = Invoke-RestMethod -Uri $Uri -Method Get -TimeoutSec 4 if ($Result) { $Title = if ($Result.name) { $Result.name } else { $Result.title } $RawDate = if ($Result.first_air_date) { $Result.first_air_date } else { $Result.release_date } # Safe string extraction logic for the release year $Year = "" if ($RawDate -and $RawDate.Length -ge 4) { $Year = $RawDate.Substring(0, 4) } # RESTORED: Using the working base image location from the JSON specifications $PosterPath = "" if ($Result.poster_path) { $PosterPath = "https://tmdb.org" + $Result.poster_path } $ResolvedIMDbID = if ($Result.external_ids -and $Result.external_ids.imdb_id) { $Result.external_ids.imdb_id } else { "" } # Dynamically map runtime patterns out of the last_episode_to_air matrix block $RuntimeText = "" if ($Result.runtime) { $RuntimeText = "$($Result.runtime) min" } elseif ($Result.episode_run_time -and $Result.episode_run_time.Count -gt 0) { $RuntimeText = "$($Result.episode_run_time) min" } elseif ($Result.last_episode_to_air -and $Result.last_episode_to_air.runtime) { $RuntimeText = "$($Result.last_episode_to_air.runtime) min" } $TotalSeasonsText = "1" if ($Result.number_of_seasons) { $TotalSeasonsText = [string]$Result.number_of_seasons } # Map the direct total episodes string from the root JSON payload $TotalEpisodesCount = "0" if ($Result.number_of_episodes) { $TotalEpisodesCount = [string]$Result.number_of_episodes } $PlaceholderList = [System.Collections.Generic.List[string]]::new() return [PSCustomObject]@{ Title = $Title Year = $Year Type = $Type Plot = if ($Result.overview) { $Result.overview } else { "No description available." } Poster = $PosterPath Genre = (($Result.genres | ForEach-Object { $_.name }) -join ", ") Rated = "N/A" Runtime = $RuntimeText imdbRating = if ($Result.vote_average) { [string]$Result.vote_average } else { "" } totalSeasons = $TotalSeasonsText totalEpisodes = $TotalEpisodesCount imdbID = $ResolvedIMDbID IsTMDbOnly = $true TMDbID = $CleanID CachedEpisodesList = $PlaceholderList CachedEpisodesObjects = @() CustomScraperString = "None" } } } catch { return $null } return $null } #endregion #region - Block D03 - Invoke-MediaOrchestrator() # ============================================================================== # REMARK: Invoke-MediaOrchestrator # DESCRIPTION: The primary data repository gatekeeper. # LOGIC: FIXED - Completely removed the '6de0a087' bottleneck constraint from # $HasNoOMDbKey to ensure your active key evaluates to $false (meaning a key IS present). # ============================================================================== function Invoke-MediaOrchestrator { param ( [Parameter(Mandatory = $true)][ValidateSet('Search', 'MainData', 'Episodes', 'EpisodeDetail')][string]$Action, [Parameter(Mandatory = $true)][string]$QueryOrID, [Parameter(Mandatory = $true)][string]$Type ) # FIXED: Cleaned evaluation criteria. Only empty, whitespace, or "N/A" will trigger $true. # Since your key is '6de0a087', this variable will now properly evaluate to $false! $HasNoOMDbKey = ([string]::IsNullOrWhiteSpace($Global:MediaApiKey) -or $Global:MediaApiKey -eq "N/A") $TMDbApiKey = "2474e559675331d0f535aa763541c3b7" if ($null -eq $Script:DetailsCache) { $Script:DetailsCache = @{} } # DELEGATION HOOK: Forward episode-centric requests seamlessly to Block D04 if ($Action -eq "Episodes" -or $Action -eq "EpisodeDetail") { return Invoke-EpisodeOrchestrator -Action $Action -QueryOrID $QueryOrID -Type $Type } switch ($Action) { # -------------------------------------------------------------------------- # ACTION 1: BROAD TEXT SEARCH QUERY (Rule 1 & Rule 2) # -------------------------------------------------------------------------- "Search" { if ($HasNoOMDbKey -eq $false -and ($QueryOrID -match '^\d+$' -or $QueryOrID -match '^tt\d{7,8}$')) { $CleanID = if ($QueryOrID -match '^\d+$') { "tt" + $QueryOrID } else { $QueryOrID } $ParamsID = @{ i = $CleanID; plot = "full"; apikey = $Global:MediaApiKey } $Result = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body $ParamsID -TimeoutSec 10 if ($Result.Response -eq "True") { $Script:DetailsCache[$Result.imdbID] = $Result return @($Result) } } # RULE 1: Execute primary fuzzy string matching over TMDb $Results = Invoke-TMDbSearch -QueryText $QueryOrID -Type $Type if ($null -ne $Results -and $Results.Count -gt 0) { return $Results } # RULE 2: Fallback to OMDb parameters if TMDb returns zero records if ($HasNoOMDbKey -eq $false) { $ParamsTitle = @{ t = $QueryOrID; type = $Type; apikey = $Global:MediaApiKey } $TitleResult = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body $ParamsTitle -TimeoutSec 4 if ($TitleResult.Response -eq "True") { return @($TitleResult) } $RawSearchResponse = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body @{ s = $QueryOrID; type = $Type; apikey = $Global:MediaApiKey } -TimeoutSec 4 if ($RawSearchResponse -and $RawSearchResponse.Response -eq "True" -and $RawSearchResponse.Search) { return @($RawSearchResponse.Search) } } return @() } # -------------------------------------------------------------------------- # ACTION 2: FETCH FULL METADATA & COVER FOR RIGHT VIEWPORT (Rule 3 & Rule 4) # -------------------------------------------------------------------------- "MainData" { $TargetID = $QueryOrID $ResolvedIMDbID = $null if ($TargetID -like "tmdb_*") { $TMDbId = $TargetID -replace "tmdb_" , "" $TMDbType = if ($Type -eq "series") { "tv" } else { "movie" } try { $ExtUri = "https://themoviedb.org" $ExtResults = Invoke-RestMethod -Uri $ExtUri -Method Get -TimeoutSec 4 if ($ExtResults -and $ExtResults.imdb_id) { $ResolvedIMDbID = $ExtResults.imdb_id } } catch {} } else { $ResolvedIMDbID = $TargetID } $HasDeepCache = ($Script:DetailsCache.ContainsKey($TargetID) -and $null -ne $Script:DetailsCache[$TargetID].Plot -and $Script:DetailsCache[$TargetID].Plot -ne "No description available.") if ($HasDeepCache) { return $Script:DetailsCache[$TargetID] } # RULE 3: Fetch foundational main data components from TMDb $ActiveDetails = $null if ($TargetID -like "tmdb_*") { $ActiveDetails = Get-TMDbMediaDetails -TargetID $TargetID -Type $Type } elseif ($TargetID -like "tt*" -and $HasNoOMDbKey -eq $false) { $ActiveDetails = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body @{i=$TargetID;plot="full";apikey=$Global:MediaApiKey} -TimeoutSec 4 } # RULE 4: Crucial extraction protection fallback layer $NeedsIMDbFallback = ($null -eq $ActiveDetails -or [string]::IsNullOrWhiteSpace($ActiveDetails.Plot) -or $ActiveDetails.Plot -eq "No description available." -or [string]::IsNullOrWhiteSpace($ActiveDetails.Poster) -or $ActiveDetails.Poster -eq "N/A") if ($NeedsIMDbFallback -and $ResolvedIMDbID -like "tt*" -and $HasNoOMDbKey -eq $false) { try { $IMDbDetails = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body @{i=$ResolvedIMDbID;plot="full";apikey=$Global:MediaApiKey} -TimeoutSec 4 if ($IMDbDetails -and $IMDbDetails.Response -eq "True") { if ($null -ne $ActiveDetails) { if ([string]::IsNullOrWhiteSpace($ActiveDetails.Plot) -or $ActiveDetails.Plot -eq "No description available.") { $ActiveDetails.Plot = $IMDbDetails.Plot } if ([string]::IsNullOrWhiteSpace($ActiveDetails.Poster) -or $ActiveDetails.Poster -eq "N/A") { $ActiveDetails.Poster = $IMDbDetails.Poster } if ($IMDbDetails.imdbRating -and $IMDbDetails.imdbRating -ne "N/A") { $ActiveDetails.imdbRating = $IMDbDetails.imdbRating } $ActiveDetails.CustomScraperString = "TMDb + IMDb Fallback" } else { $ActiveDetails = $IMDbDetails $ActiveDetails | Add-Member -MemberType NoteProperty -Name "CustomScraperString" -Value "IMDb" -Force } } } catch {} } if ($ActiveDetails) { if (-not ($ActiveDetails.PSObject.Properties.Name -contains "CustomScraperString")) { $ActiveDetails | Add-Member -MemberType NoteProperty -Name "CustomScraperString" -Value "TMDb" -Force } $Script:DetailsCache[$TargetID] = $ActiveDetails if ($ResolvedIMDbID) { $Script:DetailsCache[$ResolvedIMDbID] = $ActiveDetails } return $ActiveDetails } return $null } } } #endregion #region - Block D04 - Invoke-EpisodeOrchestrator() # ============================================================================== # REMARK: Invoke-EpisodeOrchestrator # DESCRIPTION: Dedicated sub-orchestrator managing seasonal indexing arrays. # PARAMETERS: # - $Action (String): Operational mode: 'Episodes' or 'EpisodeDetail'. # - $QueryOrID (String): The active TMDb series lookup token or target show key. # - $Type (String): Media filter mode validation ('movie' or 'series'). # RETURNS: Standardized array collections or augmented episode detail mappings. # LOGIC: Implements Rules 5 to 7 with discrete fail-safe fallback triggers. # ============================================================================== function Invoke-EpisodeOrchestrator { param ( [string]$Action, [string]$QueryOrID, [string]$Type ) $HasNoOMDbKey = ([string]::IsNullOrWhiteSpace($Global:MediaApiKey) -or $Global:MediaApiKey -eq "6de0a087" -or $Global:MediaApiKey -eq "N/A") $TMDbApiKey = "2474e559675331d0f535aa763541c3b7" switch ($Action) { # -------------------------------------------------------------------------- # ACTION 3: COMPILE FULL EPISODES ARRAY VIA RIGHT-CLICK (Rule 5 & Rule 6) # -------------------------------------------------------------------------- "Episodes" { $TargetID = $QueryOrID $Details = $Script:DetailsCache[$TargetID] if ($null -eq $Details) { return $null } if ($Details.CachedEpisodesObjects -and $Details.CachedEpisodesObjects.Count -gt 0) { return $Details } $TrackingList = [System.Collections.Generic.List[string]]::new() $TrackingObjects = [System.Collections.ArrayList]::new() $TotalCount = 0 $SeasonCount = 1 if ($Details.totalSeasons) { [int]::TryParse($Details.totalSeasons, [ref]$SeasonCount) | Out-Null } # RULE 5: Fires uitsluitend on 'series'. Pulls full season map data from TMDb. $TMDbId = if ($Details.TMDbID) { $Details.TMDbID } else { $TargetID -replace "tmdb_", "" } if ($TargetID -like "tmdb_*" -or $Details.TMDbID) { for ($s = 1; $s -le $SeasonCount; $s++) { $SeasonUri = "https://themoviedb.org" $SeasonData = Invoke-RestMethod -Uri $SeasonUri -Method Get -ErrorAction SilentlyContinue if ($SeasonData -and $SeasonData.episodes) { foreach ($Ep in $SeasonData.episodes) { $TotalCount++ $SString = $s.ToString("00") $EString = ([int]$Ep.episode_number).ToString("00") $LabelFormat = "S$SString`E$EString - $($Ep.name)" if (-not $TrackingList.Contains($LabelFormat)) { $TrackingList.Add($LabelFormat) | Out-Null $TrackingObjects.Add([PSCustomObject]@{ imdbID = "tmdb_ep_" + $Ep.id; DisplayIndex = "S$SString`E$EString"; Title = $Ep.name; Released = $Ep.air_date; imdbRating = [string]$Ep.vote_average; SeasonNum = $SString; EpNum = $EString; FullyScrapedData = $null }) | Out-Null } } } } } # RULE 6: If TMDb results yield blank blocks or fail, loop through OMDb endpoints. $IsTMDbDataEmpty = ($TotalCount -eq 0 -or ($TotalCount -eq 1 -and [string]::IsNullOrWhiteSpace($TrackingObjects.Title))) if ($IsTMDbDataEmpty -and -not $HasNoOMDbKey) { $TrackingList.Clear(); $TrackingObjects.Clear(); $TotalCount = 0 $LookupIMDbID = if ($Details.imdbID) { $Details.imdbID } else { $TargetID } Add-Type -AssemblyName System.Web for ($s = 1; $s -le $SeasonCount; $s++) { $SeasonData = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body @{i=$LookupIMDbID;Season=$s;apikey=$Global:MediaApiKey} -ErrorAction SilentlyContinue if ($null -ne $SeasonData -and $SeasonData.Episodes) { foreach ($Ep in $SeasonData.Episodes) { $TotalCount++ $SString = $s.ToString("00") $ParsedEp = 0 $EString = if ([int]::TryParse($Ep.Episode, [ref]$ParsedEp)) { $ParsedEp.ToString("00") } else { "00" } $CleanedEpTitle = [System.Web.HttpUtility]::HtmlDecode($Ep.Title) $LabelFormat = "S$SString`E$EString - $CleanedEpTitle" if (-not $TrackingList.Contains($LabelFormat)) { $TrackingList.Add($LabelFormat) | Out-Null $TrackingObjects.Add([PSCustomObject]@{ imdbID = $Ep.imdbID; DisplayIndex = "S$SString`E$EString"; Title = $CleanedEpTitle; Released = $Ep.Released; imdbRating = $Ep.imdbRating; SeasonNum = $SString; EpNum = $EString; FullyScrapedData = $null }) | Out-Null } } } } $Details.CustomScraperString = "TMDb + IMDb Episodes Fallback" } $Details | Add-Member -MemberType NoteProperty -Name "CachedTotalEpisodesCount" -Value $TotalCount -Force $Details | Add-Member -MemberType NoteProperty -Name "CachedEpisodesList" -Value $TrackingList -Force $Details | Add-Member -MemberType NoteProperty -Name "CachedEpisodesObjects" -Value $TrackingObjects -Force return $Details } # -------------------------------------------------------------------------- # ACTION 4: FETCH SINGLE GRANULAR EPISODE DETAILS (Rule 7) # -------------------------------------------------------------------------- "EpisodeDetail" { $TargetEpObj = $Global:ActiveSelectedEpisodeObjectReference if ($null -eq $TargetEpObj -or $TargetEpObj.FullyScrapedData) { return $TargetEpObj } $Success = $false $ParentSeriesDetails = $Script:DetailsCache[$QueryOrID] $ParentTMDbID = if ($ParentSeriesDetails.TMDbID) { $ParentSeriesDetails.TMDbID } else { $QueryOrID -replace "tmdb_", "" } # RULE 7 STEP A: Attempts to scrape the specific episode plot layout through TMDb. if ($TargetEpObj.imdbID -like "tmdb_ep_*" -or $QueryOrID -like "tmdb_*") { try { $EpNumInt = [int]$TargetEpObj.EpNum $SeasonNumInt = [int]$TargetEpObj.SeasonNum $EpUri = "https://themoviedb.org" $FullEpDataTMDb = Invoke-RestMethod -Uri $EpUri -Method Get -TimeoutSec 4 if ($FullEpDataTMDb -and -not [string]::IsNullOrWhiteSpace($FullEpDataTMDb.overview)) { $TargetEpObj.FullyScrapedData = [PSCustomObject]@{ Title = $FullEpDataTMDb.name Year = $FullEpDataTMDb.air_date Plot = $FullEpDataTMDb.overview CustomEpString = "Ep. $($TargetEpObj.EpNum) of Season $($TargetEpObj.SeasonNum) (TMDb)" imdbRating = [string]$FullEpDataTMDb.vote_average Rated = "N/A" Runtime = if ($FullEpDataTMDb.runtime) { "$($FullEpDataTMDb.runtime) min" } else { "N/A" } Genre = $ParentSeriesDetails.Genre imdbID = if ($FullEpDataTMDb.production_code) { $FullEpDataTMDb.production_code } else { $TargetEpObj.imdbID } } $Success = $true } } catch {} } # RULE 7 STEP B: Fall back to the OMDb API if TMDb returns blank properties. if (-not $Success -and -not $HasNoOMDbKey) { try { $LookupIMDbID = if ($ParentSeriesDetails.imdbID) { $ParentSeriesDetails.imdbID } else { $QueryOrID } $Params = @{ i = $LookupIMDbID; Season = $TargetEpObj.SeasonNum; Episode = $TargetEpObj.EpNum; plot = "short"; apikey = $Global:MediaApiKey } $FullEpDataOMDb = Invoke-RestMethod -Uri "https://omdbapi.com" -Method Get -Body $Params -TimeoutSec 4 if ($FullEpDataOMDb -and $FullEpDataOMDb.Response -eq "True") { if ($FullEpDataOMDb.Released -and $FullEpDataOMDb.Released -ne "N/A") { $FullEpDataOMDb.Year = $FullEpDataOMDb.Released } $FullEpDataOMDb | Add-Member -MemberType NoteProperty -Name "CustomEpString" -Value "Ep. $($TargetEpObj.EpNum) of Season $($TargetEpObj.SeasonNum) (IMDb)" -Force $TargetEpObj.FullyScrapedData = $FullEpDataOMDb } } catch {} } return $TargetEpObj } } } #endregion #region - Block E01 - Update-ResultListBox() # ============================================================================== # REMARK: Update-ResultListBox # DESCRIPTION: Clears and repopulates the primary media list (Column 0 ListBox). # This is positioned high in the module scope execution mapping to # prevent 'command not recognized' parser initialization failures. # PARAMETERS: # - $TargetListBox (ListBox): Reference to the UI left column ListBox object. # - $MediaList (Array): Dynamic collection holding retrieved search object rows. # - $MediaTypeFilter (String): Current mode configuration ('movie' or 'series'). # LOGIC: Flushes old list metrics, evaluates open-ended interval bounds safely using # pure ASCII hex structures to dodge CMD prompt encodering drop-outs, and maps # the final formatted Title strings directly into the UI list view framework. # ============================================================================== function Update-ResultListBox { param ( [System.Windows.Controls.ListBox]$TargetListBox, [psobject[]]$MediaList, [string]$MediaTypeFilter ) # Empty existing rows to prepare for a fresh presentation render cycle $TargetListBox.Items.Clear() if ($null -eq $MediaList) { return } $CurrentYear = (Get-Date).Year foreach ($Item in $MediaList) { $CleanedListYear = $Item.Year # Check if the year string contains open-ended running pointers (e.g., '2020-') if ($CleanedListYear -is [string]) { # Unicode dashes replaced with raw safe ASCII check to prevent CMD parser crashes if ($CleanedListYear.EndsWith("-") -or $CleanedListYear.EndsWith("–") -or $CleanedListYear.EndsWith("—")) { # Append current system year dynamically to provide accurate runtime contexts $CleanedListYear = $CleanedListYear + $CurrentYear } # Secure regex pattern layout using safe hex characters (\x2d = gewone dash) elseif ($MediaTypeFilter -eq "series" -and $CleanedListYear -notmatch '[\x2d]') { # Enforce clean interval bounds styling for flat single-year string properties $CleanedListYear = "$CleanedListYear-$CurrentYear" } } # Assemble unified title and year display string element $DisplayText = "$($Item.Title) ($CleanedListYear)" $TargetListBox.Items.Add($DisplayText) | Out-Null } } #endregion #region - Block F01 - Update-MediaTableUI() # ============================================================================== # REMARK: Update-MediaTableUI # DESCRIPTION: Dynamically renders the properties grid in the right preview panel. # FIXED - Restored Type and Seasons visibility for operational rows, # even if the initialization state flag was initially set to None. # ============================================================================== function Update-MediaTableUI { param ( [System.Windows.Controls.Grid]$TableGrid, [System.Windows.Controls.TextBox]$PlotTextBox, $Details, [string]$TargetID ) if ($null -eq $Details) { return } $TableGrid.Children.Clear() $TableGrid.RowDefinitions.Clear() $CurrentYear = (Get-Date).Year $Props = $Details.PSObject.Properties.Name # 1. SAFE PROPERTY EXTRACTION $DisplayYear = "" if ($Props -contains "Year" -and $Details.Year -and $Details.Year -ne "N/A") { $DisplayYear = [string]$Details.Year } if ($DisplayYear -is [string] -and $DisplayYear -match '[\x2d]$') { $DisplayYear = $DisplayYear + $CurrentYear } $CleanTitle = "" if ($Props -contains "Title" -and $Details.Title -and $Details.Title -ne "N/A") { $CleanTitle = $Details.Title } $CleanRated = "N/A" if ($Props -contains "Rated" -and $Details.Rated -and $Details.Rated -ne "N/A" -and $Details.Rated -ne "") { $CleanRated = $Details.Rated } $CleanRuntime = "" if ($Props -contains "Runtime" -and $Details.Runtime -and $Details.Runtime -ne "N/A") { $CleanRuntime = $Details.Runtime } $CleanGenre = "" if ($Props -contains "Genre" -and $Details.Genre -and $Details.Genre -ne "N/A") { $CleanGenre = $Details.Genre } $CleanRating = "" if ($Props -contains "imdbRating" -and $Details.imdbRating -and $Details.imdbRating -ne "N/A") { $CleanRating = $Details.imdbRating } $Script:FinalSelection = [PSCustomObject]@{ Title = $CleanTitle; Year = $DisplayYear; imdbID = $TargetID } $TableRows = [System.Collections.Generic.List[Hashtable]]::new() #================================================================================= # Process Scraper header string tracking logic safely $MainSource = "IMDb" if ($Props -contains "IsTMDbOnly" -and $Details.IsTMDbOnly) { $MainSource = "TMDb" } $EpSource = $MainSource if ($Props -contains "CustomScraperString" -and $Details.CustomScraperString -and $Details.CustomScraperString -ne "None") { $EpSource = $Details.CustomScraperString } else { $EpSource = "?" } # Check if we are in the initial blank startup view framework $IsBlankStartup = ($Props -contains "CustomScraperString" -and $Details.CustomScraperString -eq "None" -and $CleanTitle -eq "") $ScraperFormatString = if ($IsBlankStartup) { "" } else { "Main ($MainSource) Episodes ($EpSource)" } $TableRows.Add(@{ Label = "Scraper"; Value = $ScraperFormatString; IsRating = $false; IsSpacer = $false }) $TableRows.Add(@{ Label = ""; Value = ""; IsRating = $false; IsSpacer = $true }) $TableRows.Add(@{ Label = "Title"; Value = $CleanTitle; IsRating = $false; IsSpacer = $false }) # FIXED: Restored core type string rendering rules for active data records $RawType = if ($Global:MediaTypeParam) { $Global:MediaTypeParam } else { if ($Props -contains "Type") { $Details.Type } else { "" } } $MediaTypeDisplay = if ($RawType -eq "movie") { "Movie" } elseif ($RawType -eq "series") { "Serie" } else { $RawType } if ($IsBlankStartup) { $MediaTypeDisplay = "" } $TableRows.Add(@{ Label = "Type"; Value = $MediaTypeDisplay; IsRating = $false; IsSpacer = $false }) if ($Global:MediaTypeParam -eq "series") { $SeasonsCount = "1" if ($Props -contains "totalSeasons" -and $Details.totalSeasons) { $SeasonsCount = $Details.totalSeasons } $HasEpisodesCount = $false if ($Props -contains "CachedTotalEpisodesCount") { if ($null -ne $Details.CachedTotalEpisodesCount -and $Details.CachedTotalEpisodesCount -gt 0) { $HasEpisodesCount = $true } } # FIXED: Enforced visual season metrics mapping layout boundaries flawlessly $SeasonInfoText = "" if (-not $IsBlankStartup) { if ($Props -contains "CustomEpString" -and $Details.CustomEpString) { $SeasonInfoText = $Details.CustomEpString } elseif ($HasEpisodesCount) { $SeasonInfoText = "$SeasonsCount seasons, $($Details.CachedTotalEpisodesCount) episodes" } else { $SeasonInfoText = "$SeasonsCount seasons (Single right-click to load)" } } $TableRows.Add(@{ Label = "Seasons / Ep"; Value = $SeasonInfoText; IsRating = $false; IsSpacer = $false }) } $TableRows.Add(@{ Label = "Year"; Value = $DisplayYear; IsRating = $false; IsSpacer = $false }) $TableRows.Add(@{ Label = "Rated"; Value = $CleanRated; IsRating = $false; IsSpacer = $false }) $TableRows.Add(@{ Label = "Runtime"; Value = $CleanRuntime; IsRating = $false; IsSpacer = $false }) $TableRows.Add(@{ Label = "Genre"; Value = $CleanGenre; IsRating = $false; IsSpacer = $false }) $TableRows.Add(@{ Label = "IMDb Rating"; Value = $CleanRating; IsRating = $true; IsSpacer = $false }) # 2. PRESENTATION RENDER LOOP $VisualRowIndex = 0 for ($r = 0; $r -lt $TableRows.Count; $r++) { $RowDef = [System.Windows.Controls.RowDefinition]::new() if ($TableRows[$r].IsSpacer) { $RowDef.Height = [System.Windows.GridLength]::new([double]8, [System.Windows.GridUnitType]::Pixel) $TableGrid.RowDefinitions.Add($RowDef) $VisualRowIndex++ continue } $RowDef.Height = [System.Windows.GridLength]::Auto $TableGrid.RowDefinitions.Add($RowDef) $HeaderLabel = [System.Windows.Controls.TextBlock]::new() $HeaderLabel.Text = $TableRows[$r].Label $HeaderLabel.FontWeight = [System.Windows.FontWeights]::Bold $HeaderLabel.Margin = "0,1,15,1" if ($TableRows[$r].Label -eq "Scraper") { $HeaderLabel.Foreground = [System.Windows.Media.Brushes]::DarkGray } [System.Windows.Controls.Grid]::SetRow($HeaderLabel, $VisualRowIndex) [System.Windows.Controls.Grid]::SetColumn($HeaderLabel, 0) $TableGrid.Children.Add($HeaderLabel) | Out-Null if ($TableRows[$r].IsRating) { $RatingStack = [System.Windows.Controls.StackPanel]::new() $RatingStack.Orientation = [System.Windows.Controls.Orientation]::Horizontal $RatingStack.Margin = "0,1,0,1" New-MediaRatingStars -RatingString $TableRows[$r].Value -TargetStack $RatingStack $ScoreText = [System.Windows.Controls.TextBlock]::new() $ScoreTextFormat = "" if ($TableRows[$r].Value -like "*/10*") { $ScoreTextFormat = $TableRows[$r].Value } elseif ($TableRows[$r].Value -ne "") { $ScoreTextFormat = "$($TableRows[$r].Value)/10" } $ScoreText.Text = $ScoreTextFormat $ScoreText.Margin = "5,0,0,0" $ScoreText.Foreground = [System.Windows.Media.Brushes]::Gray $RatingStack.Children.Add($ScoreText) | Out-Null [System.Windows.Controls.Grid]::SetRow($RatingStack, $VisualRowIndex) [System.Windows.Controls.Grid]::SetColumn($RatingStack, 1) $TableGrid.Children.Add($RatingStack) | Out-Null } else { $ValueLabel = [System.Windows.Controls.TextBlock]::new() $ValueLabel.Text = $TableRows[$r].Value $ValueLabel.TextWrapping = [System.Windows.TextWrapping]::Wrap $ValueLabel.Margin = "0,1,0,1" if ($TableRows[$r].Label -eq "Scraper") { $ValueLabel.Foreground = [System.Windows.Media.Brushes]::Gray } if ($TableRows[$r].Value -eq "") { $ValueLabel.Foreground = [System.Windows.Media.Brushes]::Gray } [System.Windows.Controls.Grid]::SetRow($ValueLabel, $VisualRowIndex) [System.Windows.Controls.Grid]::SetColumn($ValueLabel, 1) $TableGrid.Children.Add($ValueLabel) | Out-Null } $VisualRowIndex++ } } #endregion #region - Block G01 - Search-IMDbMedia() # ============================================================================== # ROUTINE 5: Search-IMDbMedia # PURPOSE: Main UI execution routine creating the WPF desktop application frame. # LOGIC: STRICTLY DELEGATED - Requests initial search arrays exclusively from # the central Orchestrator (D03). Contains zero direct API queries. # ============================================================================== function Search-IMDbMedia { param ( [Parameter(Mandatory = $true)][string]$Title, [Parameter(Mandatory = $true)][ValidateSet('movie', 'series')][string]$Type, [Parameter(Mandatory = $false)][string]$ApiKey = "6de0a087" ) # Establish global parameter definitions across the active instance session $Global:MediaApiKey = $ApiKey $Global:MediaTypeParam = $Type # Sanitize query strings by wiping away internal bracket arrays context $Title = ($Title -replace '\(.+?\)', '').Trim() # Strictly fetch the initial data matrix through the centralized master coordinator (D03) $Global:MediaList = Invoke-MediaOrchestrator -Action "Search" -QueryOrID $Title -Type $Type #endregion #region - Block G02 # ============================================================================== # PREPARATION: WPF Presentation Window Core Initialization # DESCRIPTION: Instantiates the primary user interface container framework, # setting explicit aspect ratios, positioning, and styling values. # ============================================================================== # Core window container parameter definitions $Global:SearchWindow = [System.Windows.Window]::new() $Global:SearchWindow.Title = "IMDb Media Search Engine" $Global:SearchWindow.Height = 650 $Global:SearchWindow.Width = 1280 $Global:SearchWindow.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen $Global:SearchWindow.Topmost = $false $Global:SearchWindow.Background = [System.Windows.Media.Brushes]::White # ============================================================================== # LAYOUT GRID: Master Framework Partitioning # DESCRIPTION: Configures the multi-dimensional UI layout structure utilizing # proportional row definitions and absolute column definitions. # ============================================================================== # Master Layout Framework Grid initialization $MainGrid = [System.Windows.Controls.Grid]::new() $MainGrid.Margin = [System.Windows.Thickness]::new(15) # Grid Row definitions setup (Row 0: Search Bar, Row 1: Content Arrays) $MainRow0 = [System.Windows.Controls.RowDefinition]::new(); $MainRow0.Height = [System.Windows.GridLength]::Auto $MainRow1 = [System.Windows.Controls.RowDefinition]::new(); $MainRow1.Height = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) $MainGrid.RowDefinitions.Add($MainRow0) | Out-Null $MainGrid.RowDefinitions.Add($MainRow1) | Out-Null # Grid Column definitions setup (Col 0: Media, Col 1: Episodes, Col 2: Previews Panel) $MainCol0 = [System.Windows.Controls.ColumnDefinition]::new(); $MainCol0.Width = [System.Windows.GridLength]::new(380) $MainCol1 = [System.Windows.Controls.ColumnDefinition]::new(); $MainCol1.Width = [System.Windows.GridLength]::new(350) $MainCol2 = [System.Windows.Controls.ColumnDefinition]::new(); $MainCol2.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) $MainGrid.ColumnDefinitions.Add($MainCol0) | Out-Null $MainGrid.ColumnDefinitions.Add($MainCol1) | Out-Null $MainGrid.ColumnDefinitions.Add($MainCol2) | Out-Null # ============================================================================== # SEARCH ROW: Input Controls & Alignment Framework # DESCRIPTION: Constructs the horizontal search panel banner mapping child # controls securely across multiple column partitions. # ============================================================================== # Search panel alignment mapping (Row 0 spans horizontally across columns 0 and 1) $SearchPanel = [System.Windows.Controls.Grid]::new() $SearchPanel.Margin = [System.Windows.Thickness]::new(0, 0, 10, 15) $SearchPanel.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::Auto}) $SearchPanel.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::new(580)}) # Exact pixel tracking matching $SearchPanel.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::new(80)}) [System.Windows.Controls.Grid]::SetRow($SearchPanel, 0) [System.Windows.Controls.Grid]::SetColumn($SearchPanel, 0) [System.Windows.Controls.Grid]::SetColumnSpan($SearchPanel, 2) # Search prompt construction $SearchLabel = [System.Windows.Controls.TextBlock]@{Text="Search : ";FontWeight=[System.Windows.FontWeights]::Bold;FontSize=14;VerticalAlignment=[System.Windows.VerticalAlignment]::Center} [System.Windows.Controls.Grid]::SetColumn($SearchLabel, 0); $SearchPanel.Children.Add($SearchLabel) | Out-Null # Input Box control mapping setup $Global:SearchTextBox = [System.Windows.Controls.TextBox]@{Text=$Title;FontSize=13;Height=26;VerticalContentAlignment=[System.Windows.VerticalAlignment]::Center;Margin=[System.Windows.Thickness]::new(5,0,10,0)} [System.Windows.Controls.Grid]::SetColumn($Global:SearchTextBox, 1); $SearchPanel.Children.Add($Global:SearchTextBox) | Out-Null # Search button structural setup $Global:SearchBtn = [System.Windows.Controls.Button]@{Content="Search";FontSize=13;Height=26;FontWeight=[System.Windows.FontWeights]::SemiBold} [System.Windows.Controls.Grid]::SetColumn($Global:SearchBtn, 2); $SearchPanel.Children.Add($Global:SearchBtn) | Out-Null $MainGrid.Children.Add($SearchPanel) | Out-Null #endregion #region - Block G03 # ============================================================================== # LAYOUT ENGINE: Left and Middle Columns (Column 0 & Column 1) # DESCRIPTION: Constructs the presentation sub-grids for structural listing components, # applying shaded header plates and scrollable list containers. # ============================================================================== # COLUMN 0: Media Results Grid (Movies / Series) with full-width shaded header $LeftListGrid = [System.Windows.Controls.Grid]::new() $LeftListGrid.Margin = [System.Windows.Thickness]::new(0, 0, 10, 0) $LeftListGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{Height=[System.Windows.GridLength]::Auto}) $LeftListGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{Height=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star)}) [System.Windows.Controls.Grid]::SetColumn($LeftListGrid, 0) # FIXED: Replaced invalid class mapping type with official Grid routing methods safely to anchor at Row 1 [System.Windows.Controls.Grid]::SetRow($LeftListGrid, 1) # Render the full-width header label with light gray shading (#F0F0F0) $HeaderText = if ($Type -eq "movie") { "Movies" } else { "Series" } $LeftHeader = [System.Windows.Controls.Label]@{ Content = $HeaderText FontWeight = [System.Windows.FontWeights]::Bold FontSize = 13 Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#F0F0F0") Padding = [System.Windows.Thickness]::new(5,3,5,3) BorderBrush = [System.Windows.Media.Brushes]::LightGray BorderThickness = [System.Windows.Thickness]::new(1,1,1,0) } [System.Windows.Controls.Grid]::SetRow($LeftHeader, 0) $LeftListGrid.Children.Add($LeftHeader) | Out-Null $Global:ResultListBox = [System.Windows.Controls.ListBox]@{FontSize=13} [System.Windows.Controls.Grid]::SetRow($Global:ResultListBox, 1) $LeftListGrid.Children.Add($Global:ResultListBox) | Out-Null $MainGrid.Children.Add($LeftListGrid) | Out-Null # ============================================================================== # COLUMN 1: Episode Structures Grid with full-width shaded header # ============================================================================== $MiddleListGrid = [System.Windows.Controls.Grid]::new() $MiddleListGrid.Margin = [System.Windows.Thickness]::new(5, 0, 15, 0) $MiddleListGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{Height=[System.Windows.GridLength]::Auto}) $MiddleListGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{Height=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star)}) [System.Windows.Controls.Grid]::SetColumn($MiddleListGrid, 1) [System.Windows.Controls.Grid]::SetRow($MiddleListGrid, 1) # Render the secondary full-width header label with light gray shading $MiddleHeaderText = if ($Type -eq "movie") { "Episodes (Disabled in Movie Mode)" } else { "Episodes" } $MiddleHeader = [System.Windows.Controls.Label]@{Content=$MiddleHeaderText;FontWeight=[System.Windows.FontWeights]::Bold;FontSize=13;Background=[System.Windows.Media.BrushConverter]::new().ConvertFromString("#F0F0F0");Padding=[System.Windows.Thickness]::new(5,3,5,3);BorderBrush=[System.Windows.Media.Brushes]::LightGray;BorderThickness=[System.Windows.Thickness]::new(1,1,1,0)} [System.Windows.Controls.Grid]::SetRow($MiddleHeader, 0); $MiddleListGrid.Children.Add($MiddleHeader) | Out-Null # If Movie Mode is active, explicitly disable (Gray out) the complete Episode ListBox $Global:EpisodeListBox = [System.Windows.Controls.ListBox]@{FontSize=12} if ($Type -eq "movie") { $Global:EpisodeListBox.IsEnabled = $false $Global:EpisodeListBox.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#F5F5F5") } [System.Windows.Controls.Grid]::SetRow($Global:EpisodeListBox, 1); $MiddleListGrid.Children.Add($Global:EpisodeListBox) | Out-Null $MainGrid.Children.Add($MiddleListGrid) | Out-Null #endregion #region - Block G04 # ============================================================================== # COLUMN 2: Independent Preview Panel Framework (Set to row 0, spanning 2 rows) # DESCRIPTION: Configures permanent structural Borders (Frames) for the Cover # and Plot containers. Row heights optimized to fully eliminate # bottom clipping and restore property table grid visibility. # ============================================================================== $RightGrid = [System.Windows.Controls.Grid]::new() $RightGrid.Margin = [System.Windows.Thickness]::new(15, 0, 0, 0) # FIXED: Increased height from 315 to 325 to secure full layout bounds for covers and margins $RightGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{Height=[System.Windows.GridLength]::new(325)}) $RightGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{Height=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star)}) $RightGrid.RowDefinitions.Add([System.Windows.Controls.RowDefinition]@{Height=[System.Windows.GridLength]::Auto}) [System.Windows.Controls.Grid]::SetColumn($RightGrid, 2) [System.Windows.Controls.Grid]::SetRow($RightGrid, 0) [System.Windows.Controls.Grid]::SetRowSpan($RightGrid, 2) $TopPreviewGrid = [System.Windows.Controls.Grid]::new() $TopPreviewGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::new(215)}) $TopPreviewGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star)}) [System.Windows.Controls.Grid]::SetRow($TopPreviewGrid, 0); $RightGrid.Children.Add($TopPreviewGrid) | Out-Null # FIXED: Height bounded cleanly within the new 325 pixel grid row parameters $PosterBorderFrame = [System.Windows.Controls.Border]::new() $PosterBorderFrame.Width = 202 $PosterBorderFrame.Height = 302 $PosterBorderFrame.BorderBrush = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A3C7E8") $PosterBorderFrame.BorderThickness = [System.Windows.Thickness]::new(1) $PosterBorderFrame.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#FAFAFA") $PosterBorderFrame.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left $PosterBorderFrame.VerticalAlignment = [System.Windows.VerticalAlignment]::Top $PosterBorderFrame.Margin = [System.Windows.Thickness]::new(0,0,10,15) [System.Windows.Media.RenderOptions]::SetEdgeMode($PosterBorderFrame, [System.Windows.Media.EdgeMode]::Aliased) $Global:PosterImage = [System.Windows.Controls.Image]@{Width=200;Height=300;Stretch=[System.Windows.Media.Stretch]::Uniform} $PosterBorderFrame.Child = $Global:PosterImage [System.Windows.Controls.Grid]::SetColumn($PosterBorderFrame, 0); $TopPreviewGrid.Children.Add($PosterBorderFrame) | Out-Null # FIXED: Standardized layout padding matching the adjacent cover asset wrapper $PlotBorderFrame = [System.Windows.Controls.Border]::new() $PlotBorderFrame.Height = 302 $PlotBorderFrame.BorderBrush = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A3C7E8") $PlotBorderFrame.BorderThickness = [System.Windows.Thickness]::new(1) $PlotBorderFrame.Background = [System.Windows.Media.Brushes]::Transparent $PlotBorderFrame.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch $PlotBorderFrame.VerticalAlignment = [System.Windows.VerticalAlignment]::Top $PlotBorderFrame.Margin = [System.Windows.Thickness]::new(5,0,0,15) $PlotBorderFrame.Padding = [System.Windows.Thickness]::new(5) [System.Windows.Media.RenderOptions]::SetEdgeMode($PlotBorderFrame, [System.Windows.Media.EdgeMode]::Aliased) $Global:PlotTextBox = [System.Windows.Controls.TextBox]@{FontSize=13;TextWrapping=[System.Windows.TextWrapping]::Wrap;IsReadOnly=$true;VerticalScrollBarVisibility=[System.Windows.Controls.ScrollBarVisibility]::Auto;Background=[System.Windows.Media.Brushes]::Transparent;BorderThickness=[System.Windows.Thickness]::new(0)} $PlotBorderFrame.Child = $Global:PlotTextBox [System.Windows.Controls.Grid]::SetColumn($PlotBorderFrame, 1); $TopPreviewGrid.Children.Add($PlotBorderFrame) | Out-Null $Global:InfoTableGrid = [System.Windows.Controls.Grid]@{Margin=[System.Windows.Thickness]::new(0,10,0,0)} $Global:InfoTableGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::new(115)}) $Global:InfoTableGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star)}) [System.Windows.Controls.Grid]::SetRow($Global:InfoTableGrid, 1); $RightGrid.Children.Add($Global:InfoTableGrid) | Out-Null $Global:StatusLabel = [System.Windows.Controls.TextBlock]@{Text="Please wait...reading all episodes";Foreground=[System.Windows.Media.Brushes]::Red;FontWeight=[System.Windows.FontWeights]::Bold;FontSize=13;Margin=[System.Windows.Thickness]::new(0,10,0,0);HorizontalAlignment=[System.Windows.HorizontalAlignment]::Center;Visibility=[System.Windows.Visibility]::Collapsed} [System.Windows.Controls.Grid]::SetRow($Global:StatusLabel, 2); $RightGrid.Children.Add($Global:StatusLabel) | Out-Null $Global:ErrorDiagnosticsLabel = [System.Windows.Controls.TextBlock]::new() $Global:ErrorDiagnosticsLabel.Text = "Diagnostics Active..." $Global:ErrorDiagnosticsLabel.Foreground = [System.Windows.Media.Brushes]::DarkGray $Global:ErrorDiagnosticsLabel.FontWeight = [System.Windows.FontWeights]::SemiBold $Global:ErrorDiagnosticsLabel.FontSize = 11 $Global:ErrorDiagnosticsLabel.TextWrapping = [System.Windows.TextWrapping]::Wrap $Global:ErrorDiagnosticsLabel.VerticalAlignment = [System.Windows.VerticalAlignment]::Center $Global:ErrorDiagnosticsLabel.Margin = [System.Windows.Thickness]::new(10, 0, 15, 0) $Global:ErrorDiagnosticsLabel.Visibility = [System.Windows.Visibility]::Visible #endregion #region - Block G05 # PREVIEW COLUMN FOOTER: Cancel and Save Buttons Framework (Fixed Grid Alignment) $FooterPanelGrid = [System.Windows.Controls.Grid]::new() $FooterPanelGrid.VerticalAlignment = [System.Windows.VerticalAlignment]::Bottom $FooterPanelGrid.Margin = [System.Windows.Thickness]::new(0,0,0,0) $FooterPanelGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star)}) $FooterPanelGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::Auto}) $FooterPanelGrid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]@{Width=[System.Windows.GridLength]::Auto}) [System.Windows.Controls.Grid]::SetRow($FooterPanelGrid, 1) [System.Windows.Controls.Grid]::SetColumn($FooterPanelGrid, 2) if ($Global:StatusLabel) { $Global:StatusLabel.VerticalAlignment = [System.Windows.VerticalAlignment]::Top $Global:StatusLabel.Margin = [System.Windows.Thickness]::new(0, 0, 0, 45) } $Global:CancelBtn = [System.Windows.Controls.Button]@{Content="Cancel";Width=80;Height=26;Margin=[System.Windows.Thickness]::new(0,0,10,0)} $Global:SaveBtn = [System.Windows.Controls.Button]@{Content="Save";Width=80;Height=26;FontWeight=[System.Windows.FontWeights]::SemiBold} [System.Windows.Controls.Grid]::SetColumn($Global:ErrorDiagnosticsLabel, 0); $FooterPanelGrid.Children.Add($Global:ErrorDiagnosticsLabel) | Out-Null [System.Windows.Controls.Grid]::SetColumn($Global:CancelBtn, 1); $FooterPanelGrid.Children.Add($Global:CancelBtn) | Out-Null [System.Windows.Controls.Grid]::SetColumn($Global:SaveBtn, 2); $FooterPanelGrid.Children.Add($Global:SaveBtn) | Out-Null $MainGrid.Children.Add($FooterPanelGrid) | Out-Null # ============================================================================== # ENGINE: Universal PSCustomObject Output Builder Framework (JAGGED ARRAY) # LOGIC: DELEGATED - Requests multi-season episode inflation directly through # the specialized Episode Orchestrator (D04) before final packaging. # ============================================================================== $Script:BuildAndReturnMediaObject = { if ($Global:ResultListBox.SelectedIndex -ge 0) { $TargetItem = $Global:MediaList[$Global:ResultListBox.SelectedIndex] $TargetID = $TargetItem.imdbID # Request full multi-season compilation through the central controller (D04) if ($Global:MediaTypeParam -eq "series") { $Global:StatusLabel.Text = "Compiling Complete Episode Matrix...Please Wait!" $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Visible [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([System.Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) # Triggers data inflation in cache securely down Block D04 rules $Details = Invoke-MediaOrchestrator -Action "Episodes" -QueryOrID $TargetID -Type $Global:MediaTypeParam $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Collapsed } else { $Details = Invoke-MediaOrchestrator -Action "MainData" -QueryOrID $TargetID -Type $Global:MediaTypeParam } $SourceArray = $Details.CachedEpisodesObjects if ($null -eq $SourceArray) { $SourceArray = @() } $OutputHash = [ordered]@{}; foreach ($Property in $Details.PSObject.Properties) { if ($Property.Name -notmatch '^Cached|^Custom') { $OutputHash[$Property.Name] = $Property.Value } } if ($OutputHash.Contains("Type")) { if ($OutputHash["Type"] -eq "movie") { $OutputHash["Type"] = "Movie" } elseif ($OutputHash["Type"] -eq "series") { $OutputHash["Type"] = "Serie" } } if ($Global:MediaTypeParam -eq "series" -and $SourceArray.Count -gt 0) { $JaggedArray = [object[]]::new($SourceArray.Count) for ($row = 0; $row -lt $SourceArray.Count; $row++) { $EpisodeObj = $SourceArray[$row] $ComboIndexText = "S" + $EpisodeObj.SeasonNum + "E" + $EpisodeObj.EpNum + " - " + $EpisodeObj.Title $JaggedArray[$row] = @($EpisodeObj.imdbID, $EpisodeObj.imdbRating, $EpisodeObj.SeasonNum, $EpisodeObj.EpNum, $ComboIndexText) } $OutputHash["EpisodesList"] = $JaggedArray } else { $OutputHash["EpisodesList"] = @() } $Global:IMDbSearchResult = [PSCustomObject]$OutputHash $Global:SearchWindow.Close() } } $Global:CancelBtn.Add_Click({ $Global:SearchWindow.Close() }) $Global:SaveBtn.Add_Click({ & $Script:BuildAndReturnMediaObject }) $Global:ResultListBox.Add_MouseDoubleClick({ if ($_.ChangedButton -eq 'Left') { & $Script:BuildAndReturnMediaObject } }) $Global:EpisodeListBox.Add_MouseDoubleClick({ if ($_.ChangedButton -eq 'Left') { & $Script:BuildAndReturnMediaObject } }) $MainGrid.Children.Add($RightGrid) | Out-Null $Global:SearchWindow.Content = $MainGrid #endregion #region - Block G06 # Initialize instance data memory structures $Script:FinalSelection = $null # ============================================================================== # CLICK TRIGGER: Search Button Click Engine # DESCRIPTION: Cleaned and delegated entirely to the central data repository core. # ============================================================================== $Global:SearchBtn.Add_Click({ $RawQuery = $Global:SearchTextBox.Text.Trim() if (-not [string]::IsNullOrEmpty($RawQuery)) { # Flush UI views instantly to prevent layout corruption $Global:ResultListBox.Items.Clear() $Global:EpisodeListBox.Items.Clear() $Global:InfoTableGrid.Children.Clear() $Global:InfoTableGrid.RowDefinitions.Clear() $Global:PosterImage.Source = $null $Global:PlotTextBox.Text = "" # Directly request the standardized search list from the Orchestrator (D03) $Global:MediaList = Invoke-MediaOrchestrator -Action "Search" -QueryOrID $RawQuery -Type $Global:MediaTypeParam if ($null -ne $Global:MediaList -and $Global:MediaList.Count -gt 0) { Update-ResultListBox -TargetListBox $Global:ResultListBox -MediaList $Global:MediaList -MediaTypeFilter $Global:MediaTypeParam $Global:ResultListBox.SelectedIndex = 0 } else { $Global:ResultListBox.Items.Clear() $Global:PlotTextBox.Text = "No results found matching: '$RawQuery'" } } }) $Global:SearchTextBox.Add_KeyDown({ if ($_.Key -eq [System.Windows.Input.Key]::Enter) { $Global:SearchBtn.RaiseEvent([System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent)) } }) #endregion #region - Block G07 # ============================================================================== # INTERACTION TRIGGER: Main Result ListBox (LB 1) Selection Changed # DESCRIPTION: Requests comprehensive detail sheets from the master orchestrator. # ============================================================================== $Global:ResultListBox.Add_SelectionChanged({ if ($Global:ResultListBox.SelectedIndex -ge 0) { $Script:IsLoadingSeries = $true if ($Global:ErrorDiagnosticsLabel) { $Global:ErrorDiagnosticsLabel.Text = "Loading Selection..." $Global:ErrorDiagnosticsLabel.Foreground = [System.Windows.Media.Brushes]::DarkGray } $TargetItem = $Global:MediaList[$Global:ResultListBox.SelectedIndex] $TargetID = $TargetItem.imdbID try { $Global:StatusLabel.Text = "Getting Media Layout Details...Please Wait!" $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Visible [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([System.Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) # Pull fully synthesized data records through the central orchestrator (D03) $Details = Invoke-MediaOrchestrator -Action "MainData" -QueryOrID $TargetID -Type $Global:MediaTypeParam $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Collapsed if ($Details) { $Global:PlotTextBox.Text = if ($Details.Plot) { $Details.Plot } else { "No series plot summary available." } # FIXED: Case-insensitive extraction of the Poster string to bulletproof WPF rendering $ActivePosterUrl = "" foreach ($P in $Details.PSObject.Properties) { if ($P.Name -eq "Poster" -or $P.Name -eq "poster") { $ActivePosterUrl = [string]$P.Value } } if ($ActivePosterUrl -and $ActivePosterUrl -ne "N/A" -and $ActivePosterUrl -ne "") { try { $Bitmap = [System.Windows.Media.Imaging.BitmapImage]::new() $Bitmap.BeginInit() $Bitmap.UriSource = [System.Uri]::new($ActivePosterUrl) $Bitmap.EndInit() $Bitmap.Freeze() $Global:PosterImage.Source = $Bitmap } catch { $Global:PosterImage.Source = $null } } else { $Global:PosterImage.Source = $null } $Global:EpisodeListBox.Items.Clear() if ($Details.CachedEpisodesList) { foreach ($Label in $Details.CachedEpisodesList) { $Global:EpisodeListBox.Items.Add($Label) | Out-Null } } # Render properties matrix onto UI grid container node Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $Details -TargetID $Details.imdbID if ($Global:ErrorDiagnosticsLabel) { $Global:ErrorDiagnosticsLabel.Text = "Layout Loaded Successfully."; $Global:ErrorDiagnosticsLabel.Foreground = [System.Windows.Media.Brushes]::Green } } } catch { if ($Global:ErrorDiagnosticsLabel) { $Global:ErrorDiagnosticsLabel.Text = "UI Selection Error: $($_.Exception.Message)"; $Global:ErrorDiagnosticsLabel.Foreground = [System.Windows.Media.Brushes]::Crimson } } $Script:IsLoadingSeries = $false } }) #endregion #region - Block G08 # Traps sequential re-clicks on already highlighted left rows to force right panel restore $Global:ResultListBox.Add_PreviewMouseLeftButtonDown({ $VisualTarget = $_.OriginalSource while ($null -ne $VisualTarget -and $VisualTarget.GetType().Name -ne "ListBoxItem") { $VisualTarget = [System.Windows.Media.VisualTreeHelper]::GetParent($VisualTarget) } if ($null -ne $VisualTarget) { $ClickIndex = $Global:ResultListBox.ItemContainerGenerator.IndexFromContainer($VisualTarget) if ($ClickIndex -eq $Global:ResultListBox.SelectedIndex -and $ClickIndex -ge 0) { $TargetID = $Global:MediaList[$ClickIndex].imdbID # DELEGATED: Re-fetch layout data directly via Orchestrator cache routing (D03) $Details = Invoke-MediaOrchestrator -Action "MainData" -QueryOrID $TargetID -Type $Global:MediaTypeParam if ($Details) { $Global:PlotTextBox.Text = if ($Details.Plot) { $Details.Plot } else { "No series plot summary available." } Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $Details -TargetID $Details.imdbID } } } }) # ============================================================================== # SELECTION CHANGED: Episode ListBox (LB 2) # ============================================================================== $Global:EpisodeListBox.Add_SelectionChanged({ if (-not $Script:IsLoadingSeries -and $Global:EpisodeListBox.SelectedIndex -ge 0 -and $Global:ResultListBox.SelectedIndex -ge 0) { $SeriesID = $Global:MediaList[$Global:ResultListBox.SelectedIndex].imdbID # DELEGATED: Request the main parent data packet through the repository cache (D03) $ParentDetails = Invoke-MediaOrchestrator -Action "MainData" -QueryOrID $SeriesID -Type $Global:MediaTypeParam $TargetEpObj = $ParentDetails.CachedEpisodesObjects[$Global:EpisodeListBox.SelectedIndex] if ($TargetEpObj) { $DisplayDetails = if ($TargetEpObj.FullyScrapedData) { $TargetEpObj.FullyScrapedData } else { [PSCustomObject]@{ Title = $TargetEpObj.Title Year = $TargetEpObj.Released CustomEpString = "Ep. $($TargetEpObj.EpNum) of Season $($TargetEpObj.SeasonNum)" imdbRating = $TargetEpObj.imdbRating Rated = "N/A" Runtime = "N/A" Genre = $ParentDetails.Genre } } $Global:PlotTextBox.Text = if ($TargetEpObj.FullyScrapedData) { "Episode $($TargetEpObj.DisplayIndex) Plot:`n`n" + $TargetEpObj.FullyScrapedData.Plot } else { "Episode $($TargetEpObj.DisplayIndex) Plot:`n`nPlot summary not loaded yet. Right-click this row to fetch details." } Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $DisplayDetails -TargetID $TargetEpObj.imdbID } } }) #endregion #region - Block G09 # ============================================================================== # INTERACTION TRIGGER: Episode ListBox (LB 2) - Right-Click Data Scraper # DESCRIPTION: Cleaned UI event caller tracking granular episode metadata updates. # LOGIC: DELEGATED - Stores object reference inside global workspace pointers, # triggers Block D04, and populates rendered results immediately. # ============================================================================== $Global:EpisodeListBox.Add_PreviewMouseRightButtonDown({ $VisualTarget = $_.OriginalSource while ($null -ne $VisualTarget -and $VisualTarget.GetType().Name -ne "ListBoxItem") { $VisualTarget = [System.Windows.Media.VisualTreeHelper]::GetParent($VisualTarget) } if ($null -ne $VisualTarget) { $EpIndex = $Global:EpisodeListBox.ItemContainerGenerator.IndexFromContainer($VisualTarget) $SeriesIndex = $Global:ResultListBox.SelectedIndex if ($EpIndex -ge 0 -and $SeriesIndex -ge 0) { $Global:EpisodeListBox.SelectedIndex = $EpIndex $SeriesID = $Global:MediaList[$SeriesIndex].imdbID $ParentDetails = Invoke-MediaOrchestrator -Action "MainData" -QueryOrID $SeriesID -Type $Global:MediaTypeParam $TargetEpObj = $ParentDetails.CachedEpisodesObjects[$EpIndex] if ($TargetEpObj -and -not $TargetEpObj.FullyScrapedData) { $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Visible [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([System.Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) # INJECT SCOPE LINK: Pass object reference downstream to the Episode Orchestrator (D04) $Global:ActiveSelectedEpisodeObjectReference = $TargetEpObj # DELEGATED: Trigger deep metadata retrieval via Part B Orchestrator (D04) $ResolvedEpObject = Invoke-MediaOrchestrator -Action "EpisodeDetail" -QueryOrID $SeriesID -Type $Global:MediaTypeParam # Render resulting payload attributes onto view panels instantly if ($ResolvedEpObject -and $ResolvedEpObject.FullyScrapedData) { $Global:PlotTextBox.Text = "Episode $($ResolvedEpObject.DisplayIndex) Plot:`n`n" + $ResolvedEpObject.FullyScrapedData.Plot Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $ResolvedEpObject.FullyScrapedData -TargetID $ResolvedEpObject.imdbID } else { $Global:PlotTextBox.Text = "Episode $($TargetEpObj.DisplayIndex) Plot:`n`nPlot summary could not be loaded." } $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Collapsed } } } }) #endregion #region - Block G10 # ============================================================================== # INTERACTION TRIGGER: Main Result ListBox (LB 1) - Right-Click Season Scraper # DESCRIPTION: Implements Rule 5 & Rule 6. Pulls full season blocks through D04. # ============================================================================== $Global:ResultListBox.Add_PreviewMouseRightButtonDown({ $VisualTarget = $_.OriginalSource while ($null -ne $VisualTarget -and $VisualTarget.GetType().Name -ne "ListBoxItem") { $VisualTarget = [System.Windows.Media.VisualTreeHelper]::GetParent($VisualTarget) } if ($null -ne $VisualTarget){ $Index = $Global:ResultListBox.ItemContainerGenerator.IndexFromContainer($VisualTarget) if ($Index -ge 0) { $TargetItem = $Global:MediaList[$Index] $TargetID = $TargetItem.imdbID $Global:ResultListBox.SelectedIndex = $Index if ($Global:MediaTypeParam -eq "series" -and $Global:EpisodeListBox.Items.Count -eq 0) { $Global:StatusLabel.Text = "Getting Complete Episode List...Please Wait!" $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Visible [System.Windows.Threading.Dispatcher]::CurrentDispatcher.Invoke([System.Action]{}, [System.Windows.Threading.DispatcherPriority]::Background) $Global:EpisodeListBox.Items.Clear() # DELEGATED: Ask the specialized Episode Orchestrator (D04) to inflate all seasons in cache $UpdatedDetails = Invoke-MediaOrchestrator -Action "Episodes" -QueryOrID $TargetID -Type $Global:MediaTypeParam # Render newly inflated records onto the centralized listbox layout views if ($UpdatedDetails -and $UpdatedDetails.CachedEpisodesList) { foreach ($Label in $UpdatedDetails.CachedEpisodesList) { $Global:EpisodeListBox.Items.Add($Label) | Out-Null } Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $UpdatedDetails -TargetID $UpdatedDetails.imdbID } if ($Global:EpisodeListBox.Items.Count -eq 0) { $Global:EpisodeListBox.Items.Add("No episodes found in databases.") | Out-Null } $Global:StatusLabel.Visibility = [System.Windows.Visibility]::Collapsed } } } }) #endregion #region - Block G11 # ============================================================================== # FOCUS TRIGGER: Main Result ListBox (LB 1) - Restores Show Plot Upon Back-Tab # ============================================================================== $Global:ResultListBox.Add_GotFocus({ if ($Global:ResultListBox.SelectedIndex -ge 0) { $TargetID = $Global:MediaList[$Global:ResultListBox.SelectedIndex].imdbID $Details = Invoke-MediaOrchestrator -Action "MainData" -QueryOrID $TargetID -Type $Global:MediaTypeParam if ($Details) { $Global:PlotTextBox.Text = if ($Details.Plot) { $Details.Plot } else { "No series plot summary available." } Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $Details -TargetID $TargetID } } }) # ============================================================================== # FOCUS TRIGGER: Episode ListBox (LB 2) - ALWAYS and Exclusively Controls Plot Status # ============================================================================== $Global:EpisodeListBox.Add_GotFocus({ if (-not $Script:IsLoadingSeries -and $Global:ResultListBox.SelectedIndex -ge 0) { if ($Global:EpisodeListBox.SelectedIndex -lt 0 -and $Global:EpisodeListBox.Items.Count -gt 0) { $Global:EpisodeListBox.SelectedIndex = 0 } if ($Global:EpisodeListBox.SelectedIndex -ge 0) { $SeriesID = $Global:MediaList[$Global:ResultListBox.SelectedIndex].imdbID $ParentDetails = Invoke-MediaOrchestrator -Action "MainData" -QueryOrID $SeriesID -Type $Global:MediaTypeParam $TargetEpObj = $ParentDetails.CachedEpisodesObjects[$Global:EpisodeListBox.SelectedIndex] if ($TargetEpObj) { $DisplayDetails = if ($TargetEpObj.FullyScrapedData) { $TargetEpObj.FullyScrapedData } else { [PSCustomObject]@{ Title = $TargetEpObj.Title; Year = $TargetEpObj.Released; imdbRating = $TargetEpObj.imdbRating CustomEpString = "Ep. $($TargetEpObj.EpNum) of Season $($TargetEpObj.SeasonNum) (Single right-click to load details)" Rated = "N/A"; Runtime = "N/A"; Genre = $ParentDetails.Genre } } $Global:PlotTextBox.Text = if ($TargetEpObj.FullyScrapedData) { "Episode Plot ($($TargetEpObj.DisplayIndex)):`n`n" + $TargetEpObj.FullyScrapedData.Plot } else { "Episode $($TargetEpObj.DisplayIndex) Plot:`n`nPlot summary not loaded yet. Right-click this row to fetch details." } Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $DisplayDetails -TargetID $TargetEpObj.imdbID } } } }) # ============================================================================== # WINDOW LOADED: Forces sharp OS workspace focus and triggers completely clean prompts # ============================================================================== $Global:SearchWindow.Add_Loaded({ Update-ResultListBox -TargetListBox $Global:ResultListBox -MediaList $Global:MediaList -MediaTypeFilter $Global:MediaTypeParam # INITIALIZATION FIX: Generates a completely blank configuration payload state $BlankStartupDetails = [PSCustomObject]@{ Title = "" Year = "" Type = "" Plot = "" Poster = "" Genre = "" Rated = "" Runtime = "" imdbRating = "" totalSeasons = "" imdbID = "" IsTMDbOnly = $true CustomScraperString = "None" # Tells Block F01 to skip scraper header labels CachedEpisodesList = @() CachedEpisodesObjects = @() } Update-MediaTableUI -TableGrid $Global:InfoTableGrid -PlotTextBox $Global:PlotTextBox -Details $BlankStartupDetails -TargetID "" if ($Global:ResultListBox.Items.Count -gt 0) { $Global:ResultListBox.SelectedIndex = 0 } $Global:SearchWindow.Focus() | Out-Null $Global:ResultListBox.Focus() | Out-Null }) # Pre-emptively clear global search results to ensure accurate return tracking structures $Global:IMDbSearchResult = $null # Render the window view modal layout frame $Global:SearchWindow.ShowDialog() | Out-Null # Context scope cleanups to keep the memory footprint lightweight Remove-Variable -Name SearchWindow, ResultListBox, EpisodeListBox, PosterImage, InfoTableGrid, PlotTextBox, StatusLabel, SearchTextBox, SearchBtn, MediaList, MediaApiKey, MediaTypeParam -Scope Global -ErrorAction SilentlyContinue return $Global:IMDbSearchResult } #endregion #region - Block H01 - Rename() #======================================================================================================= # [int] RenameFiles - Renames Files according a list with new Names from a Text file or from clipboard # # Input: TxtFilePath - The path and filename of the text file or "#" to use the clipboard content # TargetFolder - Path & Filename of Media Files to rename # Recur - 1 = Recursive, 0 = Not Recursive # #======================================================================================================= function [int]Rename{ param ( [string]$TargetFolder, [string[]]$Filenames, [int]$Recur = 0 ) if ([string]::IsNullOrWhiteSpace($Path)){ Write-Host "No Path provided!" return -1 } if ([string]::IsNullOrWhiteSpace($Filenames) -or !$Filenames.Count){ Write-Host "No Filenames provided!" return -2 } if (-not (Test-Path -Path $Path)){ Write-Host "Path not found!" $Path return -3 } # # Read each line from the list and filter by SnnEnn format # $Filenames | Where-Object { $_ -match '^\s*(S\d{2}E\d{2})\s*-\s*(.+)$' } | ForEach-Object { $Code = $Matches[1] # e.g., "S01E05" $NewName = $_.Trim() # The full (new) desired name without extension # Search recursively from Disk for files starting with the SnnEnn code [bool]$Recur = if ($Recur -eq 1) { $true } else { $false } $TargetFiles = Get-ChildItem -Path $TargetFolder -Filter "$Code*" -File -Recurse:$Recur if ($TargetFiles){ foreach ($File in $TargetFiles){ # For each file found matching the code $Extension = $File.Extension # Extension $FullNewName = "$NewName$Extension" # New Filename if ($File.Name -ne $FullNewName){ # Prevent renaming if the file already has the correct name Write-Host "Renaming: '$($File.FullName)' -> '$FullNewName'" -ForegroundColor Green # Rename-Item -Path $File.FullName -NewName $FullNewName } else { Write-Host "Skipping: '$($File.FullName)'" -ForegroundColor Cyan } } } } return 0 } #endregion