Get-Download.psm1

#Requires -Version 5.0

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$CachePath = Join-Path -Path $env:APPDATA -ChildPath "Get-Download\PRIVATE"
$YtDlpExe = Join-Path -Path $CachePath -ChildPath "yt-dlp.exe"
$YtDlpDownloadUrl = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe"
$ModulePath = $PSScriptRoot
$GitHubRawUrl = "https://raw.githubusercontent.com/Goodwintek/Get-Download/main"
$script:InstanceId = [guid]::NewGuid().ToString("N")

function Write-Log {
    param([string]$Text)
    if ($script:logBox) {
        $script:logBox.AppendText($Text + [Environment]::NewLine)
    } else {
        Write-Host $Text
    }
}

function Initialize-YtDlp {
    if (Test-Path -Path $YtDlpExe -PathType Leaf) {
        return $true
    }

    if (-not (Test-Path -Path $CachePath -PathType Container)) {
        New-Item -ItemType Directory -Path $CachePath -Force | Out-Null
    }

    try {
        Write-Log "Downloading yt-dlp.exe..."
        Invoke-WebRequest -Uri $YtDlpDownloadUrl -OutFile $YtDlpExe -ErrorAction Stop
        Write-Log "yt-dlp.exe downloaded successfully."
        return $true
    } catch {
        [System.Windows.Forms.MessageBox]::Show("Failed to download yt-dlp.exe: $($_.Exception.Message)", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
        return $false
    }
}

function Update-GetDownload {
    [CmdletBinding()]
    param()

    $result = [System.Windows.Forms.MessageBox]::Show("This will update Get-Download module and yt-dlp. Continue?", "Update", [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question)

    if ($result -ne [System.Windows.Forms.DialogResult]::Yes) {
        return
    }

    try {
        Write-Log "Updating Get-Download module..."

        $psd1Url = "$GitHubRawUrl/Get-Download.psd1"
        $psm1Url = "$GitHubRawUrl/Get-Download.psm1"
        $psd1Path = Join-Path -Path $ModulePath -ChildPath "Get-Download.psd1"
        $psm1Path = Join-Path -Path $ModulePath -ChildPath "Get-Download.psm1"

        Invoke-WebRequest -Uri $psd1Url -OutFile $psd1Path -ErrorAction Stop
        Invoke-WebRequest -Uri $psm1Url -OutFile $psm1Path -ErrorAction Stop

        Write-Log "Module updated successfully."

        if (Test-Path -Path $YtDlpExe -PathType Leaf) {
            Write-Log "Updating yt-dlp.exe..."
            & $YtDlpExe -U | Out-Null
            Write-Log "yt-dlp.exe updated successfully."
        }

        [System.Windows.Forms.MessageBox]::Show("Update complete. Application will restart.", "Success", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)

        $global:RestartApp = $true
    } catch {
        [System.Windows.Forms.MessageBox]::Show("Update failed: $($_.Exception.Message)", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
        Write-Log "Update error: $_"
    }
}

function Get-YtDlpArgs {
    param(
        [string]$OutputDir,
        [string]$Url,
        [string]$FileName,
        [bool]$AudioOnly,
        [bool]$SslFix,
        [bool]$FfmpegHls
    )

    if ($FileName) {
        $template = "$OutputDir\$FileName.%(ext)s"
    } else {
        $template = "$OutputDir\%(title)s.%(ext)s"
    }

    $argString = "--output `"$template`" --embed-metadata"

    if ($FileName) {
        $argString += ' --replace-in-metadata title "^.*$" "' + $FileName + '"'
    }
    if ($Url -notmatch '\.m3u8') {
        $argString += " --download-archive `"$OutputDir\.yt-dlp-archive.txt`""
    }
    if ($FfmpegHls) {
        $argString += " --merge-output-format mp4 --downloader m3u8:ffmpeg --downloader-args `"ffmpeg:-protocol_whitelist file,http,https,tcp,tls,crypto`""
    }
    if ($AudioOnly) { $argString += " -x" }
    if ($SslFix) { $argString += " --legacy-server-connect --no-check-certificate" }
    $argString += " `"$Url`""

    return $argString
}

function Read-NewText {
    param(
        [string]$Path,
        [ref]$Position
    )

    if (-not (Test-Path -Path $Path -PathType Leaf)) {
        return ""
    }

    $stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
    try {
        $stream.Position = $Position.Value
        $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8)
        $text = $reader.ReadToEnd()
        $Position.Value = $stream.Position
        return $text
    } finally {
        $stream.Dispose()
    }
}

function Start-YtDlpAttempt {
    $attempt = $script:attempts[$script:attemptIndex]
    $argString = Get-YtDlpArgs -OutputDir $script:runOutputDir -Url $script:runUrl -FileName $script:runFileName -AudioOnly $script:runAudio -SslFix $attempt.Ssl -FfmpegHls $attempt.Ffmpeg

    Write-Log "Attempt [$($attempt.Label)]: $YtDlpExe $argString"
    $script:statusLabel.Text = "Running: $($attempt.Label)"

    $env:PYTHONIOENCODING = "utf-8"
    $script:outFile = Join-Path -Path $CachePath -ChildPath "$($script:InstanceId)-stdout.log"
    $script:errFile = Join-Path -Path $CachePath -ChildPath "$($script:InstanceId)-stderr.log"
    $script:outPos = 0
    $script:errPos = 0

    $script:proc = Start-Process -FilePath $YtDlpExe -ArgumentList $argString -NoNewWindow -PassThru -RedirectStandardOutput $script:outFile -RedirectStandardError $script:errFile
    $script:timer.Start()
}

function Flush-YtDlpOutput {
    $text = Read-NewText -Path $script:outFile -Position ([ref]$script:outPos)
    $text += Read-NewText -Path $script:errFile -Position ([ref]$script:errPos)
    if ($text) {
        $script:logBox.AppendText($text)
    }
}

function Finish-Run {
    param([string]$Status)
    $script:downloadButton.Enabled = $true
    $script:statusLabel.Text = $Status
    Remove-Item -Path $script:outFile, $script:errFile -ErrorAction SilentlyContinue
}

function Get-Download {
    [CmdletBinding()]
    param(
        [string]$DefaultOutputPath = "",
        [switch]$U
    )

    if ($U) {
        Update-GetDownload
        return
    }

    if (-not (Initialize-YtDlp)) {
        return
    }

    if (-not (Test-Path -Path $YtDlpExe -PathType Leaf)) {
        throw "yt-dlp.exe not found at: $YtDlpExe"
    }

    do {
        $global:RestartApp = $false

        $form = New-Object System.Windows.Forms.Form
        $form.Text = "Get-Download"
        $form.Width = 400
        $form.Height = 500
        $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen

        $menuStrip = New-Object System.Windows.Forms.MenuStrip
        $helpMenu = New-Object System.Windows.Forms.ToolStripMenuItem
        $helpMenu.Text = "Help"
        $updateItem = New-Object System.Windows.Forms.ToolStripMenuItem
        $updateItem.Text = "Check for Updates"
        $updateItem.Add_Click({ Update-GetDownload })
        $helpMenu.DropDownItems.Add($updateItem) | Out-Null
        $menuStrip.Items.Add($helpMenu) | Out-Null
        $form.Controls.Add($menuStrip)
        $form.MainMenuStrip = $menuStrip

        $urlLabel = New-Object System.Windows.Forms.Label
        $urlLabel.Location = New-Object System.Drawing.Point(10, 35)
        $urlLabel.AutoSize = $true
        $urlLabel.Text = "Playlist URL:"
        $form.Controls.Add($urlLabel)

        $script:urlTextBox = New-Object System.Windows.Forms.TextBox
        $script:urlTextBox.Location = New-Object System.Drawing.Point(100, 32)
        $script:urlTextBox.Width = 245
        $script:urlTextBox.Add_KeyDown({
            if ($_.Control -and $_.KeyCode -eq [System.Windows.Forms.Keys]::A) {
                $script:urlTextBox.SelectAll()
                $_.SuppressKeyPress = $true
            }
        })
        $script:urlTextBox.Add_DoubleClick({
            $script:urlTextBox.SelectAll()
        })
        $form.Controls.Add($script:urlTextBox)

        $clearUrlButton = New-Object System.Windows.Forms.Button
        $clearUrlButton.Location = New-Object System.Drawing.Point(350, 30)
        $clearUrlButton.Width = 30
        $clearUrlButton.Height = 22
        $clearUrlButton.Text = "X"
        $clearUrlButton.Add_Click({
            $script:urlTextBox.Clear()
            $script:urlTextBox.Focus()
        })
        $form.Controls.Add($clearUrlButton)

        $script:audioOnlyCheckBox = New-Object System.Windows.Forms.CheckBox
        $script:audioOnlyCheckBox.Location = New-Object System.Drawing.Point(10, 70)
        $script:audioOnlyCheckBox.AutoSize = $true
        $script:audioOnlyCheckBox.Text = "Extract Audio Only (-x)"
        $form.Controls.Add($script:audioOnlyCheckBox)

        $outputLabel = New-Object System.Windows.Forms.Label
        $outputLabel.Location = New-Object System.Drawing.Point(10, 100)
        $outputLabel.AutoSize = $true
        $outputLabel.Text = "Output Folder:"
        $form.Controls.Add($outputLabel)

        $script:outputTextBox = New-Object System.Windows.Forms.TextBox
        $script:outputTextBox.Location = New-Object System.Drawing.Point(100, 97)
        $script:outputTextBox.Width = 200
        if ($DefaultOutputPath) {
            $script:outputTextBox.Text = $DefaultOutputPath
        }
        $form.Controls.Add($script:outputTextBox)

        $browseButton = New-Object System.Windows.Forms.Button
        $browseButton.Location = New-Object System.Drawing.Point(305, 95)
        $browseButton.Text = "Browse"
        $browseButton.Add_Click({
            $folderBrowserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
            $folderBrowserDialog.Description = "Select the output folder"
            if (-not [string]::IsNullOrEmpty($script:outputTextBox.Text) -and (Test-Path $script:outputTextBox.Text)) {
                $folderBrowserDialog.SelectedPath = $script:outputTextBox.Text
            }
            if ($folderBrowserDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
                $script:outputTextBox.Text = $folderBrowserDialog.SelectedPath
            }
        })
        $form.Controls.Add($browseButton)

        $fileLabel = New-Object System.Windows.Forms.Label
        $fileLabel.Location = New-Object System.Drawing.Point(10, 130)
        $fileLabel.AutoSize = $true
        $fileLabel.Text = "File Name:"
        $form.Controls.Add($fileLabel)

        $script:fileTextBox = New-Object System.Windows.Forms.TextBox
        $script:fileTextBox.Location = New-Object System.Drawing.Point(100, 127)
        $script:fileTextBox.Width = 245
        $form.Controls.Add($script:fileTextBox)

        $script:downloadButton = New-Object System.Windows.Forms.Button
        $script:downloadButton.Location = New-Object System.Drawing.Point(150, 165)
        $script:downloadButton.Text = "Download"
        $script:downloadButton.Add_Click({
            $script:runUrl = $script:urlTextBox.Text
            $script:runOutputDir = $script:outputTextBox.Text
            $script:runFileName = $script:fileTextBox.Text.Trim() -replace '[\\/:*?"<>|]', ''
            $script:runAudio = $script:audioOnlyCheckBox.Checked

            if ([string]::IsNullOrEmpty($script:runUrl) -or [string]::IsNullOrEmpty($script:runOutputDir)) {
                [System.Windows.Forms.MessageBox]::Show("Please enter a URL and select an output folder.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
                return
            }

            if (-not (Test-Path $script:runOutputDir)) {
                try {
                    New-Item -ItemType Directory -Path $script:runOutputDir -Force | Out-Null
                } catch {
                    [System.Windows.Forms.MessageBox]::Show("Could not create folder: $($script:runOutputDir)`n$($_.Exception.Message)", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error)
                    return
                }
            }

            if ($script:proc -and -not $script:proc.HasExited) {
                Write-Log "A download is already running."
                return
            }

            $script:attempts = @(
                @{ Label = "Standard";       Ssl = $false; Ffmpeg = $false }
                @{ Label = "SSL fix";        Ssl = $true;  Ffmpeg = $false }
                @{ Label = "HLS via ffmpeg"; Ssl = $false; Ffmpeg = $true }
                #@{ Label = "SSL + HLS"; Ssl = $true; Ffmpeg = $true }
            )
            $script:attemptIndex = 0
            $script:logBox.Clear()
            $script:downloadButton.Enabled = $false
            Start-YtDlpAttempt
        })
        $form.Controls.Add($script:downloadButton)

        $script:logBox = New-Object System.Windows.Forms.TextBox
        $script:logBox.Multiline = $true
        $script:logBox.ReadOnly = $true
        $script:logBox.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical
        $script:logBox.Font = New-Object System.Drawing.Font("Consolas", 8)
        $script:logBox.Location = New-Object System.Drawing.Point(10, 200)
        $script:logBox.Size = New-Object System.Drawing.Size(365, 220)
        $form.Controls.Add($script:logBox)

        $script:statusLabel = New-Object System.Windows.Forms.Label
        $script:statusLabel.Location = New-Object System.Drawing.Point(10, 425)
        $script:statusLabel.Size = New-Object System.Drawing.Size(365, 24)
        $script:statusLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10, [System.Drawing.FontStyle]::Bold)
        $script:statusLabel.Text = "Ready"
        $form.Controls.Add($script:statusLabel)

        $script:timer = New-Object System.Windows.Forms.Timer
        $script:timer.Interval = 300
        $script:timer.Add_Tick({
            Flush-YtDlpOutput
            if ($script:proc -and $script:proc.HasExited) {
                $script:timer.Stop()
                Flush-YtDlpOutput
                $attempt = $script:attempts[$script:attemptIndex]
                $exitCode = $script:proc.ExitCode

                if ($exitCode -eq 0) {
                    Write-Log "Download completed successfully."
                    Finish-Run "Completed"
                    [System.Windows.Forms.MessageBox]::Show("Download completed successfully!", "Success", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)
                    return
                }

                Write-Log "[$($attempt.Label)] exited with code $exitCode."
                $script:attemptIndex++

                if ($script:attemptIndex -lt $script:attempts.Count) {
                    Start-YtDlpAttempt
                } else {
                    Write-Log "All attempts failed."
                    Finish-Run "Failed - see log"
                    [System.Windows.Forms.MessageBox]::Show("All attempts failed. Check the log for details.", "yt-dlp error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning)
                }
            }
        })

        $form.Add_FormClosed({ $script:timer.Stop() })

        $form.ShowDialog() | Out-Null
        $form.Dispose()

        if ($global:RestartApp) {
            Remove-Module Get-Download -Force
            Import-Module (Join-Path -Path $ModulePath -ChildPath "Get-Download.psd1") -Force
            Get-Download
            return
        }
    } while ($global:RestartApp)
}

Export-ModuleMember -Function Get-Download, Update-GetDownload