Functions/GenXdev.Queries.Webbrowser/Open-AllYoutubeVideos.ps1

################################################################################

<#
.SYNOPSIS
Opens and controls YouTube videos in a browser window with keyboard shortcuts.
 
.DESCRIPTION
Opens YouTube videos matching search terms or from various YouTube sections in a
browser window. Provides keyboard controls for video playback and navigation.
 
.PARAMETER Queries
YouTube search terms to find videos for. Opens all videos matching each term.
 
.PARAMETER Subscriptions
Opens all videos from subscribed channels.
 
.PARAMETER WatchLater
Opens all videos from the watch-later playlist.
 
.PARAMETER Recommended
Opens all recommended videos from YouTube homepage.
 
.PARAMETER Trending
Opens all currently trending videos on YouTube.
 
.PARAMETER Edge
Use Microsoft Edge browser instead of default.
 
.PARAMETER Chrome
Use Google Chrome browser instead of default.
 
.EXAMPLE
Open-AllYoutubeVideos -Queries "PowerShell tutorial","vscode tips" -Edge
 
.EXAMPLE
qvideos "PowerShell tutorial" -e
#>

function Open-AllYoutubeVideos {

    [CmdletBinding()]
    [Alias("qvideos", "qyt")]
    param(
        ########################################################################
        [parameter(
            Mandatory = $false,
            Position = 0,
            ValueFromRemainingArguments = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "YouTube search terms to find videos"
        )]
        [Alias("q", "Value", "Name", "Text", "Query")]
        [string[]] $Queries = @(""),
        ########################################################################
        [parameter(
            Mandatory = $false,
            HelpMessage = "Open videos from subscribed channels"
        )]
        [switch] $Subscriptions,
        ########################################################################
        [parameter(
            Mandatory = $false,
            HelpMessage = "Open videos from watch later playlist"
        )]
        [switch] $WatchLater,
        ########################################################################
        [parameter(
            Mandatory = $false,
            HelpMessage = "Open recommended videos"
        )]
        [switch] $Recommended,
        ########################################################################
        [parameter(
            Mandatory = $false,
            HelpMessage = "Open trending videos"
        )]
        [switch] $Trending,
        ########################################################################
        [parameter(
            Mandatory = $false,
            HelpMessage = "Use Microsoft Edge browser"
        )]
        [Alias("e")]
        [switch] $Edge,
        ########################################################################
        [parameter(
            Mandatory = $false,
            HelpMessage = "Use Google Chrome browser"
        )]
        [Alias("ch")]
        [switch] $Chrome
        ########################################################################
    )

    begin {
        # tracks which video was last displayed to avoid redrawing unchanged content
        $lastVideo = ""

        # obtain main powershell window handle for proper window positioning
        $powershellProcess = Get-PowershellMainWindowProcess
        $powershellWindow = [GenXdev.Helpers.WindowObj]::GetMainWindow($powershellProcess)

        Write-Verbose "Starting YouTube video browser"
    }

    process {
        # determine if we're working with current tab or need to open new one
        [bool] $currentTab = ($Recommended -ne $true) -and
            ($Subscriptions -ne $true) -and
            ($Trending -ne $true) -and
            ($WatchLater -ne $true) -and
            (($Queries.Count -eq 0) -or [String]::IsNullOrWhiteSpace($queries[0]))

        # internal function that handles video navigation and control interface
        function go($Url = $null, $Query) {
            # initialize state tracking for current video session
            $Global:data = @{
                query          = $Query
                urls           = @()
                description    = ""
                title          = ""
                subscribeTitle = ""
                playing        = $true
                duration       = 0
                position       = 0
            }

            # detect and configure multi-monitor setup
            $AllScreens = @([WpfScreenHelper.Screen]::AllScreens | ForEach-Object { $PSItem })

            # get console dimensions for UI layout
            $hostInfo = & { $H = Get-Host; $H.ui.rawui }
            $size = "$($hostInfo.WindowSize.Width)x$($hostInfo.WindowSize.Height)"

            Clear-Host
            Write-Host "Hold on.. launching query".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)
            $browser = $null;

            if (-not $currentTab) {

                if ($PowershellWindow.Count -gt 0) {

                    $PowershellScreen = [WpfScreenHelper.Screen]::FromPoint(@{X = $PowershellWindow.Position().X; Y = $PowershellWindow.Position().Y });
                    $PowershellScreenIndex = $AllScreens.IndexOf($PowershellScreen) + 1;

                    [int] $defaultMonitor = 1;

                    if ([int]::TryParse($Global:DefaultSecondaryMonitor, [ref] $defaultMonitor)) {

                        $Monitor = $defaultMonitor % ($AllScreens.Length + 1);
                    }
                    else {

                        $Monitor = 2 % ($AllScreens.AllScreens.Length + 1);
                    }

                    if ($monitor -lt 1) {

                        if ($monitor -lt 0) {

                            $monitor = $PowershellWindow;
                        }
                        else {

                            $monitor = $AllScreens.IndexOf([WpfScreenHelper.Screen]::PrimaryScreen) + 1;
                        }
                    }

                    if ($PowershellScreenIndex -eq $Monitor) {

                        if ($PowershellScreen.WorkingArea.Width -gt $PowershellScreen.WorkingArea.Height) {

                            Set-WindowPosition -Left -Monitor $Monitor
                            $browser = Open-Webbrowser -NewWindow -RestoreFocus -Chromium -Edge:$Edge -Chrome:$Chrome -Right -Url $Url -PassThru -Force -NoBrowserExtensions
                            $null = Start-Sleep 1
                            $Global:chrome = $null
                        }
                        else {

                            Set-WindowPosition -Bottom -Monitor $Monitor
                            $browser = Open-Webbrowser -NewWindow -RestoreFocus -Chromium -Edge:$Edge -Chrome:$Chrome -Top -Url $Url -PassThru -Force -NoBrowserExtensions
                            $null = Start-Sleep 1
                            $Global:chrome = $null
                        }
                    }
                }

                if ($null -eq $browser) {

                    $browser = Open-Webbrowser -NewWindow -FullScreen -RestoreFocus -Chromium -Edge:$Edge -Chrome:$Chrome -Url $Url -PassThru -Force -NoBrowserExtensions
                    $null = Start-Sleep 1
                    $Global:chrome = $null

                }

                try {
                    $null = Select-WebbrowserTab -Name "*youtube*" -ErrorAction SilentlyContinue -Edge:$Edge -Chrome:$Chrome
                    $ChromeSessions | ForEach-Object { Select-WebbrowserTab -ByReference $_; Get-WebbrowserTabDomNodes "video" "e.pause()" }
                }
                catch {

                }

                $null = Select-WebbrowserTab -Name "*youtube*" -ErrorAction SilentlyContinue -Edge:$Edge -Chrome:$Chrome
            }
            else {

                $Global:chrome = $null
                $null = Select-WebbrowserTab -Name "*youtube*" -ErrorAction SilentlyContinue -Edge:$Edge -Chrome:$Chrome
            }

            # loads and executes the JavaScript controller code
            $job = [System.IO.File]::ReadAllText("$PSScriptRoot\..\..\Open-AllYoutubeVideos.js")

            # track scroll positions for header animations
            [int] $scrollPosition = -1
            [int] $scrollPosition2 = -1

            # get chrome automation interfaces
            $page = $Global:chromeController
            $reference = Get-ChromiumSessionReference

            # handles asynchronous tab opening and video pausing
            function checkOpened() {

                $opened = $false

                try {
                    $i = 0
                    if ($null -ne $Global:data.open) {
                        $Global:data.open | ForEach-Object {
                            $i++
                            $opened = $true
                            $page = $page.Context.NewPageAsync().GetAwaiter().GetResult()
                            $null = $page.GoToAsync($PSItem).GetAwaiter().GetResult()
                            try {
                                $null = Select-WebbrowserTab -Name "*youtube*" -ErrorAction SilentlyContinue -Edge:$Edge -Chrome:$Chrome -Force
                                $reference = Get-ChromiumSessionReference
                                $page = $Global:chromeController
                                $null = Invoke-WebbrowserEvaluation "$job"
                                $null = Get-WebbrowserTabDomNodes "video" "e.pause()"
                            }
                            catch {
                                Write-Warning "Error in checkOpened: $_"
                            }
                        }
                    }
                }
                catch {

                    Write-Warning "Error in checkOpened: $_"
                }

                if ($opened) {

                    $Global:data.open = @();
                    $null = Set-ForegroundWindow ($powershellWindow.handle)
                }
            }

            # main event loop - handles UI updates and keyboard input
            while ((-not $completed) -and ($null -ne $page) -and ($null -ne $reference)) {

                try {
                    # reset cursor position and colors for status line
                    $hostInfo = & { $H = Get-Host; $H.ui.rawui }
                    [Console]::SetCursorPosition(0, $hostInfo.WindowSize.Height - 2)
                    [Console]::ResetColor()

                    # execute JavaScript controller code
                    $null = Invoke-WebbrowserEvaluation "$job;" -Page $Page -ByReference $reference

                    # process any pending tab operations
                    checkOpened

                    if ($completed) { return }

                    try {

                        # handle console window resizing
                        $newsize = "$($hostInfo.WindowSize.Width)x$($hostInfo.WindowSize.Height)"
                        if ($newsize -ne $size) {
                            $size = $newsize
                            $LastVideo = $null
                        }

                        # construct and display control header
                        $sub = ""
                        $pause = " [P]ause | "
                        $header = "[Q]uit | $sub$pause SPACE=Next | S = $(($data.subscribeTitle)) | F = Toggle fullscreen | [0]..[9] = skip | â—€ -20s | +20s â–¶ | ".PadRight($hostInfo.WindowSize.Width, " ")

                        if ($header.Length -gt $hostInfo.WindowSize.Width) {

                            $scrollPosition = ($scrollPosition + 1) % $header.length
                            try {
                                $header = "$header $header".Substring($scrollPosition, $hostInfo.WindowSize.Width)
                            }
                            catch {
                                try {
                                    $header = "$header $header".Substring(0, $hostInfo.WindowSize.Width)
                                }
                                catch {
                                }
                            }
                        }

                        $scrollPosition = -1
                        $scrollPosition2 = -1
                        $videoInfo = "$($Global:data.title)$($Global:data.description)"

                        if ($videoInfo -ne $LastVideo) {
                            Clear-Host
                            Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)
                            $header = "$($Global:data.title)".Replace("`r", "").Replace("`n", "`r").Replace("`t", " ").Trim().PadRight($hostInfo.WindowSize.Width, " ")
                            if ($header.Length -gt $hostInfo.WindowSize.Width) {

                                $scrollPosition2 = ($scrollPosition2 + 1) % $header.length
                                try {
                                    $header = "$header $header".Substring($scrollPosition, $hostInfo.WindowSize.Width)
                                }
                                catch {
                                    try {
                                        $header = "$header $header".Substring(0, $hostInfo.WindowSize.Width)
                                    }
                                    catch {
                                    }
                                }
                            }

                            Write-Host $header -ForegroundColor ([ConsoleColor]::black) -BackgroundColor ([ConsoleColor]::Gray)
                            [int] $nn = 0

                            if ($Global:data.description.Contains("\n1")) {

                                $Global:data.description = "loading.."
                            }

                            $txt = "$($Global:data.description)".Replace("Show less", "").Replace("Show more", "").Replace("`r", "").Replace("`n", "`r").Replace("`t", " ").Trim()
                            Write-Host ((($txt -Split "`r"  | ForEach-Object -ErrorAction SilentlyContinue {
                                            if ([string]::IsNullOrWhiteSpace($PSItem)) {
                                                $nn = $nn + 1
                                            }
                                            else {
                                                $nn = 0
                                            }
                                            if ($nn -lt 2) {
                                                $s = $PSItem.Trim()
                                                for ([int] $i = $hostInfo.WindowSize.Width - 1; $i -lt $s.length - 1; $i += $hostInfo.WindowSize.Width - 3) {

                                                    $s = $s.substring(0, $i) + "`r" + $s.substring($i)
                                                }

                                                $s
                                            }
                                        }
                                    ) -Join "`r" -Split "`r" | Select-Object -First ($hostInfo.WindowSize.Height - 3)) -Join "`r`n")

                            $LastVideo = $videoInfo
                        }

                        [Console]::SetCursorPosition(0, $hostInfo.WindowSize.Height - 1)
                        [Console]::BackgroundColor = [ConsoleColor]::Blue
                        [Console]::ForegroundColor = [ConsoleColor]::Yellow
                        try { [Console]::Write([System.TimeSpan]::FromSeconds([Math]::Round($Global:data.Position, 0)).ToString()) } catch {}
                        [Console]::SetCursorPosition($hostInfo.WindowSize.Width - 9, $hostInfo.WindowSize.Height - 1)
                        try { [Console]::Write([System.TimeSpan]::FromSeconds([Math]::Round($Global:data.Duration - $Global:data.Position, 0)).ToString()) } catch {}
                        $hostInfo = & { $H = Get-Host; $H.ui.rawui }
                        [Console]::SetCursorPosition(0, $hostInfo.WindowSize.Height - 2)
                        [Console]::ResetColor()

                        # process keyboard input when available
                        while ([Console]::KeyAvailable) {

                            [Console]::SetCursorPosition(0, $hostInfo.WindowSize.Height - 2)
                            $c = [Console]::ReadKey()

                            switch ("$($c.KeyChar)".ToLowerInvariant()) {

                                "q" {
                                    $completed = $true
                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host "Quiting..".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)

                                    if ($null -ne $browser) {

                                        $browser.CloseMainWindow() | Out-Null
                                    }
                                    return
                                }

                                " " {

                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host "Skipping to next video".PadRight($hostInfo.WindowSize.Width - 2, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)
                                    [Console]::SetCursorPosition(0, $hostInfo.WindowSize.Height - 2)
                                    $Page.CloseAsync().Wait()
                                    $null = Select-WebbrowserTab -Name "*youtube*" -ErrorAction SilentlyContinue -Edge:$Edge -Chrome:$Chrome
                                    $page = $Global:chromeController
                                    $reference = Get-ChromiumSessionReference
                                    $LastVideo = ""
                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)
                                    continue
                                }

                                "s" {

                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host "Toggling subscription".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)
                                    $null = Invoke-WebbrowserEvaluation "$job;await toggleSubscribeToChannel();" -Page $Page -ByReference $reference | Out-Null
                                    $null = Start-Sleep 1
                                    checkOpened
                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)

                                    break;
                                }

                                "f" {

                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host "Toggling fullscreen".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)
                                    $null = Invoke-WebbrowserEvaluation "$job;await toggleFullscreenVideo();" -Page $Page -ByReference $reference | Out-Null
                                    $null = Start-Sleep 1
                                    checkOpened
                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)

                                    break;
                                }

                                "p" {

                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host "Toggling pause video".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)
                                    $LastVideo = ""
                                    $null = Invoke-WebbrowserEvaluation "$job;await togglePauseVideo();" -Page $Page -ByReference $reference | Out-Null
                                    $null = Start-Sleep 1
                                    checkOpened
                                    [Console]::SetCursorPosition(0, 0)
                                    Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)

                                    break;
                                }

                                default {

                                    [int] $n = 0
                                    if ([int]::TryParse("$($c.KeyChar)", [ref] $n)) {

                                        [Console]::SetCursorPosition(0, 0)
                                        Write-Host "Skipping to position".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)
                                        $null = Invoke-WebbrowserEvaluation "$job;await setVideoPosition($n);" -Page $Page -ByReference $reference | Out-Null
                                        $null = Start-Sleep 1
                                        checkOpened
                                        [Console]::SetCursorPosition(0, 0)
                                        Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)
                                    }
                                    else {
                                        if ($c.Key -eq [ConsoleKey]::RightArrow) {

                                            [Console]::SetCursorPosition(0, 0)
                                            Write-Host "Skipping 20 seconds forward".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)
                                            $null = Invoke-WebbrowserEvaluation "$job;await forwardInVideo();" -Page $Page -ByReference $reference | Out-Null
                                            $null = Start-Sleep 1
                                            checkOpened
                                            [Console]::SetCursorPosition(0, 0)
                                            Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)
                                        }
                                        else {
                                            if ($c.Key -eq [ConsoleKey]::LeftArrow) {

                                                [Console]::SetCursorPosition(0, 0)
                                                Write-Host "Skipping 20 seconds backwards".PadRight($hostInfo.WindowSize.Width, " ") -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::Yellow)
                                                $null = Invoke-WebbrowserEvaluation "$job;await backwardsInVideo();" -Page $Page -ByReference $reference | Out-Null
                                                $null = Start-Sleep 1
                                                checkOpened
                                                [Console]::SetCursorPosition(0, 0)
                                                Write-Host $header -BackgroundColor ([ConsoleColor]::Blue) -ForegroundColor ([ConsoleColor]::White)
                                            }
                                        }
                                    }

                                    break;
                                }
                            }
                        }
                    }
                    catch {

                        if ($LastVideo -ne "") {
                            $completed = $true
                            return
                        }
                    }

                    # refresh chrome automation interfaces
                    $hostInfo = & { $H = Get-Host; $H.ui.rawui }
                    [Console]::SetCursorPosition(0, $hostInfo.WindowSize.Height - 2)
                    [Console]::ResetColor()
                    $null = Select-WebbrowserTab -Name "*youtube*" -ErrorAction SilentlyContinue -Edge:$Edge -Chrome:$Chrome
                    $page = $Global:chromeController
                    $reference = Get-ChromiumSessionReference

                    # verify chrome session is still active
                    if ((@($Global:chromeSessions).Count -eq 0) -or ($null -eq $Global:chrome)) {
                        throw "No active session"
                    }
                }
                catch {

                    $completed = $true
                }
            }
        }

        # suppress verbose output during video playback
        $OldVerbosePreference = $VerbosePreference
        $VerbosePreference = 'SilentlyContinue'

        try {
            # handle different video source scenarios based on parameters
            if ($currentTab) {
                go
                return
            }

            # process search queries if provided
            if ($Queries.Count -gt 0) {
                foreach ($Query in $Queries) {
                    if ([string]::IsNullOrWhiteSpace($Query) -eq $false) {
                        go "https://www.youtube.com/results?search_query=$([Uri]::EscapeUriString($query))" $Query
                    }
                }
            }

            # handle special feed types
            if ($Subscriptions -eq $true) {
                go "https://www.youtube.com/feed/subscriptions"
            }

            if ($Recommended -eq $true) {
                go "https://www.youtube.com/"
            }

            if ($WatchLater -eq $true) {
                go "https://www.youtube.com/playlist?list=WL"
            }

            if ($Trending -eq $true) {
                go "https://www.youtube.com/feed/trending"
            }
        }
        finally {
            Clear-Host
            $VerbosePreference = $OldVerbosePreference
        }
    }

    end {
        Write-Verbose "YouTube video browser session ended"
    }
}

################################################################################