Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $script:RepoRoot = Split-Path -Parent $PSScriptRoot $script:ToolsRoot = Join-Path $script:RepoRoot ".tools" $script:BinRoot = Join-Path $script:ToolsRoot "bin" $script:GoRoot = Join-Path $script:ToolsRoot "go" $script:GoBin = Join-Path $script:GoRoot "bin" $script:FFmpegRoot = Join-Path $script:ToolsRoot "ffmpeg" $script:FFmpegBin = Join-Path $script:FFmpegRoot "bin" $script:VenvRoot = Join-Path $script:RepoRoot ".venv" $script:VenvScripts = Join-Path $script:VenvRoot "Scripts" $script:LocalRoot = Join-Path $script:RepoRoot ".local" $script:GoPathRoot = Join-Path $script:LocalRoot "go" $script:GoModCacheRoot = Join-Path $script:GoPathRoot "pkg\mod" $script:GoBuildCacheRoot = Join-Path $script:LocalRoot "go-build-cache" $script:CredentialFile = Join-Path $script:LocalRoot "git-credentials.psd1" function Write-Step { param([string]$Message) Write-Host "[ai-media-hub] $Message" } function Invoke-CheckedCommand { param( [Parameter(Mandatory = $true)][string]$FilePath, [string[]]$Arguments = @() ) & $FilePath @Arguments if ($LASTEXITCODE -ne 0) { $joined = if ($Arguments.Count -gt 0) { " " + ($Arguments -join " ") } else { "" } throw "명령 실행 실패: $FilePath$joined" } } function Ensure-Directory { param([Parameter(Mandatory = $true)][string]$Path) if (-not (Test-Path -LiteralPath $Path)) { New-Item -ItemType Directory -Path $Path | Out-Null } } function Get-RepoRoot { return $script:RepoRoot } function Get-ToolsRoot { return $script:ToolsRoot } function Get-BinRoot { return $script:BinRoot } function Get-GoRoot { return $script:GoRoot } function Get-GoBin { return $script:GoBin } function Get-FFmpegBin { return $script:FFmpegBin } function Get-VenvRoot { return $script:VenvRoot } function Get-VenvScripts { return $script:VenvScripts } function Get-LocalRoot { return $script:LocalRoot } function Get-GoPathRoot { return $script:GoPathRoot } function Get-GoModCacheRoot { return $script:GoModCacheRoot } function Get-GoBuildCacheRoot { return $script:GoBuildCacheRoot } function Get-CredentialFile { return $script:CredentialFile } function Resolve-SystemCommand { param([Parameter(Mandatory = $true)][string]$Name) try { return (Get-Command $Name -ErrorAction Stop).Source } catch { return $null } } function Resolve-PythonExe { $candidates = @( (Join-Path $script:VenvScripts "python.exe"), (Resolve-SystemCommand -Name "python") ) foreach ($candidate in $candidates) { if ($candidate -and (Test-Path -LiteralPath $candidate)) { return $candidate } } throw "python.exe를 찾을 수 없습니다. Python 3.12+가 필요합니다." } function Resolve-BasePythonExe { $all = @(Get-Command python -All -ErrorAction SilentlyContinue) foreach ($item in $all) { if ($item.Source -and $item.Source -notlike "*\.venv\*") { return $item.Source } } $fallback = Resolve-SystemCommand -Name "python" if ($fallback -and $fallback -notlike "*\.venv\*") { return $fallback } throw "시스템 python.exe를 찾지 못했습니다." } function Resolve-VenvPythonExe { $pythonExe = Join-Path $script:VenvScripts "python.exe" if (-not (Test-Path -LiteralPath $pythonExe)) { throw ".venv가 아직 준비되지 않았습니다. scripts/setup-dev.ps1를 먼저 실행하세요." } return $pythonExe } function Use-LocalTooling { Ensure-Directory -Path $script:ToolsRoot Ensure-Directory -Path $script:BinRoot Ensure-Directory -Path $script:LocalRoot Ensure-Directory -Path $script:GoPathRoot Ensure-Directory -Path $script:GoModCacheRoot Ensure-Directory -Path $script:GoBuildCacheRoot $segments = @( $script:BinRoot, $script:GoBin, $script:FFmpegBin, $script:VenvScripts, $env:PATH ) | Where-Object { $_ -and $_.Trim() -ne "" } $env:PATH = ($segments | Select-Object -Unique) -join ";" $env:GOPATH = $script:GoPathRoot $env:GOMODCACHE = $script:GoModCacheRoot $env:GOCACHE = $script:GoBuildCacheRoot } function Ensure-Venv { $venvPython = Join-Path $script:VenvScripts "python.exe" if (Test-Path -LiteralPath $venvPython) { return $venvPython } Ensure-Directory -Path $script:LocalRoot $pythonExe = Resolve-SystemCommand -Name "python" if (-not $pythonExe) { throw "시스템 python이 없어 .venv를 만들 수 없습니다." } Write-Step ".venv 생성" & $pythonExe -m venv $script:VenvRoot return $venvPython } function Install-PythonRequirements { param([switch]$UpgradePip) $venvPython = Ensure-Venv $systemPython = Resolve-BasePythonExe if ($UpgradePip) { Write-Step "pip 업그레이드" Invoke-CheckedCommand -FilePath $systemPython -Arguments @("-m", "pip", "--python", $venvPython, "install", "--upgrade", "pip") } Write-Step "worker requirements 설치" Invoke-CheckedCommand -FilePath $systemPython -Arguments @("-m", "pip", "--python", $venvPython, "install", "-r", (Join-Path $script:RepoRoot "worker\requirements.txt")) } function New-CmdShim { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Target ) $content = @( "@echo off", "set SCRIPT_DIR=%~dp0", "`"%SCRIPT_DIR%$Target`" %*" ) -join "`r`n" Set-Content -LiteralPath $Path -Value $content -Encoding ASCII } function Ensure-CommandShims { Ensure-Directory -Path $script:BinRoot $pythonShimTarget = "..\..\ .venv\Scripts\python.exe".Replace(" ", "") New-CmdShim -Path (Join-Path $script:BinRoot "python3.cmd") -Target $pythonShimTarget $ytDlpExe = Join-Path $script:VenvScripts "yt-dlp.exe" if (Test-Path -LiteralPath $ytDlpExe) { New-CmdShim -Path (Join-Path $script:BinRoot "yt-dlp.cmd") -Target "..\..\ .venv\Scripts\yt-dlp.exe".Replace(" ", "") } } function Test-GoReady { return [bool](Resolve-SystemCommand -Name "go") } function Test-FFmpegReady { return [bool](Resolve-SystemCommand -Name "ffmpeg") } function Test-YtDlpReady { return [bool](Resolve-SystemCommand -Name "yt-dlp") } function Install-LocalGo { param( [string]$Version = "1.24.0", [string]$ArchiveUrl = "" ) if (Test-Path -LiteralPath (Join-Path $script:GoBin "go.exe")) { Write-Step "로컬 Go 이미 준비됨" return } if (-not $ArchiveUrl) { $ArchiveUrl = "https://go.dev/dl/go$Version.windows-amd64.zip" } Ensure-Directory -Path $script:ToolsRoot $archivePath = Join-Path $script:LocalRoot "go-$Version.windows-amd64.zip" Write-Step "로컬 Go 다운로드: $ArchiveUrl" Invoke-WebRequest -Uri $ArchiveUrl -OutFile $archivePath if (Test-Path -LiteralPath $script:GoRoot) { Remove-Item -Recurse -Force $script:GoRoot } Expand-Archive -LiteralPath $archivePath -DestinationPath $script:ToolsRoot -Force } function Install-LocalFFmpeg { param([string]$ArchiveUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip") if (Test-Path -LiteralPath (Join-Path $script:FFmpegBin "ffmpeg.exe")) { Write-Step "로컬 ffmpeg 이미 준비됨" return } Ensure-Directory -Path $script:ToolsRoot $archivePath = Join-Path $script:LocalRoot "ffmpeg-release-essentials.zip" Write-Step "로컬 ffmpeg 다운로드: $ArchiveUrl" Invoke-WebRequest -Uri $ArchiveUrl -OutFile $archivePath $extractRoot = Join-Path $script:LocalRoot "ffmpeg-extract" if (Test-Path -LiteralPath $extractRoot) { Remove-Item -Recurse -Force $extractRoot } Expand-Archive -LiteralPath $archivePath -DestinationPath $extractRoot -Force $binCandidate = Get-ChildItem -Path $extractRoot -Recurse -Filter "ffmpeg.exe" | Select-Object -First 1 if (-not $binCandidate) { throw "ffmpeg.exe를 압축 파일에서 찾지 못했습니다." } $sourceBin = Split-Path -Parent $binCandidate.FullName if (Test-Path -LiteralPath $script:FFmpegRoot) { Remove-Item -Recurse -Force $script:FFmpegRoot } Ensure-Directory -Path $script:FFmpegBin Copy-Item -Path (Join-Path $sourceBin "*") -Destination $script:FFmpegBin -Recurse -Force } function Get-AppEnvironment { param( [Parameter(Mandatory = $true)][string]$WorkspaceRoot, [Parameter(Mandatory = $true)][string]$DownloadsDir, [Parameter(Mandatory = $true)][string]$SqlitePath, [Parameter(Mandatory = $true)][string]$AppAddr, [string]$SearxUrl = "http://127.0.0.1:18080" ) return @{ APP_ROOT = $WorkspaceRoot APP_ADDR = $AppAddr SQLITE_PATH = $SqlitePath DOWNLOADS_DIR = $DownloadsDir FRONTEND_DIR = (Join-Path $WorkspaceRoot "frontend") WORKER_SCRIPT = (Join-Path $WorkspaceRoot "worker\downloader.py") SEARXNG_BASE_URL = $SearxUrl SEARXNG_GOOGLE_VIDEO_ENGINE = "google videos" SEARXNG_WEB_ENGINE = "google" } } function Import-GitCredential { $credentialFile = Get-CredentialFile if (-not (Test-Path -LiteralPath $credentialFile)) { throw "git 자격정보 파일이 없습니다: $credentialFile" } return Import-PowerShellDataFile -Path $credentialFile }