Public/Install-BATCRelayBot.ps1
|
function Install-BATCRelayBot { <# .SYNOPSIS Installs all prerequisites and generates config.json for the BATC Relay Bot. .DESCRIPTION Installs Python, ffmpeg, and VoiceMeeter (via winget), pip dependencies, detects VoiceMeeter device, and interactively prompts for Discord configuration. Installs bot files to AppData\Local\BATCRelayBot. No admin rights required. .PARAMETER BotPath Installation path for bot files and configuration. Defaults to $env:USERPROFILE\AppData\Local\BATCRelayBot .EXAMPLE Install-BATCRelayBot .EXAMPLE Install-BATCRelayBot -BotPath "D:\MyBot\BATCRelayBot" #> param( [string]$BotPath = "$env:USERPROFILE\AppData\Local\BATCRelayBot" ) $ErrorActionPreference = "Stop" # Create log file for troubleshooting $logFile = Join-Path $BotPath "install.log" $null = New-Item -ItemType Directory -Path $BotPath -Force -ErrorAction SilentlyContinue # Start logging "=== BATCRelayBot Installation Log ===" | Out-File -FilePath $logFile -Force "Started: $(Get-Date)" | Add-Content -Path $logFile "PowerShell Version: $($PSVersionTable.PSVersion)" | Add-Content -Path $logFile "" | Add-Content -Path $logFile try { if (-not (Test-Path $BotPath)) { New-Item -ItemType Directory -Path $BotPath -Force | Out-Null Write-Host "Created bot directory: $BotPath" -ForegroundColor Green } Set-Location $BotPath # Copy bot files if they don't exist Write-Step "Preparing bot files" # $PSScriptRoot = BATCRelayBot/Public, so we need 2 parents to get to repo root $moduleDir = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) $sourceBot = Join-Path $moduleDir "bot.py" $sourceReqs = Join-Path $moduleDir "requirements.txt" $sourceConfig = Join-Path $moduleDir "config.example.json" "Copying bot files from: $moduleDir" | Add-Content -Path $logFile if (Test-Path $sourceBot) { Copy-Item $sourceBot $BotPath -Force -ErrorAction SilentlyContinue Write-Host "bot.py copied to $BotPath" -ForegroundColor Green "✓ bot.py copied" | Add-Content -Path $logFile } else { "✗ bot.py not found at $sourceBot" | Add-Content -Path $logFile } if (Test-Path $sourceReqs) { Copy-Item $sourceReqs $BotPath -Force -ErrorAction SilentlyContinue Write-Host "requirements.txt copied to $BotPath" -ForegroundColor Green "✓ requirements.txt copied" | Add-Content -Path $logFile } else { "✗ requirements.txt not found at $sourceReqs" | Add-Content -Path $logFile } if (Test-Path $sourceConfig) { Copy-Item $sourceConfig $BotPath -Force -ErrorAction SilentlyContinue Write-Host "config.example.json copied to $BotPath" -ForegroundColor Green "✓ config.example.json copied" | Add-Content -Path $logFile } else { "✗ config.example.json not found at $sourceConfig" | Add-Content -Path $logFile } # Check winget Write-Step "Checking prerequisites" if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Host "ERROR: winget (Windows Package Manager) not found." -ForegroundColor Red Write-Host "Install 'App Installer' from the Microsoft Store:" -ForegroundColor Red Write-Host "https://apps.microsoft.com/detail/9nblggh4nns1" -ForegroundColor Red exit 1 } Write-Host "winget found." -ForegroundColor Green # Install Python Write-Step "Python" $pythonExe = Find-PerUserPython if ($pythonExe) { Write-Host "Python already installed: $pythonExe" -ForegroundColor Green } else { Write-Host "Python not found, installing latest via winget..." -ForegroundColor Cyan $searchOutput = winget search "Python.Python.3." --source winget --accept-source-agreements 2>$null $matches = [regex]::Matches(($searchOutput -join "`n"), 'Python\.Python\.3\.(\d+)') $latestId = $null $latestMinor = -1 foreach ($m in $matches) { $minor = [int]$m.Groups[1].Value if ($minor -gt $latestMinor) { $latestMinor = $minor $latestId = $m.Value } } if (-not $latestId) { Write-Host "ERROR: Could not find Python via winget. Install manually from python.org" -ForegroundColor Red exit 1 } Write-Host "Installing $latestId (current user only)..." -ForegroundColor Cyan winget install --id $latestId -e --scope user --silent --accept-package-agreements --accept-source-agreements if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: Python installation failed (exit code $LASTEXITCODE)." -ForegroundColor Red exit 1 } $pythonExe = Find-PerUserPython if (-not $pythonExe) { Write-Host "ERROR: Python was installed but could not be located. Restart the terminal and try again." -ForegroundColor Red exit 1 } Write-Host "Python installed: $pythonExe" -ForegroundColor Green } # Install ffmpeg Write-Step "ffmpeg" $ffmpegExe = Find-FfmpegExe if ($ffmpegExe) { Write-Host "ffmpeg already installed: $ffmpegExe" -ForegroundColor Green } else { Write-Host "ffmpeg not found, installing via winget..." -ForegroundColor Cyan winget install --id Gyan.FFmpeg -e --scope user --silent --accept-package-agreements --accept-source-agreements if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: ffmpeg installation failed (exit code $LASTEXITCODE)." -ForegroundColor Red exit 1 } $ffmpegExe = Find-FfmpegExe if (-not $ffmpegExe) { Write-Host "ERROR: ffmpeg was installed but could not be located." -ForegroundColor Red exit 1 } Write-Host "ffmpeg installed: $ffmpegExe" -ForegroundColor Green } $ffmpegDir = Split-Path -Parent $ffmpegExe $userPath = [Environment]::GetEnvironmentVariable("Path", "User") if ($userPath -notlike "*$ffmpegDir*") { Write-Host "Adding ffmpeg to user PATH: $ffmpegDir" -ForegroundColor Cyan $newUserPath = if ($userPath) { "$userPath;$ffmpegDir" } else { $ffmpegDir } [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") } else { Write-Host "ffmpeg is already on PATH." -ForegroundColor Green } if ($env:Path -notlike "*$ffmpegDir*") { $env:Path = "$env:Path;$ffmpegDir" } # Install VoiceMeeter Write-Step "VoiceMeeter" $voicemeeterInstalled = Get-ChildItem "C:\Program Files (x86)\VB\Voicemeeter\" -ErrorAction SilentlyContinue if ($voicemeeterInstalled) { Write-Host "VoiceMeeter already installed" -ForegroundColor Green } else { Write-Host "VoiceMeeter not found, attempting installation via winget..." -ForegroundColor Cyan # Try main version first, then Potato if it fails $voicemeeterInstalled = $false foreach ($pkgId in @("VB-Audio.Voicemeeter", "VB-Audio.Voicemeeter.Potato")) { Write-Host "Trying package: $pkgId" -ForegroundColor Cyan winget install --id $pkgId -e --scope user --silent --accept-package-agreements --accept-source-agreements 2>$null if ($LASTEXITCODE -eq 0) { $voicemeeterInstalled = $true Write-Host "VoiceMeeter installed successfully" -ForegroundColor Green break } } if (-not $voicemeeterInstalled) { Write-Host "" Write-Host "⚠️ VoiceMeeter installation via winget failed." -ForegroundColor Yellow Write-Host "" Write-Host "This may be due to:" -ForegroundColor Yellow Write-Host " • Network connectivity issues" -ForegroundColor Yellow Write-Host " • Windows version incompatibility" -ForegroundColor Yellow Write-Host " • Local security policy restrictions" -ForegroundColor Yellow Write-Host "" Write-Host "Please install VoiceMeeter manually:" -ForegroundColor Cyan Write-Host " 1. Visit: https://vb-audio.com/Voicemeeter/download.html" -ForegroundColor Cyan Write-Host " 2. Download and run the installer" -ForegroundColor Cyan Write-Host " 3. Restart Windows" -ForegroundColor Cyan Write-Host "" $answer = Read-Host "Continue without VoiceMeeter? (y/N)" if ($answer -notmatch '^[yY]') { Write-Host "Setup cancelled. Please install VoiceMeeter and run Install-BATCRelayBot again." -ForegroundColor Yellow exit 1 } } else { Write-Host "NOTE: VoiceMeeter requires system restart to fully activate." -ForegroundColor Cyan } } # Install pip requirements Write-Step "Python dependencies" "Installing Python dependencies..." | Add-Content -Path $logFile Write-Host "Upgrading pip..." -ForegroundColor Cyan & $pythonExe -m pip install --upgrade pip --quiet 2>&1 | Add-Content -Path $logFile if ($LASTEXITCODE -ne 0) { $errMsg = "ERROR: pip upgrade failed (exit code $LASTEXITCODE)" Write-Host $errMsg -ForegroundColor Red $errMsg | Add-Content -Path $logFile exit 1 } $reqFile = Join-Path $BotPath "requirements.txt" if (-not (Test-Path $reqFile)) { $errMsg = "ERROR: requirements.txt not found at $reqFile" Write-Host $errMsg -ForegroundColor Red $errMsg | Add-Content -Path $logFile exit 1 } Write-Host "Installing required packages (this may take a minute)..." -ForegroundColor Cyan & $pythonExe -m pip install -r $reqFile 2>&1 | Add-Content -Path $logFile if ($LASTEXITCODE -ne 0) { $errMsg = "ERROR: pip install failed (exit code $LASTEXITCODE). Check $logFile for details." Write-Host $errMsg -ForegroundColor Red $errMsg | Add-Content -Path $logFile exit 1 } Write-Host "Python dependencies installed." -ForegroundColor Green "Python dependencies installed successfully" | Add-Content -Path $logFile # Detect VoiceMeeter Write-Step "Detecting VoiceMeeter device" $tempOut = New-TemporaryFile $tempErr = New-TemporaryFile Start-Process -FilePath $ffmpegExe -ArgumentList @("-list_devices", "true", "-f", "dshow", "-i", "dummy") ` -RedirectStandardOutput $tempOut -RedirectStandardError $tempErr -NoNewWindow -Wait $listOutput = (Get-Content $tempOut -Raw -ErrorAction SilentlyContinue) + "`n" + (Get-Content $tempErr -Raw -ErrorAction SilentlyContinue) Remove-Item $tempOut, $tempErr -ErrorAction SilentlyContinue $deviceMatches = [regex]::Matches($listOutput, '"(Voicemeeter Out [^"]+)"\s*\(audio\)') $devices = $deviceMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique if ($devices.Count -eq 0) { Write-Host "No VoiceMeeter device found. Is VoiceMeeter installed and started?" -ForegroundColor Yellow $audioDeviceName = Prompt-WithDefault -Message "Enter device name manually" -Default $null } elseif ($devices.Count -eq 1) { $audioDeviceName = $devices[0] Write-Host "Found: $audioDeviceName" -ForegroundColor Green } else { Write-Host "Multiple VoiceMeeter devices found:" -ForegroundColor Cyan for ($i = 0; $i -lt $devices.Count; $i++) { Write-Host " [$i] $($devices[$i])" } $choice = -1 while ($choice -lt 0 -or $choice -ge $devices.Count) { $raw = Read-Host "Which device? (enter number)" [int]::TryParse($raw, [ref]$choice) | Out-Null } $audioDeviceName = $devices[$choice] } # Ask for Discord config Write-Step "Discord bot configuration" $configPath = Join-Path $BotPath "config.json" $writeConfig = $true if (Test-Path $configPath) { $answer = Read-Host "config.json exists. Overwrite? (y/N)" if ($answer -notmatch '^[yY]') { $writeConfig = $false Write-Host "config.json will not be changed." -ForegroundColor Yellow } } if ($writeConfig) { Write-Host "Find your bot token at: https://discord.com/developers/applications" -ForegroundColor Cyan $secureToken = Read-Host "Bot token (input will not be shown)" -AsSecureString $botToken = [Runtime.InteropServices.Marshal]::PtrToStringAuto( [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken) ) Write-Host "Get IDs by enabling Developer Mode in Discord settings, then right-click." -ForegroundColor Cyan $guildId = Prompt-WithDefault -Message "Server ID (guild_id)" -Default $null $voiceChannelId = Prompt-WithDefault -Message "Voice channel ID" -Default $null # Auto-detect VoiceMeeter path (installed via winget) $defaultVoicemeeterPath = "C:\Program Files (x86)\VB\Voicemeeter\voicemeeter_x64.exe" $voicemeeterPath = $null if (Test-Path $defaultVoicemeeterPath) { $voicemeeterPath = $defaultVoicemeeterPath Write-Host "VoiceMeeter detected automatically: $voicemeeterPath" -ForegroundColor Green } else { # Fallback: ask user if auto-detection failed Write-Host "VoiceMeeter not found in default location." -ForegroundColor Yellow $voicemeeterPath = Prompt-FilePath -Message "Path to VoiceMeeter (voicemeeter_x64.exe)" -Filter "Programs (*.exe)|*.exe" if ([string]::IsNullOrWhiteSpace($voicemeeterPath)) { Write-Host "WARNING: VoiceMeeter path not provided. You may need to configure this manually." -ForegroundColor Yellow } } $voicemeeterProcessName = if ($voicemeeterPath) { [System.IO.Path]::GetFileNameWithoutExtension($voicemeeterPath) } else { "voicemeeter_x64" } $batcPath = Prompt-FilePath -Message "Path to BeyondATC.exe" -Filter "Programs (*.exe)|*.exe" $batcProcessName = [System.IO.Path]::GetFileNameWithoutExtension($batcPath) $voicemeeterWait = Prompt-WithDefault -Message "Wait time after starting VoiceMeeter (seconds)" -Default "6" $batcWait = Prompt-WithDefault -Message "Wait time after starting BeyondATC (seconds)" -Default "8" $configObject = [ordered]@{ bot_token = $botToken guild_id = [int64]$guildId voice_channel_id = [int64]$voiceChannelId audio_device_name = $audioDeviceName python_path = $pythonExe voicemeeter_path = $voicemeeterPath voicemeeter_process_name = $voicemeeterProcessName voicemeeter_wait_seconds = [int]$voicemeeterWait batc_path = $batcPath batc_process_name = $batcProcessName batc_wait_seconds = [int]$batcWait } $jsonContent = $configObject | ConvertTo-Json -Depth 5 [System.IO.File]::WriteAllText($configPath, $jsonContent, (New-Object System.Text.UTF8Encoding($false))) Write-Host "config.json created." -ForegroundColor Green } Write-Step "Setup complete" Write-Host "Technical prerequisites are installed and configured." -ForegroundColor Green Write-Host "" Write-Host "Still to do (manual):" -ForegroundColor Cyan Write-Host " 1. Create a Discord bot at https://discord.com/developers/applications" -ForegroundColor Cyan Write-Host " 2. Invite it to your server with 'Connect' and 'Speak' permissions" -ForegroundColor Cyan Write-Host " 3. Set channel permissions for the bot's role: View Channel, Connect, Speak" -ForegroundColor Cyan Write-Host "" Write-Host "Then start the bot with: Start-BATCRelayBot" -ForegroundColor Green } catch { $errorMsg = $_.Exception.Message "" | Add-Content -Path $logFile "ERROR: $_" | Add-Content -Path $logFile "Exception: $errorMsg" | Add-Content -Path $logFile Write-Host "" Write-Host "❌ INSTALLATION ERROR:" -ForegroundColor Red Write-Host $errorMsg -ForegroundColor Red Write-Host "" Write-Host "Full error details:" -ForegroundColor Yellow Write-Host $_ -ForegroundColor Yellow Write-Host "" Write-Host "📝 Log file saved at:" -ForegroundColor Cyan Write-Host " $logFile" -ForegroundColor White Write-Host "" Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Gray Write-Host "Press ENTER to close this window..." -ForegroundColor Cyan Read-Host exit 1 } } |