# CCLS Games CLI Tool # Complete Version with Setup, Search, Get functionality, and Logging # Configuration $baseUrl = "https://games.ccls.icu" $cliApiUrl = "$baseUrl/CLI" $settingsFolder = ".\settings" $credentialsFile = "$settingsFolder\credentials.dat" $settingsFile = "$settingsFolder\settings.json" # Using JSON instead of INI for simplicity $logsFolder = ".\logs" # Logs folder path # Script-level variables for progress display, logging, and credentials $script:progressInitialized = $false $script:topProgressLine = $null $script:logFile = $null $script:sessionStartTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" # Cache credentials at the script level $script:cachedCredentials = $null # Ensure settings and logs directories exist if (-not (Test-Path $settingsFolder)) { New-Item -Path $settingsFolder -ItemType Directory | Out-Null } if (-not (Test-Path $logsFolder)) { New-Item -Path $logsFolder -ItemType Directory | Out-Null } # Initialize log file for the current session $script:logFile = Join-Path -Path $logsFolder -ChildPath "ccls_session_$($script:sessionStartTime).log" "CCLS Games CLI Session started at $($script:sessionStartTime)" | Out-File -FilePath $script:logFile # Custom Write-Host function to handle logging and output filtering function Write-CCLSHost { param ( [Parameter(Position = 0)] [string]$Message = "", [Parameter()] [System.ConsoleColor]$ForegroundColor = [System.ConsoleColor]::White, [Parameter()] [System.ConsoleColor]$BackgroundColor = [System.ConsoleColor]::Black, [Parameter()] [switch]$NoNewline, [Parameter()] [switch]$Log, [Parameter()] [switch]$NoConsole ) # Check if the message is marked for removal if ($Message.StartsWith('#')) { # Don't show in console, but log it if needed if ($Log) { $cleanMessage = $Message.Substring(1).Trim() if (-not [string]::IsNullOrWhiteSpace($cleanMessage)) { $cleanMessage | Out-File -FilePath $script:logFile -Append } } return } if ($Message.StartsWith('/')) { # Skip completely - don't log or display return } # Handle empty lines if ([string]::IsNullOrWhiteSpace($Message)) { # Display in console if not suppressed if (-not $NoConsole) { Write-Host "" -NoNewline:$NoNewline } # But don't log empty lines return } # Display in console if not suppressed if (-not $NoConsole) { Write-Host $Message -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor -NoNewline:$NoNewline } # Log if requested if ($Log) { $plainMessage = $Message $plainMessage | Out-File -FilePath $script:logFile -Append } } # Initialize or load settings function Initialize-Settings { # Get version information using existing function $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion if (-not (Test-Path $settingsFile)) { # Create default settings file $defaultSettings = @{ RememberLogin = $false DownloadPath = ".\downloads" TempDownloadPath = ".\tmp" HasCompletedSetup = $false Version = $currentVersion # Add version to settings } $defaultSettings | ConvertTo-Json | Set-Content -Path $settingsFile # Return settings without version change flag return $defaultSettings } # Load settings $settings = Get-Content -Path $settingsFile | ConvertFrom-Json # Check if Version property exists, add it if not if (-not (Get-Member -InputObject $settings -Name "Version" -MemberType Properties)) { $settings | Add-Member -MemberType NoteProperty -Name "Version" -Value $currentVersion $settings | ConvertTo-Json | Set-Content -Path $settingsFile } # Check if version has changed and update if needed if ($settings.Version -ne $currentVersion) { Write-CCLSHost "#Detected version change from $($settings.Version) to $currentVersion" -Log -NoConsole # Store the old version $oldVersion = $settings.Version # Update to new version $settings.Version = $currentVersion $settings | ConvertTo-Json | Set-Content -Path $settingsFile # Keep track that version has changed for later notification $script:versionChanged = $true $script:previousVersion = $oldVersion } return $settings } # Save settings function Save-Settings($settings) { $settings | ConvertTo-Json | Set-Content -Path $settingsFile } # Store credentials securely function Save-Credentials($username, $password) { $credentials = @{ Username = $username Password = $password } | ConvertTo-Json # Simple encryption (replace with more secure method if needed) $securePassword = ConvertTo-SecureString -String $credentials -AsPlainText -Force $encryptedText = ConvertFrom-SecureString -SecureString $securePassword # Save to file $encryptedText | Set-Content -Path $credentialsFile } # Load stored credentials function Get-StoredCredentials { if (Test-Path $credentialsFile) { try { $encryptedText = Get-Content -Path $credentialsFile $securePassword = ConvertTo-SecureString -String $encryptedText $credentials = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword) ) return $credentials | ConvertFrom-Json } catch { Write-CCLSHost "Error reading credentials: $($_.Exception.Message)" -ForegroundColor Red -Log return $null } } return $null } # Run setup process function Start-Setup { Write-CCLSHost "`n`nCCLS Games CLI Setup" -ForegroundColor Green -Log Write-CCLSHost "=====================" -ForegroundColor Green -Log Write-CCLSHost "Please configure the following settings:`n" -ForegroundColor Cyan -Log $settings = Initialize-Settings # Get game installation directory $validPath = $false while (-not $validPath) { Write-CCLSHost "Set your games default installation directory: " -ForegroundColor Yellow -NoNewline -Log $downloadPath = Read-Host Write-CCLSHost "$downloadPath" -NoConsole -Log if ([string]::IsNullOrWhiteSpace($downloadPath)) { Write-CCLSHost "Please enter a valid directory path." -ForegroundColor Red -Log } else { # Create directory if it doesn't exist if (-not (Test-Path $downloadPath)) { try { New-Item -ItemType Directory -Path $downloadPath -Force | Out-Null $validPath = $true } catch { Write-CCLSHost "Error creating directory: $($_.Exception.Message)" -ForegroundColor Red -Log } } else { $validPath = $true } } } # Get temporary download directory $validTempPath = $false while (-not $validTempPath) { Write-CCLSHost "Set the temporary directory of downloading files before they have finished downloading: " -ForegroundColor Yellow -NoNewline -Log $tempDownloadPath = Read-Host Write-CCLSHost "$tempDownloadPath" -NoConsole -Log if ([string]::IsNullOrWhiteSpace($tempDownloadPath)) { Write-CCLSHost "Please enter a valid directory path." -ForegroundColor Red -Log } else { # Create directory if it doesn't exist if (-not (Test-Path $tempDownloadPath)) { try { New-Item -ItemType Directory -Path $tempDownloadPath -Force | Out-Null $validTempPath = $true } catch { Write-CCLSHost "Error creating directory: $($_.Exception.Message)" -ForegroundColor Red -Log } } else { $validTempPath = $true } } } # Update settings $settings.DownloadPath = $downloadPath $settings.TempDownloadPath = $tempDownloadPath $settings.HasCompletedSetup = $true Save-Settings -settings $settings Write-CCLSHost "`nGreat, you have now completed the setup. Type 'help' for a list of commands to get you started." -ForegroundColor Green -Log } # Validate username against server function Test-Username($username) { $params = @{ Uri = "$cliApiUrl/username_check.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $username } } try { $response = Invoke-RestMethod @params return $response.exists } catch { Write-CCLSHost "Error connecting to the server: $($_.Exception.Message)" -ForegroundColor Red -Log return $false } } # Validate password against server function Test-Password($username, $password) { $params = @{ Uri = "$cliApiUrl/password_check.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $username password = $password } } try { $response = Invoke-RestMethod @params return $response.success } catch { Write-CCLSHost "Error connecting to the server: $($_.Exception.Message)" -ForegroundColor Red -Log return $false } } function Start-AutoLogin { $credentials = Get-StoredCredentials if ($credentials -ne $null) { Write-CCLSHost "#Attempting to login with stored credentials..." -Log if (Test-Password -username $credentials.Username -password $credentials.Password) { Write-CCLSHost "#Auto-login successful!" -Log # Cache the credentials for the session $script:cachedCredentials = @{ Username = $credentials.Username Password = $credentials.Password } return @{ Success = $true Username = $credentials.Username } } else { Write-CCLSHost "Stored credentials are no longer valid." -ForegroundColor Yellow -Log # Remove invalid credentials if (Test-Path $credentialsFile) { Remove-Item -Path $credentialsFile -Force } } } return @{ Success = $false } } function Start-ManualLogin { $maxAttempts = 3 $attempts = 0 while ($attempts -lt $maxAttempts) { Write-CCLSHost "Username: " -ForegroundColor Cyan -NoNewline -Log $username = Read-Host Write-CCLSHost "$username" -NoConsole -Log # Check if username exists Write-CCLSHost "#Checking username..." -Log if (Test-Username -username $username) { Write-CCLSHost "#Username found!" -Log Write-CCLSHost "Password: " -ForegroundColor Cyan -NoNewline -Log $password = Read-Host -AsSecureString $passwordPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto( [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) ) Write-CCLSHost "********" -NoConsole -Log Write-CCLSHost "#Validating password..." -Log if (Test-Password -username $username -password $passwordPlain) { Write-CCLSHost "#Login successful!" -Log # Cache the credentials for the session $script:cachedCredentials = @{ Username = $username Password = $passwordPlain } # Ask if user wants to save credentials - no newline before this Write-CCLSHost "Do you want to remember your login info for next time? (Y/N)" -ForegroundColor Yellow -Log $rememberLogin = Read-Host Write-CCLSHost "$rememberLogin" -NoConsole -Log if ($rememberLogin.ToLower() -eq "y") { Save-Credentials -username $username -password $passwordPlain $settings = Initialize-Settings $settings.RememberLogin = $true Save-Settings -settings $settings Write-CCLSHost "#Login information saved." -Log } return @{ Success = $true Username = $username } } else { Write-CCLSHost "Incorrect password, please try again." -ForegroundColor Red -Log $attempts++ } } else { Write-CCLSHost "Username not found, please try again." -ForegroundColor Red -Log $attempts++ } } Write-CCLSHost "Too many failed login attempts. Please try again later." -ForegroundColor Red -Log return @{ Success = $false } } # Updated Install-Utility function function Install-Utility { param ( [string]$UtilityName ) # Handle different utilities switch ($UtilityName.ToLower()) { "7zip" { Install7Zip } "requests" { InstallPythonRequests } "python" { Install-Python } default { Write-CCLSHost "Unsupported utility: $UtilityName" -ForegroundColor Red -Log Write-CCLSHost "Currently supported utilities: 7zip, requests, python" -ForegroundColor Green -Log } } } # Function to install 7-Zip (internal function for Install-Utility) function Install7Zip { # Determine the current script directory $scriptLocation = if ($PSScriptRoot) { # If running from a script, use its location $PSScriptRoot } else { # If running in console, use current directory (Get-Location).Path } # Target directory is the script location $targetDir = $scriptLocation # URLs for download $zipUrl = "https://games.ccls.icu/CLI/downloads/7zip.zip" $zipPath = Join-Path -Path $targetDir -ChildPath "7zip.zip" Write-CCLSHost "Starting 7-Zip installation..." -ForegroundColor Cyan -Log try { # Download the zip file Write-CCLSHost "Downloading 7-Zip package from $zipUrl" -ForegroundColor Yellow -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/1.0") $webClient.DownloadFile($zipUrl, $zipPath) if (-not (Test-Path $zipPath)) { Write-CCLSHost "Failed to download 7-Zip package." -ForegroundColor Red -Log return } Write-CCLSHost "Download completed successfully." -ForegroundColor Green -Log # Create temporary extraction directory $tempExtractPath = Join-Path -Path $targetDir -ChildPath "7zip_temp" if (Test-Path $tempExtractPath) { Remove-Item -Path $tempExtractPath -Recurse -Force } New-Item -ItemType Directory -Path $tempExtractPath -Force | Out-Null # Final 7zip directory $finalPath = Join-Path -Path $targetDir -ChildPath "7zip" if (Test-Path $finalPath) { Remove-Item -Path $finalPath -Recurse -Force } # Extract the zip file to temp location Write-CCLSHost "Extracting 7-Zip package..." -ForegroundColor Yellow -Log $extractionSuccess = $false try { # Try to use built-in Expand-Archive Expand-Archive -Path $zipPath -DestinationPath $tempExtractPath -Force $extractionSuccess = $true } catch { Write-CCLSHost "#Built-in extraction failed, trying alternate method: $($_.Exception.Message)" -ForegroundColor Yellow -Log -NoConsole try { # Alternative extraction method using .NET Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $tempExtractPath) $extractionSuccess = $true } catch { Write-CCLSHost "Extraction failed: $($_.Exception.Message)" -ForegroundColor Red -Log $extractionSuccess = $false } } # Fix nested folder issue if extraction was successful if ($extractionSuccess) { # Check if we have a nested 7zip folder $nestedFolder = Join-Path -Path $tempExtractPath -ChildPath "7zip" if (Test-Path $nestedFolder) { # Move the contents from nested folder to final location Write-CCLSHost "#Fixing folder structure..." -Log -NoConsole Move-Item -Path $nestedFolder -Destination $targetDir -Force $extractionSuccess = $true } else { # If no nested folder, just rename temp folder to final name Rename-Item -Path $tempExtractPath -NewName "7zip" $extractionSuccess = $true } } # Clean up the downloaded zip file if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force Write-CCLSHost "#Removed temporary download file." -ForegroundColor Gray -Log -NoConsole } # Clean up temp folder if it still exists if (Test-Path $tempExtractPath) { Remove-Item -Path $tempExtractPath -Recurse -Force Write-CCLSHost "#Removed temporary extraction folder." -ForegroundColor Gray -Log -NoConsole } # Verify installation $exePath = Join-Path -Path $finalPath -ChildPath "7z.exe" if ($extractionSuccess -and (Test-Path $exePath)) { Write-CCLSHost "7-Zip has been successfully installed to $finalPath" -ForegroundColor Green -Log Write-CCLSHost "You can now use 7-Zip for file extraction." -ForegroundColor Green -Log } else { Write-CCLSHost "7-Zip installation was not completed successfully." -ForegroundColor Red -Log Write-CCLSHost "Please try again or manually install 7-Zip." -ForegroundColor Yellow -Log } } catch { Write-CCLSHost "Error during installation: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "7-Zip installation failed." -ForegroundColor Red -Log } } # Function to install Python requests library (internal function for Install-Utility) function InstallPythonRequests { Write-CCLSHost "Checking for Python installation..." -ForegroundColor Cyan -Log # First check if Python is installed try { $pythonResult = python --version 2>&1 if ($pythonResult -match "Python (\d+\.\d+\.\d+)") { $pythonVersion = $matches[1] Write-CCLSHost "Python detected (v$pythonVersion)" -ForegroundColor Green -Log } else { Write-CCLSHost "Python not detected" -ForegroundColor Red -Log Write-CCLSHost "Please install Python first before installing the requests library." -ForegroundColor Yellow -Log Write-CCLSHost "Download Python from: https://www.python.org/downloads/" -ForegroundColor Yellow -Log return } } catch { Write-CCLSHost "Python is not installed or not in the system PATH." -ForegroundColor Red -Log Write-CCLSHost "Please install Python first before installing the requests library." -ForegroundColor Yellow -Log Write-CCLSHost "Download Python from: https://www.python.org/downloads/" -ForegroundColor Yellow -Log return } # Check if requests is already installed Write-CCLSHost "Checking if requests library is already installed..." -ForegroundColor Cyan -Log try { $requestsCheck = python -c "import requests; print('Installed (v{0})'.format(requests.__version__))" 2>&1 if ($requestsCheck -match "Installed \(v([\d\.]+)\)") { $requestsVersion = $matches[1] Write-CCLSHost "The requests library is already installed (v$requestsVersion)" -ForegroundColor Green -Log # Ask if user wants to upgrade Write-CCLSHost "Do you want to upgrade to the latest version? (Y/N)" -ForegroundColor Yellow -Log $upgradeConfirmation = Read-Host Write-CCLSHost "$upgradeConfirmation" -NoConsole -Log if ($upgradeConfirmation.ToLower() -ne "y") { Write-CCLSHost "Installation skipped." -ForegroundColor Yellow -Log return } # If yes, set upgrade flag $upgradeFlag = "--upgrade" } else { # Not installed $upgradeFlag = "" } } catch { # Not installed or error checking Write-CCLSHost "The requests library is not installed." -ForegroundColor Yellow -Log $upgradeFlag = "" } Write-CCLSHost "Installing Python requests library..." -ForegroundColor Cyan -Log try { # Use Start-Process with -Wait to run pip in the background $pipCommand = "pip" $pipArgs = "install $upgradeFlag requests" # Temporary files for output and error $tempOutputFile = [System.IO.Path]::GetTempFileName() $tempErrorFile = [System.IO.Path]::GetTempFileName() Write-CCLSHost "#Running: $pipCommand $pipArgs" -Log -NoConsole # Show progress indicator Write-CCLSHost "Installing..." -NoNewline -ForegroundColor Yellow -Log # Run pip in background and redirect output to temp files $process = Start-Process -FilePath $pipCommand -ArgumentList $pipArgs -NoNewWindow -PassThru -RedirectStandardOutput $tempOutputFile -RedirectStandardError $tempErrorFile -Wait # Read output and error from temp files $output = Get-Content -Path $tempOutputFile -Raw $error = Get-Content -Path $tempErrorFile -Raw # Clean up temp files Remove-Item -Path $tempOutputFile -Force -ErrorAction SilentlyContinue Remove-Item -Path $tempErrorFile -Force -ErrorAction SilentlyContinue Write-CCLSHost "" -Log # Newline after progress indicator # Verify installation regardless of process exit code # This is more reliable than checking process exit code $installSuccess = $false try { $verifyResult = python -c "import requests; print('Installed (v{0})'.format(requests.__version__))" 2>&1 if ($verifyResult -match "Installed \(v([\d\.]+)\)") { $installSuccess = $true $newVersion = $matches[1] } } catch { $installSuccess = $false } if ($installSuccess) { # Check if output contains "Successfully installed" as additional confirmation if (-not [string]::IsNullOrWhiteSpace($output) -and ($output -match "Successfully installed" -or $output -match "Requirement already satisfied")) { Write-CCLSHost "Python requests library v$newVersion installed successfully!" -ForegroundColor Green -Log # Show summary of what happened if ($output -match "Requirement already satisfied") { Write-CCLSHost "All requirements were already satisfied." -ForegroundColor Green -Log } else { Write-CCLSHost "Installation completed successfully." -ForegroundColor Green -Log } } else { Write-CCLSHost "Python requests library appears to be installed (v$newVersion)" -ForegroundColor Green -Log Write-CCLSHost "But the installation process reported unusual output." -ForegroundColor Yellow -Log } } else { Write-CCLSHost "Failed to install Python requests library." -ForegroundColor Red -Log if (-not [string]::IsNullOrWhiteSpace($error)) { Write-CCLSHost "Error details:" -ForegroundColor Red -Log Write-CCLSHost $error -ForegroundColor Red -Log } if (-not [string]::IsNullOrWhiteSpace($output)) { Write-CCLSHost "Output details:" -ForegroundColor Yellow -Log Write-CCLSHost $output -ForegroundColor Yellow -Log } } } catch { Write-CCLSHost "Error during installation: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Function to install Python function Install-Python { Write-CCLSHost "Starting Python installation process..." -ForegroundColor Cyan -Log # Create a temporary directory for the download $tempDir = [System.IO.Path]::GetTempPath() $tempDownloadPath = Join-Path -Path $tempDir -ChildPath "python_installer.exe" # Determine if system is 64-bit or 32-bit $is64Bit = [Environment]::Is64BitOperatingSystem # Get Python version info from our server try { Write-CCLSHost "Retrieving Python version information..." -ForegroundColor Yellow -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/1.0") # Use the CCLS API endpoint to get Python version info $pythonInfoJson = $webClient.DownloadString("$cliApiUrl/install_python.php") $pythonInfo = $pythonInfoJson | ConvertFrom-Json # Use the appropriate URL based on system architecture if ($is64Bit) { $pythonUrl = $pythonInfo.windows_64bit_url Write-CCLSHost "Using 64-bit installer for Python $($pythonInfo.version)" -ForegroundColor Green -Log } else { $pythonUrl = $pythonInfo.windows_32bit_url Write-CCLSHost "Using 32-bit installer for Python $($pythonInfo.version)" -ForegroundColor Green -Log } } catch { Write-CCLSHost "Error retrieving Python information from server: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Installation cannot proceed. Please try again later." -ForegroundColor Red -Log return } # Download the Python installer try { Write-CCLSHost "Starting download from $pythonUrl" -ForegroundColor Yellow -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/1.0") Write-CCLSHost "Downloading Python installer to $tempDownloadPath..." -ForegroundColor Cyan -Log Write-CCLSHost "Download in progress, this may take a few minutes depending on your connection speed..." -ForegroundColor Yellow -Log # Use synchronous download with simple progress indication $sw = [System.Diagnostics.Stopwatch]::StartNew() $webClient.DownloadFile($pythonUrl, $tempDownloadPath) $sw.Stop() if (Test-Path $tempDownloadPath) { $fileSize = (Get-Item $tempDownloadPath).Length / 1MB Write-CCLSHost "Download completed successfully! Downloaded $([Math]::Round($fileSize, 2)) MB in $([Math]::Round($sw.Elapsed.TotalSeconds, 1)) seconds." -ForegroundColor Green -Log } else { Write-CCLSHost "Download failed - file not found at expected location." -ForegroundColor Red -Log return } } catch { Write-CCLSHost "Error downloading Python installer: $($_.Exception.Message)" -ForegroundColor Red -Log return } # Launch the installer try { Write-CCLSHost "Starting Python installer..." -ForegroundColor Green -Log Write-CCLSHost "Please follow the on-screen instructions to complete the installation." -ForegroundColor Yellow -Log Write-CCLSHost "Recommended options:" -ForegroundColor Yellow -Log Write-CCLSHost " - Check 'Add Python to PATH'" -ForegroundColor Yellow -Log Write-CCLSHost " - Choose 'Customize installation' for more options" -ForegroundColor Yellow -Log Write-CCLSHost " - Ensure 'pip' is selected in the optional features" -ForegroundColor Yellow -Log # Start the installer $process = Start-Process -FilePath $tempDownloadPath -Wait -PassThru # Handle different exit codes if ($process.ExitCode -eq 0) { Write-CCLSHost "Python installation process completed successfully." -ForegroundColor Green -Log } elseif ($process.ExitCode -eq 1602) { Write-CCLSHost "Installation was cancelled by the user." -ForegroundColor Yellow -Log } else { Write-CCLSHost "Python installer exited with code: $($process.ExitCode)" -ForegroundColor Yellow -Log } Write-CCLSHost "To verify the installation, type 'python --version' in a new command prompt window." -ForegroundColor Cyan -Log } catch { Write-CCLSHost "Error launching Python installer: $($_.Exception.Message)" -ForegroundColor Red -Log } # Clean up the downloaded file try { if (Test-Path $tempDownloadPath) { Remove-Item -Path $tempDownloadPath -Force Write-CCLSHost "Cleaned up temporary installer file." -ForegroundColor Gray -Log -NoConsole } } catch { Write-CCLSHost "Note: Could not remove temporary installer file: $($_.Exception.Message)" -ForegroundColor Yellow -Log -NoConsole } } function Search-Game($id) { # Validate ID format - now supports both cg0000 and cb0000 formats if ($id -notmatch "^(cg|cb)\d{4}$") { Write-CCLSHost "Invalid ID format. Please use format 'cg0000' for games or 'cb0000' for bundles." -ForegroundColor Red -Log return } # Check if credentials are cached if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return } # Determine if this is a game or bundle $isBundle = $id -match "^cb\d{4}$" $itemType = if ($isBundle) { "bundle" } else { "game" } try { # Set up request parameters with credentials $params = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $id } } Write-CCLSHost "#Searching for $itemType information..." -Log # Fetch data from server try { $response = Invoke-RestMethod @params # Check if the request was successful if (-not $response.success) { Write-CCLSHost "Error: $($response.message)" -ForegroundColor Red -Log return } $itemInfo = $response } catch { Write-CCLSHost "Error fetching $itemType information: $($_.Exception.Message)" -ForegroundColor Red -Log return } # Display item information in a formatted way Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "$($itemType.ToUpper()) Information for $($itemInfo.name) ($($itemInfo.id))" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "`nDescription:" -ForegroundColor Cyan -Log Write-CCLSHost $itemInfo.description -Log if ($itemInfo.safety_score -or $itemInfo.safety_level) { Write-CCLSHost "`nSafety:" -ForegroundColor Cyan -Log if ($itemInfo.safety_score) { Write-CCLSHost "Score: $($itemInfo.safety_score)" -Log } if ($itemInfo.safety_level) { Write-CCLSHost "Level: $($itemInfo.safety_level)" -Log } } Write-CCLSHost "`nDetails:" -ForegroundColor Cyan -Log # Handle different size properties for game vs bundle if ($isBundle) { if ($itemInfo.zipped_size) { Write-CCLSHost "Zipped Size: $($itemInfo.zipped_size)" -Log } if ($itemInfo.unzipped_size) { Write-CCLSHost "Unzipped Size: $($itemInfo.unzipped_size)" -Log } if ($itemInfo.games_included) { Write-CCLSHost "Games Included: $($itemInfo.games_included)" -Log } } else { if ($itemInfo.size) { Write-CCLSHost "Size: $($itemInfo.size)" -Log } } if ($itemInfo.version -and $itemInfo.version -ne "") { Write-CCLSHost "Version: $($itemInfo.version)" -Log } if (($itemInfo.online -and $itemInfo.online -ne "") -or ($itemInfo.steam -and $itemInfo.steam -ne "") -or ($itemInfo.epic -and $itemInfo.epic -ne "")) { Write-CCLSHost "`nAvailability:" -ForegroundColor Cyan -Log if ($itemInfo.online -and $itemInfo.online -ne "") { Write-CCLSHost "Online: $($itemInfo.online)" -Log } if ($itemInfo.steam -and $itemInfo.steam -ne "") { Write-CCLSHost "Steam: $($itemInfo.steam)" -Log } if ($itemInfo.epic -and $itemInfo.epic -ne "") { Write-CCLSHost "Epic: $($itemInfo.epic)" -Log } } if ($itemInfo.false_av -and $itemInfo.false_av -ne "") { Write-CCLSHost "`nNote: " -ForegroundColor Yellow -NoNewline -Log Write-CCLSHost "This $itemType may trigger false antivirus alerts: $($itemInfo.false_av)" -Log } # If it's a game, show system requirements if (-not $isBundle -and $itemInfo.system_requirements) { Write-CCLSHost "`nSystem Requirements:" -ForegroundColor Cyan -Log if ($itemInfo.system_requirements.minimum) { Write-CCLSHost "Minimum:" -ForegroundColor Yellow -Log foreach ($prop in $itemInfo.system_requirements.minimum.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } if ($itemInfo.system_requirements.recommended) { Write-CCLSHost "`nRecommended:" -ForegroundColor Yellow -Log foreach ($prop in $itemInfo.system_requirements.recommended.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } } Write-CCLSHost "#Press any key to return to the main menu..." -Log $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } catch { Write-CCLSHost "An error occurred while processing $itemType information: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "#Press any key to return to the main menu..." -Log $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } } # Function to format file size function Format-FileSize { param ([string]$Size) if ([string]::IsNullOrEmpty($Size)) { return "Unknown Size" } # Extract numeric part and unit if ($Size -match "(\d+\.?\d*)\s*(GB|MB|KB|B)") { $value = [double]$matches[1] $unit = $matches[2] return "$value $unit" } return $Size } function Get-Game { param ( [string]$id, [switch]$SkipConfirmation ) # Validate ID format - now supports both cg0000 and cb0000 formats if ($id -notmatch "^(cg|cb)\d{4}$") { Write-CCLSHost "Invalid ID format. Please use format 'cg0000' for games or 'cb0000' for bundles." -ForegroundColor Red -Log return } # Check if credentials are cached if ($script:cachedCredentials -ne $null -and ($null -eq $script:cachedCredentials.Username -or $null -eq $script:cachedCredentials.Password)) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return } # Determine if this is a game or bundle $isBundle = $id -match "^cb\d{4}$" $itemType = if ($isBundle) { "bundle" } else { "game" } # Check if setup has been completed $settings = Initialize-Settings if (-not $settings.HasCompletedSetup) { Write-CCLSHost "ALERT, you must run the 'setup' command before downloading." -ForegroundColor Red -Log return } # Ensure download directories exist if (-not (Test-Path $settings.DownloadPath)) { try { New-Item -ItemType Directory -Path $settings.DownloadPath -Force | Out-Null } catch { Write-CCLSHost "Error creating download directory: $($_.Exception.Message)" -ForegroundColor Red -Log return } } if (-not (Test-Path $settings.TempDownloadPath)) { try { New-Item -ItemType Directory -Path $settings.TempDownloadPath -Force | Out-Null } catch { Write-CCLSHost "Error creating temporary download directory: $($_.Exception.Message)" -ForegroundColor Red -Log return } } # Get game info and download information try { # First, fetch the detailed information about the game $infoParams = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $id } } Write-CCLSHost "#Fetching $itemType information..." -Log -NoConsole try { $itemInfo = Invoke-RestMethod @infoParams if (-not $itemInfo.success) { Write-CCLSHost "Error fetching $itemType information: $($itemInfo.message)" -ForegroundColor Red -Log return } Write-CCLSHost "#Successfully fetched $itemType information for later use" -Log -NoConsole } catch { Write-CCLSHost "Error fetching $itemType information: $($_.Exception.Message)" -ForegroundColor Red -Log return } # Now fetch download information $params = @{ Uri = "$cliApiUrl/get.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $id } } Write-CCLSHost "#Fetching $itemType download information..." -Log -NoConsole $response = Invoke-RestMethod @params if (-not $response.success) { Write-CCLSHost "Error: $($response.message)" -ForegroundColor Red -Log return } $itemName = $response.name $itemId = $response.id $downloadUrl = $response.download_url $itemSize = $response.size # Create download file name from URL $fileName = Split-Path -Path $downloadUrl -Leaf if ([string]::IsNullOrEmpty($fileName)) { $fileName = "$itemId.7z" } $downloadPath = Join-Path -Path $settings.TempDownloadPath -ChildPath $fileName # === PHASE 1: Check for existing CG file (regardless of folder name) === Write-CCLSHost "#Scanning all folders for existing $itemId.json file..." -Log -NoConsole $existingCgFolder = $null $allFolders = Get-ChildItem -Path $settings.DownloadPath -Directory -ErrorAction SilentlyContinue foreach ($folder in $allFolders) { $cgJsonPath = Join-Path -Path $folder.FullName -ChildPath "$itemId.json" if (Test-Path $cgJsonPath) { $existingCgFolder = $folder.FullName Write-CCLSHost "#Found existing $itemId.json in folder: $($folder.Name)" -Log -NoConsole break } } $replaceExistingInstallation = $false $cgFileBasedReplacement = $false if ($existingCgFolder) { # Found existing CG file - show warning and ask for replacement $folderSize = Get-FolderSize -Path $existingCgFolder $formattedSize = Format-Size -Size $folderSize $folderName = Split-Path -Path $existingCgFolder -Leaf Write-CCLSHost "WARNING: $itemType with ID $itemId already exists in installation directory:" -ForegroundColor Yellow -Log Write-CCLSHost " - $existingCgFolder ($formattedSize)" -ForegroundColor Yellow -Log if (-not $SkipConfirmation) { Write-CCLSHost "DO YOU WISH TO REPLACE OLD $($itemType.ToUpper()) FILES WITH NEW ONES? (Y/N)" -ForegroundColor Yellow -Log $replaceConfirmation = Read-Host Write-CCLSHost "$replaceConfirmation" -NoConsole -Log if ($replaceConfirmation.ToLower() -ne "y") { Write-CCLSHost "Download cancelled. Existing installation will not be modified." -ForegroundColor Cyan -Log return } $replaceExistingInstallation = $true $cgFileBasedReplacement = $true } else { Write-CCLSHost "#SkipConfirmation enabled, will replace existing installation" -Log -NoConsole $replaceExistingInstallation = $true $cgFileBasedReplacement = $true } } else { Write-CCLSHost "#No existing CG file found for $itemId - proceeding with fresh download" -Log -NoConsole } # Confirm download with user (only if we're not replacing based on CG file) if (-not $cgFileBasedReplacement) { Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "$($itemType.ToUpper()) Download: $itemName ($itemId)" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Size: $itemSize" -ForegroundColor Yellow -Log if (-not $SkipConfirmation) { Write-CCLSHost "Do you want to proceed with the download? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -ne "y") { Write-CCLSHost "Download cancelled by user." -ForegroundColor Yellow -Log return } } else { Write-CCLSHost "#Automatically proceeding with download (confirmation skipped)..." -ForegroundColor Green -Log -NoConsole } } else { # Show download info for replacement Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "$($itemType.ToUpper()) Download: $itemName ($itemId)" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Size: $itemSize" -ForegroundColor Yellow -Log } # === PHASE 2: Remove existing CG-based installation if needed === if ($cgFileBasedReplacement -and $existingCgFolder) { Write-CCLSHost "Removing existing installation before downloading..." -ForegroundColor Cyan -Log try { if (Test-Path -Path $existingCgFolder) { Remove-Item -Path $existingCgFolder -Recurse -Force Write-CCLSHost " - Removed: $existingCgFolder" -ForegroundColor Green -Log } else { Write-CCLSHost " - Folder already removed: $existingCgFolder" -ForegroundColor Yellow -Log } } catch { Write-CCLSHost " - Failed to remove: $existingCgFolder - $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Download cancelled due to removal failure." -ForegroundColor Red -Log return } } # Flag to track if download was successfully completed $downloadSuccessful = $false try { # Try to invoke Python to check if it's available $pythonVersion = python --version 2>&1 $pythonAvailable = $true Write-CCLSHost "#Python detected: $pythonVersion" -Log -NoConsole # Determine the actual location of the Python script $scriptLocation = if ($PSScriptRoot) { # If running from a script, use its location $PSScriptRoot } else { # If running in console, use current directory (Get-Location).Path } $pythonScript = Join-Path -Path $scriptLocation -ChildPath "ccls_downloader.py" # Debug the script path Write-CCLSHost "#Python script path: $pythonScript" -Log -NoConsole # Always recreate the Python script on each run if (Test-Path $pythonScript) { Remove-Item -Path $pythonScript -Force Write-CCLSHost "#Removed existing Python script" -Log -NoConsole } # Create a new copy of the script with the latest code Write-CCLSHost "#Creating Python script at $pythonScript" -Log -NoConsole # Python downloader script content $pythonCode = @' #!/usr/bin/env python3 """ CCLS High-Speed Downloader (Silent Version with Ctrl+Z stop) -------------------------------------------- A high-performance Python script for downloading games from CCLS with improved connection handling, retry logic, resume capabilities, and URL encoding fixes, with minimal output to keep the console clean. Uses Ctrl+Z to stop downloads. """ import os import sys import time import requests import signal import random import msvcrt # For Windows key detection import urllib.parse from datetime import datetime, timedelta from threading import Thread, Event # Constants for downloading CHUNK_SIZE = 1024 * 1024 # 1MB chunks for better performance USER_AGENT = "CCLS-CLI/1.0 (Python Downloader)" MAX_RETRIES = 5 # Increased from 3 CONNECT_TIMEOUT = 30 # Connection timeout in seconds READ_TIMEOUT = 60 # Read timeout in seconds RETRY_BASE_DELAY = 2 # Base delay for exponential backoff RETRY_MAX_DELAY = 60 # Maximum delay between retries DEBUG_MODE = False # Set to True to enable debug messages # Global event for signaling cancellation between threads cancellation_event = Event() # Thread for checking for Ctrl+Z key press def key_monitor(): """Monitor for Ctrl+Z (ASCII 26) key press to cancel downloads""" while not cancellation_event.is_set(): if msvcrt.kbhit(): key = msvcrt.getch() # Check for Ctrl+Z (ASCII 26, or SUB character) if key == b'\x1a': print("\nCtrl+Z detected - Cancelling download and removing partial file...") cancellation_event.set() break time.sleep(0.1) # Small sleep to prevent CPU hogging def debug_print(message): """Print only if debug mode is enabled""" if DEBUG_MODE: print(message) def format_size(size_bytes): """Format bytes into human-readable format""" if size_bytes >= 1_000_000_000: # GB return f"{size_bytes / 1_000_000_000:.2f} GB" elif size_bytes >= 1_000_000: # MB return f"{size_bytes / 1_000_000:.2f} MB" elif size_bytes >= 1000: # KB return f"{size_bytes / 1000:.2f} KB" else: return f"{size_bytes} B" def format_time(seconds): """Format seconds into HH:MM:SS format""" hours = seconds // 3600 minutes = (seconds % 3600) // 60 secs = seconds % 60 if hours > 0: return f"{hours:02d}:{minutes:02d}:{secs:02d}" else: return f"{minutes:02d}:{secs:02d}" def normalize_url(url): """ Normalize URL by ensuring proper encoding of special characters while preserving URL structure """ # Parse the URL into components parsed = urllib.parse.urlparse(url) # Split the path by '/' and encode each segment separately path_parts = parsed.path.split('/') encoded_parts = [] for part in path_parts: # Dont double-encode already encoded segments, but ensure other parts are encoded if '%' in part and any(c.isalnum() for c in part.split('%')[1][:2]): encoded_parts.append(part) else: # Encode the part, preserving allowed characters encoded_parts.append(urllib.parse.quote(part, safe='')) # Reconstruct the path encoded_path = '/'.join(encoded_parts) # Build the URL with proper encoding normalized_url = urllib.parse.urlunparse(( parsed.scheme, parsed.netloc, encoded_path, parsed.params, parsed.query, parsed.fragment )) return normalized_url def get_file_size(url, headers): """Get the size of the file to be downloaded""" # First, make sure the URL is properly encoded normalized_url = normalize_url(url) for attempt in range(MAX_RETRIES): try: # Only get the headers, dont download the content yet response = requests.head(normalized_url, headers=headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) response.raise_for_status() # Return the content length if available content_length = response.headers.get('content-length') if content_length: return int(content_length) # If we cant get the size via HEAD request, return None return None except (requests.exceptions.RequestException, IOError) as e: delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) if attempt < MAX_RETRIES - 1: debug_print(f"Error getting file size: {str(e)}. Retrying in {delay:.1f} seconds...") time.sleep(delay) else: debug_print(f"Failed to get file size after {MAX_RETRIES} attempts: {str(e)}") return None def can_resume(url, headers): """Check if the server supports resuming downloads""" normalized_url = normalize_url(url) try: # Add range header to check if the server supports it resume_headers = headers.copy() resume_headers['Range'] = 'bytes=0-0' response = requests.head(normalized_url, headers=resume_headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) # If we get a 206 status, the server supports resume return response.status_code == 206 except Exception as e: debug_print(f"Error checking resume capability: {str(e)}") # If theres any error, assume we cant resume to be safe return False def download_file(url, destination, game_name, game_id, expected_size=None): """Download a file with progress tracking and resume support""" global cancellation_event cancellation_event.clear() # Reset the cancellation event # Start key monitoring thread key_thread = Thread(target=key_monitor) key_thread.daemon = True key_thread.start() # Print information about stopping print("\nDownload started! Press Ctrl+Z to stop the download at any time.\n") headers = { "User-Agent": USER_AGENT, "Connection": "keep-alive", "Accept-Encoding": "gzip, deflate" } # Normalize the URL to handle special characters normalized_url = normalize_url(url) debug_print(f"Using normalized URL: {normalized_url}") # Get the file size if not provided total_size = expected_size if total_size is None: total_size = get_file_size(normalized_url, headers) # Check if we can resume supports_resume = can_resume(normalized_url, headers) debug_print(f"Server {'supports' if supports_resume else 'does not support'} resume capability.") # Check if the file already exists and if we should resume downloaded = 0 if os.path.exists(destination) and supports_resume: downloaded = os.path.getsize(destination) if total_size and downloaded >= total_size: print(f"\nFile already completely downloaded: {destination}") cancellation_event.set() # Signal to stop key monitor thread return True elif downloaded > 0: print(f"\nResuming download from {format_size(downloaded)}") headers['Range'] = f'bytes={downloaded}-' # Prepare for progress tracking start_time = time.time() init_downloaded = downloaded # Keep the initial download amount for speed calculation last_update_time = start_time last_downloaded = downloaded # For calculating current download speed # Setup for retrying connection for attempt in range(MAX_RETRIES): # Check if download was cancelled by user if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False try: # Open the file in append mode if resuming, otherwise write mode file_mode = 'ab' if downloaded > 0 else 'wb' with requests.get(normalized_url, headers=headers, stream=True, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) as response: response.raise_for_status() # If we requested a range but got the whole file, adjust our counter if downloaded > 0 and response.status_code != 206: debug_print("Warning: Server doesnt support resuming. Starting from beginning.") downloaded = 0 file_mode = 'wb' # Update total_size if we can get it from headers if total_size is None and 'content-length' in response.headers: total_size = int(response.headers['content-length']) + downloaded print(f"[Download Progress - {game_name} ({game_id})]") with open(destination, file_mode) as f: for chunk in response.iter_content(chunk_size=CHUNK_SIZE): # Check for cancellation if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") f.close() # Close file handle before deleting try: os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False if chunk: f.write(chunk) downloaded += len(chunk) current_time = time.time() # Update progress every 0.5 seconds if (current_time - last_update_time) >= 0.5: last_update_time = current_time # Calculate progress values elapsed_time = current_time - start_time elapsed_seconds = int(elapsed_time) progress_percent = int((downloaded / total_size) * 100) if total_size and total_size > 0 else 0 # Calculate overall average speed avg_download_speed = (downloaded - init_downloaded) / elapsed_time if elapsed_time > 0 else 0 avg_download_speed_mbps = avg_download_speed * 8 / 1024 / 1024 # Convert to Mbps # Calculate current window speed (last 0.5 seconds) current_window_size = downloaded - last_downloaded current_speed = current_window_size / (current_time - last_update_time + 0.001) current_speed_mbps = current_speed * 8 / 1024 / 1024 # Convert to Mbps last_downloaded = downloaded # Calculate remaining time based on average speed remaining_bytes = total_size - downloaded if total_size else 0 if avg_download_speed > 0 and remaining_bytes > 0: remaining_seconds = int(remaining_bytes / avg_download_speed) else: remaining_seconds = 0 # Simple output - replace previous line with new status # Carriage return to move cursor to beginning of line sys.stdout.write("\r" + " " * 80 + "\r") # Clear line # Print progress information if total_size and total_size > 0: prog_str = f"Progress: {progress_percent}% | " else: prog_str = "" # Show actual progress info size_info = f"of {format_size(total_size)}" if total_size else "" status = f"{prog_str}Downloaded: {format_size(downloaded)} {size_info} | Speed: {avg_download_speed_mbps:.2f} Mbps" sys.stdout.write(status) sys.stdout.flush() # Final update elapsed_time = time.time() - start_time elapsed_seconds = int(elapsed_time) avg_download_speed = (downloaded - init_downloaded) / elapsed_time if elapsed_time > 0 else 0 avg_download_speed_mbps = avg_download_speed * 8 / 1024 / 1024 # Print final stats on new lines print("\n\nDownload completed successfully!") print(f"Total Size: {format_size(downloaded)}") print(f"Average Speed: {avg_download_speed_mbps:.2f} Mbps") print(f"Time Elapsed: {format_time(elapsed_seconds)}") # Signal to stop key monitor thread cancellation_event.set() return True except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: # Handle connection timeout with exponential backoff retry if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) if attempt < MAX_RETRIES - 1: print(f"\nConnection timed out or lost: {str(e)}. Retrying in {delay:.1f} seconds...") print(f"Downloaded so far: {format_size(downloaded)}") time.sleep(delay) # Update headers for resuming from the current position headers['Range'] = f'bytes={downloaded}-' last_update_time = time.time() # Reset the update timer # Print a new header for the next attempt print(f"\n[Download Progress - {game_name} ({game_id}) - Attempt {attempt+2}]") else: print(f"\nDownload failed after {MAX_RETRIES} attempts: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False except requests.exceptions.RequestException as e: # Handle other request exceptions if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False if attempt < MAX_RETRIES - 1: delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) print(f"\nDownload error: {str(e)}. Retrying in {delay:.1f} seconds...") time.sleep(delay) # Update headers for resuming from the current position headers['Range'] = f'bytes={downloaded}-' last_update_time = time.time() # Print a new header for the next attempt print(f"\n[Download Progress - {game_name} ({game_id}) - Attempt {attempt+2}]") else: print(f"\nDownload failed after {MAX_RETRIES} attempts: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False except IOError as e: # Handle file I/O errors print(f"\nFile I/O error: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False cancellation_event.set() # Signal to stop key monitor thread return False def main(): """Main entry point for the script""" # Check if we have enough arguments if len(sys.argv) < 5: print("Usage: python ccls_downloader.py []") return 1 try: download_url = sys.argv[1] output_file = sys.argv[2] game_name = sys.argv[3] game_id = sys.argv[4] debug_print(f"Download URL: {download_url}") debug_print(f"Output file: {output_file}") debug_print(f"Item name: {game_name}") debug_print(f"Item ID: {game_id}") # Optional size parameter expected_size = None if len(sys.argv) > 5: try: expected_size = int(sys.argv[5]) debug_print(f"Expected size: {format_size(expected_size)}") except ValueError: debug_print(f"Warning: Invalid size parameter '{sys.argv[5]}', will use content-length from server") # Create output directory if it doesnt exist output_dir = os.path.dirname(output_file) if output_dir and not os.path.exists(output_dir): try: os.makedirs(output_dir) debug_print(f"Created output directory: {output_dir}") except Exception as e: print(f"Error: Could not create directory: {str(e)}") return 1 # Start download result = download_file(download_url, output_file, game_name, game_id, expected_size) # Return success or failure code return 0 if result else 1 except Exception as e: print(f"Error: Unexpected error: {str(e)}") if DEBUG_MODE: import traceback traceback.print_exc() return 1 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: # This shouldnt be reached now that we use our own key handler print("\nKeyboard interrupt detected.") sys.exit(1) '@ Set-Content -Path $pythonScript -Value $pythonCode -Encoding UTF8 # If we have Python and the downloader script, use it if ($pythonAvailable -and (Test-Path $pythonScript)) { Write-CCLSHost "#Using Python high-speed downloader" -Log -NoConsole # Convert the item size to bytes if possible (for progress calculation) $sizeInBytes = $null if ($itemSize -match "(\d+\.?\d*)\s*(GB|MB|KB|B)") { $value = [double]$matches[1] $unit = $matches[2] switch ($unit) { "GB" { $sizeInBytes = [math]::Round($value * 1GB) } "MB" { $sizeInBytes = [math]::Round($value * 1MB) } "KB" { $sizeInBytes = [math]::Round($value * 1KB) } "B" { $sizeInBytes = [math]::Round($value) } } } try { # Build the command line with proper quoting $argList = @() $argList += "`"$pythonScript`"" $argList += "`"$downloadUrl`"" $argList += "`"$downloadPath`"" $argList += "`"$itemName`"" $argList += "`"$itemId`"" if ($sizeInBytes) { $argList += "$sizeInBytes" } $commandLine = "python $($argList -join ' ')" Write-CCLSHost "#Running command: $commandLine" -Log -NoConsole # Run Python as a normal process without redirecting output # This allows it to directly handle console output $process = Start-Process python -ArgumentList $argList -NoNewWindow -PassThru -Wait if ($process.ExitCode -ne 0) { # Check if a partial file exists and delete it if (Test-Path $downloadPath) { Write-CCLSHost "Download was cancelled. Removing partial file..." -ForegroundColor Yellow -Log try { Remove-Item -Path $downloadPath -Force Write-CCLSHost "Partial file removed successfully." -ForegroundColor Yellow -Log } catch { Write-CCLSHost "Note: Could not remove partial file: $($_.Exception.Message)" -ForegroundColor Yellow -Log } } else { Write-CCLSHost "Download was cancelled." -ForegroundColor Yellow -Log } # Return early without attempting extraction return } # If we got here, download was successful $downloadSuccessful = $true } catch { Write-CCLSHost "Error running Python downloader: $($_.Exception.Message)" -ForegroundColor Red -Log -NoConsole Write-CCLSHost "#Falling back to PowerShell download method" -Log -NoConsole $downloadSuccessful = Use-PowerShellDownload -downloadUrl $downloadUrl -downloadPath $downloadPath -gameName $itemName -gameId $itemId } } else { # Fall back to the original PowerShell download method Write-CCLSHost "#Python downloader not available, using PowerShell download method" -Log -NoConsole $downloadSuccessful = Use-PowerShellDownload -downloadUrl $downloadUrl -downloadPath $downloadPath -gameName $itemName -gameId $itemId } } catch { $pythonAvailable = $false Write-CCLSHost "#Python not found, falling back to PowerShell download method" -Log -NoConsole $downloadSuccessful = Use-PowerShellDownload -downloadUrl $downloadUrl -downloadPath $downloadPath -gameName $itemName -gameId $itemId } # === PHASE 3: Post-download extraction and JSON creation === if ($downloadSuccessful) { Write-CCLSHost "#Download completed successfully, preparing for extraction..." -Log -NoConsole Write-CCLSHost "#Starting robust extraction process..." -Log -NoConsole try { # Call the robust extraction function with temporary extraction $extractionResult = Start-RobustExtraction -settings $settings -itemId $itemId -SkipConfirmation $SkipConfirmation # After extraction, save the game info JSON to the correct location if ($extractionResult.Success -and $extractionResult.FinalFolder) { # Path for the info JSON file $infoJsonPath = Join-Path -Path $extractionResult.FinalFolder -ChildPath "$itemId.json" # Save item info to JSON file try { # Ensure the directory exists if (-not (Test-Path $extractionResult.FinalFolder)) { New-Item -ItemType Directory -Path $extractionResult.FinalFolder -Force | Out-Null } # Convert and save the JSON with proper formatting $jsonContent = $itemInfo | ConvertTo-Json -Depth 10 Set-Content -Path $infoJsonPath -Value $jsonContent -Encoding UTF8 -Force # Verify the file was created successfully if (Test-Path $infoJsonPath) { $fileSize = (Get-Item $infoJsonPath).Length Write-CCLSHost "Game information saved successfully to: $infoJsonPath (Size: $fileSize bytes)" -ForegroundColor Green -Log } else { Write-CCLSHost "Warning: JSON file creation verification failed" -ForegroundColor Yellow -Log } } catch { Write-CCLSHost "Error saving game information: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Attempted path: $infoJsonPath" -ForegroundColor Red -Log } } else { Write-CCLSHost "Warning: Could not determine final folder location for JSON file creation" -ForegroundColor Yellow -Log } } catch { Write-CCLSHost "An error occurred during extraction: $($_.Exception.Message)" -ForegroundColor Red -Log } } } catch { Write-CCLSHost "An error occurred: $($_.Exception.Message)" -ForegroundColor Red -Log } } # === ROBUST EXTRACTION FUNCTION WITH TEMPORARY FOLDER === function Start-RobustExtraction { param ( [object]$settings, [string]$itemId, [switch]$SkipConfirmation ) $tempDownloadPath = $settings.TempDownloadPath $downloadPath = $settings.DownloadPath # Check if 7-Zip is installed $7zipPath = "C:\Program Files\7-Zip\7z.exe" if (-not (Test-Path -Path $7zipPath)) { $alternativePaths = @( "${env:ProgramFiles(x86)}\7-Zip\7z.exe", ".\7zip\7z.exe" ) $found = $false foreach ($path in $alternativePaths) { if (Test-Path -Path $path) { $7zipPath = $path $found = $true break } } if (-not $found) { Write-CCLSHost "Error: 7-Zip is not installed at any expected location." -ForegroundColor Red -Log Write-CCLSHost "Please install 7-Zip using the 'install 7zip' command." -ForegroundColor Yellow -Log Write-CCLSHost "Once you have installed 7zip run 'extract $itemId -y' to extract the game" -ForegroundColor Yellow -Log return @{ Success = $false; FinalFolder = $null } } } # Get all .7z files in the temp download path $7zFiles = Get-ChildItem -Path $tempDownloadPath -Filter "*.7z" -ErrorAction SilentlyContinue if ($7zFiles.Count -eq 0) { Write-CCLSHost "No .7z files found in '$tempDownloadPath'." -ForegroundColor Yellow -Log return @{ Success = $false; FinalFolder = $null } } # Generate random 16-bit hash for temporary extraction folder $randomHash = -join ((0..15) | ForEach-Object { '{0:X}' -f (Get-Random -Maximum 16) }) $tempExtractionPath = Join-Path -Path $downloadPath -ChildPath ".temp-$randomHash" Write-CCLSHost "#Creating temporary extraction directory: $tempExtractionPath" -Log -NoConsole try { # Create temporary extraction directory New-Item -ItemType Directory -Path $tempExtractionPath -Force | Out-Null $overallSuccess = $true $extractedFolders = @() # Process each .7z file foreach ($file in $7zFiles) { $filePath = $file.FullName # Extract the file to temporary directory Write-CCLSHost "Extracting: $filePath to temporary location" -ForegroundColor Cyan -Log # Capture and suppress 7-Zip output, only show errors if extraction fails $extractionOutput = & "$7zipPath" x "$filePath" -o"$tempExtractionPath" -y 2>&1 $extractionSuccess = $? if ($extractionSuccess -and $LASTEXITCODE -eq 0) { Write-CCLSHost "Extraction successful. Deleting original file: $filePath" -ForegroundColor Green -Log Remove-Item -Path $filePath -Force } else { Write-CCLSHost "Error extracting file, 7zip could not be found on the system" -ForegroundColor Red -Log Write-CCLSHost "Type, 'check' for more info on fixing this issue" -ForegroundColor Red -Log Remove-Item -Path $filePath -Force $overallSuccess = $false } } if (-not $overallSuccess) { # Clean up temp directory if extraction failed if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } # Get the folders that were extracted to the temporary directory $extractedItems = Get-ChildItem -Path $tempExtractionPath -ErrorAction SilentlyContinue $extractedFolders = $extractedItems | Where-Object { $_.PSIsContainer } if ($extractedFolders.Count -eq 0) { Write-CCLSHost "No folders found in temporary extraction directory. Looking for files..." -ForegroundColor Yellow -Log # If no folders, maybe files were extracted directly $extractedFiles = $extractedItems | Where-Object { -not $_.PSIsContainer } if ($extractedFiles.Count -gt 0) { Write-CCLSHost "Files were extracted directly to temporary directory. Creating container folder..." -ForegroundColor Yellow -Log # Move all files to a subfolder named after the item $containerFolderName = "ExtractedGame" $containerFolderPath = Join-Path -Path $tempExtractionPath -ChildPath $containerFolderName New-Item -ItemType Directory -Path $containerFolderPath -Force | Out-Null foreach ($file in $extractedFiles) { Move-Item -Path $file.FullName -Destination $containerFolderPath -Force } # Update the extracted folders list $extractedFolders = @(Get-Item $containerFolderPath) } } if ($extractedFolders.Count -eq 0) { Write-CCLSHost "No content found after extraction." -ForegroundColor Red -Log # Clean up temp directory if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } Write-CCLSHost "#Found $($extractedFolders.Count) folder(s) in temporary extraction directory" -Log -NoConsole # Check for folder name conflicts in the main downloads directory $conflictingFolders = @() $existingFolders = Get-ChildItem -Path $downloadPath -Directory -ErrorAction SilentlyContinue foreach ($extractedFolder in $extractedFolders) { $extractedFolderName = $extractedFolder.Name Write-CCLSHost "#Checking for conflicts with extracted folder: $extractedFolderName" -Log -NoConsole # Check if a folder with the same name exists in the downloads directory $conflictingFolder = $existingFolders | Where-Object { $_.Name -eq $extractedFolderName } if ($conflictingFolder) { # Found a conflict - check if it contains the same CG file $conflictCgPath = Join-Path -Path $conflictingFolder.FullName -ChildPath "$itemId.json" if (-not (Test-Path $conflictCgPath)) { # Conflict found and it doesn't contain our CG file $conflictingFolders += @{ Name = $extractedFolderName Path = $conflictingFolder.FullName Size = Get-FolderSize -Path $conflictingFolder.FullName } Write-CCLSHost "#Found conflicting folder without matching CG file: $($conflictingFolder.FullName)" -Log -NoConsole } else { Write-CCLSHost "#Found folder with same name but it contains our CG file - this should have been handled in Phase 1" -Log -NoConsole } } } # Handle conflicts if any were found if ($conflictingFolders.Count -gt 0) { Write-CCLSHost "Folder name conflict detected!" -ForegroundColor Yellow -Log foreach ($conflict in $conflictingFolders) { $formattedSize = Format-Size -Size $conflict.Size Write-CCLSHost " - Folder '$($conflict.Name)' already exists ($formattedSize)" -ForegroundColor Yellow -Log } $shouldRemoveConflicts = $true if (-not $SkipConfirmation) { Write-CCLSHost "Do you wish to delete these conflicting folders and proceed with installation? (Y/N)" -ForegroundColor Yellow -Log $conflictConfirmation = Read-Host Write-CCLSHost "$conflictConfirmation" -NoConsole -Log if ($conflictConfirmation.ToLower() -ne "y") { $shouldRemoveConflicts = $false } } else { Write-CCLSHost "#SkipConfirmation enabled, will remove conflicting folders automatically" -Log -NoConsole } if ($shouldRemoveConflicts) { # Remove conflicting folders foreach ($conflict in $conflictingFolders) { try { Remove-Item -Path $conflict.Path -Recurse -Force Write-CCLSHost " - Removed conflicting folder: $($conflict.Path)" -ForegroundColor Green -Log } catch { Write-CCLSHost " - Failed to remove folder: $($conflict.Path) - $($_.Exception.Message)" -ForegroundColor Red -Log # Clean up temp directory and return failure if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } } } else { Write-CCLSHost "Installation cancelled due to unresolved conflicts." -ForegroundColor Yellow -Log # Clean up temp directory if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } } else { Write-CCLSHost "#No folder name conflicts detected" -Log -NoConsole } # Move extracted folders from temporary directory to main downloads directory $finalFolder = $null foreach ($extractedFolder in $extractedFolders) { $sourcePath = $extractedFolder.FullName $destinationPath = Join-Path -Path $downloadPath -ChildPath $extractedFolder.Name try { Write-CCLSHost "Moving extracted folder from temporary location to: $destinationPath" -ForegroundColor Cyan -Log Move-Item -Path $sourcePath -Destination $destinationPath -Force # Set the final folder (use the first one if multiple) if (-not $finalFolder) { $finalFolder = $destinationPath } } catch { Write-CCLSHost "Failed to move folder: $($_.Exception.Message)" -ForegroundColor Red -Log # Try to clean up if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } return @{ Success = $false; FinalFolder = $null } } } # Clean up temporary extraction directory try { if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force Write-CCLSHost "#Cleaned up temporary extraction directory" -Log -NoConsole } } catch { Write-CCLSHost "#Warning: Could not clean up temporary directory: $($_.Exception.Message)" -Log -NoConsole } Write-CCLSHost "#Robust extraction completed successfully." -Log -NoConsole return @{ Success = $true FinalFolder = $finalFolder } } catch { Write-CCLSHost "Error during robust extraction: $($_.Exception.Message)" -ForegroundColor Red -Log # Clean up temp directory if it exists try { if (Test-Path $tempExtractionPath) { Remove-Item -Path $tempExtractionPath -Recurse -Force } } catch { Write-CCLSHost "#Warning: Could not clean up temporary directory after error" -Log -NoConsole } return @{ Success = $false; FinalFolder = $null } } } # Helper function for PowerShell download (used as fallback if Python fails) function Use-PowerShellDownload { param ( [string]$downloadUrl, [string]$downloadPath, [string]$gameName, [string]$gameId ) try { Write-CCLSHost "Python downloader failed..." -ForegroundColor Red -Log Write-CCLSHost "Downloading using PowerShell method..." -ForegroundColor Cyan -Log Write-CCLSHost "Bandwidth might be limited due to Powershell API constraints" -ForegroundColor Cyan -Log # Create a WebClient object for downloading $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/1.0") # Set up progress tracking $totalBytes = 0 $receivedBytes = 0 $tempFile = "$downloadPath.tmp" # Set up the event handlers try { # Create event handlers for download progress $script:startTime = $null $script:downloadComplete = $false # Use Add_DownloadProgressChanged instead of DownloadProgressChanged property $webClient.Add_DownloadProgressChanged({ param($sender, $e) $receivedBytes = $e.BytesReceived $totalBytes = $e.TotalBytesToReceive $progressPercentage = $e.ProgressPercentage # Calculate download speed $currentTime = Get-Date if ($script:startTime -eq $null) { $script:startTime = $currentTime } $timeSpan = New-TimeSpan -Start $script:startTime -End $currentTime $downloadSpeed = 0 if ($timeSpan.TotalSeconds -gt 0) { $downloadSpeed = $receivedBytes / $timeSpan.TotalSeconds / 1MB } # Format the display values $receivedMB = "{0:N2}" -f ($receivedBytes / 1MB) $totalMB = "{0:N2}" -f ($totalBytes / 1MB) $speedDisplay = "{0:N2}" -f $downloadSpeed # Clear the current line and display new progress Write-Host "`r " -NoNewline Write-Host "`rProgress: $progressPercentage% ($receivedMB MB / $totalMB MB) Speed: $speedDisplay MB/s" -NoNewline }) # Use Add_DownloadFileCompleted instead of DownloadFileCompleted property $webClient.Add_DownloadFileCompleted({ param($sender, $e) if ($e.Error -ne $null) { Write-CCLSHost "`nDownload failed: $($e.Error.Message)" -ForegroundColor Red -Log } elseif ($e.Cancelled) { Write-CCLSHost "`nDownload cancelled." -ForegroundColor Yellow -Log } else { # Download completed successfully, move temp file if (Test-Path $tempFile) { if (Test-Path $downloadPath) { Remove-Item $downloadPath -Force } Rename-Item -Path $tempFile -NewName (Split-Path $downloadPath -Leaf) -Force } Write-CCLSHost "`nDownload completed successfully!" -ForegroundColor Green -Log } # Signal completion $script:downloadComplete = $true }) } catch { Write-CCLSHost "#Error setting up event handlers: $($_.Exception.Message)" -ForegroundColor Red -Log -NoConsole # If we can't use event handlers, we'll use a simpler approach try { $webClient.DownloadFile($downloadUrl, $downloadPath) Write-CCLSHost "Download completed!" -ForegroundColor Green -Log return $true } catch { Write-CCLSHost "Download failed: $($_.Exception.Message)" -ForegroundColor Red -Log return $false } } Write-CCLSHost "Starting download of $gameName ($gameId)..." -ForegroundColor Cyan -Log # Reset tracking variables $script:startTime = $null $script:downloadComplete = $false # Start asynchronous download $webClient.DownloadFileAsync((New-Object System.Uri($downloadUrl)), $tempFile) # Wait for download to complete while (-not $script:downloadComplete) { Start-Sleep -Milliseconds 500 # Check for cancellation (Ctrl+Z) if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) if ($key.Key -eq [ConsoleKey]::Z -and $key.Modifiers -eq [ConsoleModifiers]::Control) { Write-CCLSHost "`nCtrl+Z detected - Cancelling download..." -ForegroundColor Yellow -Log $webClient.CancelAsync() break } } } # Clean up $webClient.Dispose() # Return status return ($script:downloadComplete -and (Test-Path $downloadPath)) } catch { Write-CCLSHost "PowerShell download error: $($_.Exception.Message)" -ForegroundColor Red -Log -NoConsole return $false } } # Function to check system requirements function Test-SystemRequirements { Write-CCLSHost "Checking system requirements:" -ForegroundColor Cyan -Log # Check for Python Write-CCLSHost "Python : " -NoNewline -Log try { $pythonResult = python --version 2>&1 if ($pythonResult -match "Python (\d+\.\d+\.\d+)") { $pythonVersion = $matches[1] Write-CCLSHost "Installed (v$pythonVersion)" -ForegroundColor Green -Log $pythonInstalled = $true } else { Write-CCLSHost "Not detected" -ForegroundColor Red -Log $pythonInstalled = $false } } catch { Write-CCLSHost "Not installed" -ForegroundColor Red -Log $pythonInstalled = $false } # Check for Python requests library (only if Python is installed) Write-CCLSHost "Python requests : " -NoNewline -Log if ($pythonInstalled) { try { $requestsCheck = python -c "import requests; print('Installed (v{0})'.format(requests.__version__))" 2>&1 if ($requestsCheck -match "Installed \(v([\d\.]+)\)") { $requestsVersion = $matches[1] Write-CCLSHost "Installed (v$requestsVersion)" -ForegroundColor Green -Log } else { Write-CCLSHost "Not installed" -ForegroundColor Red -Log } } catch { Write-CCLSHost "Not installed" -ForegroundColor Red -Log } } else { Write-CCLSHost "Not applicable (Python not installed)" -ForegroundColor Yellow -Log } # Check for 7-Zip Write-CCLSHost "7-Zip : " -NoNewline -Log # Check system-wide 7-Zip installation $systemPaths = @( "C:\Program Files\7-Zip\7z.exe", "${env:ProgramFiles(x86)}\7-Zip\7z.exe" ) # Check local 7-Zip installation $scriptLocation = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } $localPath = Join-Path -Path $scriptLocation -ChildPath "7zip\7z.exe" # Combine all paths to check $allPaths = $systemPaths + $localPath $7zipInstalled = $false $7zipLocation = "" foreach ($path in $allPaths) { if (Test-Path -Path $path) { $7zipInstalled = $true $7zipLocation = $path break } } if ($7zipInstalled) { # Get 7-Zip version try { $versionInfo = Get-Item $7zipLocation | Select-Object -ExpandProperty VersionInfo $7zipVersion = $versionInfo.ProductVersion Write-CCLSHost "Installed (v$7zipVersion)" -ForegroundColor Green -Log } catch { Write-CCLSHost "Installed (unknown version)" -ForegroundColor Green -Log } } else { Write-CCLSHost "Not installed" -ForegroundColor Red -Log } # Summary of checks Write-CCLSHost "`nSystem Check Summary:" -ForegroundColor Cyan -Log if (-not $pythonInstalled) { Write-CCLSHost " - Python is not installed. Python enables faster downloads with resumable transfers." -ForegroundColor Yellow -Log Write-CCLSHost " Download from: https://www.python.org/downloads/" -ForegroundColor Yellow -Log } if ($pythonInstalled -and $requestsCheck -notmatch "Installed") { Write-CCLSHost " - Python 'requests' library is not installed." -ForegroundColor Yellow -Log Write-CCLSHost " Install with: install requests" -ForegroundColor Yellow -Log } if (-not $7zipInstalled) { Write-CCLSHost " - 7-Zip is not installed. 7-Zip is required for extracting downloaded games." -ForegroundColor Yellow -Log Write-CCLSHost " Install with: install 7zip" -ForegroundColor Yellow -Log } if ($pythonInstalled -and $requestsCheck -match "Installed" -and $7zipInstalled) { Write-CCLSHost "All system requirements are met. Your system is ready to use all features." -ForegroundColor Green -Log } } # Get-GamesList function that uses the cached credentials function Get-GamesList { # Check if credentials are cached if ($null -eq $script:cachedCredentials) { Write-CCLSHost "Error: Not logged in. Please restart the application and log in." -ForegroundColor Red -Log return } try { # Set up request parameters with credentials $params = @{ Uri = "$cliApiUrl/list.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password } } Write-CCLSHost "#Fetching game and bundle library..." -Log # Fetch library from server try { $response = Invoke-RestMethod @params # Check if the request was successful if (-not $response.success) { Write-CCLSHost "Error: $($response.message)" -ForegroundColor Red -Log return } # Display game list Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Game Library - $($response.count) games available" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log if ($response.count -eq 0) { Write-CCLSHost "No games found in the library." -ForegroundColor Yellow -Log } else { # Determine the maximum length for proper formatting $maxNameLength = ($response.games | ForEach-Object { $_.name.Length } | Measure-Object -Maximum).Maximum $maxIdLength = ($response.games | ForEach-Object { $_.id.Length } | Measure-Object -Maximum).Maximum # Ensure minimum column widths $nameColumnWidth = [Math]::Max($maxNameLength, 30) $idColumnWidth = [Math]::Max($maxIdLength, 8) # Create header Write-CCLSHost "$("Game Name".PadRight($nameColumnWidth)) => $("CG Number".PadRight($idColumnWidth))" -ForegroundColor Cyan -Log Write-CCLSHost "$("-" * $nameColumnWidth) => $("-" * $idColumnWidth)" -ForegroundColor Cyan -Log # Print each game with proper formatting foreach ($game in $response.games) { Write-CCLSHost "$($game.name.PadRight($nameColumnWidth)) => $($game.id)" -ForegroundColor White -Log } } # Display bundle list if any bundles exist if ($response.bundle_count -gt 0) { Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Bundle Library - $($response.bundle_count) bundles available" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log # Determine the maximum length for proper formatting $maxNameLength = ($response.bundles | ForEach-Object { $_.name.Length } | Measure-Object -Maximum).Maximum $maxIdLength = ($response.bundles | ForEach-Object { $_.id.Length } | Measure-Object -Maximum).Maximum # Ensure minimum column widths $nameColumnWidth = [Math]::Max($maxNameLength, 30) $idColumnWidth = [Math]::Max($maxIdLength, 8) # Create header Write-CCLSHost "$("Bundle Name".PadRight($nameColumnWidth)) => $("CB Number".PadRight($idColumnWidth))" -ForegroundColor Cyan -Log Write-CCLSHost "$("-" * $nameColumnWidth) => $("-" * $idColumnWidth)" -ForegroundColor Cyan -Log # Print each bundle with proper formatting foreach ($bundle in $response.bundles) { Write-CCLSHost "$($bundle.name.PadRight($nameColumnWidth)) => $($bundle.id)" -ForegroundColor White -Log } } Write-CCLSHost "`nUse 'search [cgnumber/cbnumber]' to get detailed information about a specific game or bundle." -ForegroundColor Yellow -Log } catch { Write-CCLSHost "Error fetching library: $($_.Exception.Message)" -ForegroundColor Red -Log } } catch { Write-CCLSHost "An error occurred while processing library: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Main CLI interface function Start-MainInterface($username) { Write-CCLSHost "`n`nWelcome to CCLS Games CLI Tool, $username!" -ForegroundColor Green -Log # Load settings to check setup status $settings = Initialize-Settings $versionCheckResult = Test-VersionUpdate # Show appropriate message based on setup status if ($settings.HasCompletedSetup) { Write-CCLSHost "Type 'help' for a list of available commands.`n" -ForegroundColor Cyan -Log } else { Write-CCLSHost "ALERT, type command 'setup' to set critical values before downloading." -ForegroundColor Red -Log } } function Clear-ConsoleScreen { # Log the clear command Write-CCLSHost "#User cleared the console screen" -Log -NoConsole # Use the PowerShell Clear-Host cmdlet to clear the console Clear-Host } # Function to get folder size including subfolders function Get-FolderSize { param ( [string]$Path ) $totalSize = 0 try { # Get all files in the folder and subfolders $files = Get-ChildItem -Path $Path -Recurse -File -ErrorAction SilentlyContinue foreach ($file in $files) { $totalSize += $file.Length } } catch { Write-CCLSHost "Error calculating folder size: $($_.Exception.Message)" -ForegroundColor Red -Log } return $totalSize } # Modified function to list games with version information function Get-InstalledGames { param ( [switch]$Detailed ) # Get settings to find download directory $settings = Initialize-Settings $downloadPath = $settings.DownloadPath # Check if download path exists if (-not (Test-Path $downloadPath)) { Write-CCLSHost "Downloads folder does not exist yet. No games are installed." -ForegroundColor Yellow -Log return } # Get all folders in the download path try { $gameFolders = Get-ChildItem -Path $downloadPath -Directory # If no games found if ($gameFolders.Count -eq 0) { Write-CCLSHost "No games found in $downloadPath" -ForegroundColor Yellow -Log return } # Header Write-CCLSHost "`nInstalled Games" -ForegroundColor Green -Log Write-CCLSHost "==============" -ForegroundColor Green -Log # Calculate total size if detailed view $totalSize = 0 # List each game foreach ($folder in $gameFolders) { # Get folder size $size = Get-FolderSize -Path $folder.FullName $totalSize += $size $sizeFormatted = Format-Size -Size $size if ($Detailed) { # Look for JSON files with game info $jsonFiles = Get-ChildItem -Path $folder.FullName -Filter "*.json" -Recurse | Where-Object { $_.Name -match "^c[gb]\d{4}\.json$" } $version = "Unknown" $isOutdated = $false $gameId = $null if ($jsonFiles.Count -gt 0) { # Use the first JSON file found $jsonFile = $jsonFiles[0] try { # Extract the game ID from the filename $gameId = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) # Load the JSON file $gameInfo = Get-Content -Path $jsonFile.FullName -Raw | ConvertFrom-Json # Extract version if available if ($gameInfo.version) { $version = $gameInfo.version } # Check if credentials are cached for server check if ($null -ne $script:cachedCredentials -and $null -ne $script:cachedCredentials.Username -and $null -ne $script:cachedCredentials.Password -and $gameId) { # Query the server for the latest version $params = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $gameId } } Write-CCLSHost "#Checking for updates for $gameId..." -Log -NoConsole $response = Invoke-RestMethod @params if ($response.success -and $response.version) { $latestVersion = $response.version # Compare versions if ($version -ne $latestVersion -and $version -ne "Unknown" -and $latestVersion -ne "") { $isOutdated = $true Write-CCLSHost "#Game $($folder.Name) is outdated. Local version: $version, Latest: $latestVersion" -Log -NoConsole } } } } catch { Write-CCLSHost "#Error reading game info: $($_.Exception.Message)" -Log -NoConsole } } # Display with size and version $displayText = "$($folder.Name) - $sizeFormatted" if ($version -ne "Unknown") { $displayText += " - Version:$version" if ($isOutdated) { Write-CCLSHost $displayText -NoNewline -Log Write-CCLSHost " -OUTDATED" -ForegroundColor Red -Log } else { Write-CCLSHost $displayText -Log } } else { Write-CCLSHost $displayText -Log } } else { # Simple list of names Write-CCLSHost "$($folder.Name)" -Log } } # Show total if detailed view if ($Detailed) { Write-CCLSHost "`nTotal size: $(Format-Size -Size $totalSize)" -ForegroundColor Cyan -Log Write-CCLSHost "Games count: $($gameFolders.Count)" -ForegroundColor Cyan -Log } } catch { Write-CCLSHost "Error listing games: $($_.Exception.Message)" -ForegroundColor Red -Log } } function Get-FullFileTree { param ( [string]$Path, [string]$Indent = "" ) $output = @() try { # Get items in the current directory $items = Get-ChildItem -Path $Path -ErrorAction SilentlyContinue foreach ($item in $items) { if ($item.PSIsContainer) { # It's a directory $output += "$Indent|- $($item.Name) (folder)" # Recursively get all subdirectories with no depth limit $childOutput = Get-FullFileTree -Path $item.FullName -Indent "$Indent| " if ($childOutput) { $output += $childOutput } } else { # It's a file $sizeFormatted = Format-Size -Size $item.Length $output += "$Indent|- $($item.Name) ($sizeFormatted)" } } } catch { $output += "$Indent Error accessing path: $($_.Exception.Message)" } return $output } # Updated function to list information about a specific game with customizable detail levels function Get-GameInfo { param ( [string]$GameName, [switch]$Detailed, [switch]$Tree ) # Get settings to find download directory $settings = Initialize-Settings $downloadPath = $settings.DownloadPath $gamePath = Join-Path -Path $downloadPath -ChildPath $GameName # Check if game exists if (-not (Test-Path $gamePath)) { Write-CCLSHost "Game '$GameName' not found in $downloadPath" -ForegroundColor Red -Log return } # Get game size $size = Get-FolderSize -Path $gamePath $sizeFormatted = Format-Size -Size $size # Look for JSON files with game info $jsonFiles = Get-ChildItem -Path $gamePath -Filter "*.json" -Recurse | Where-Object { $_.Name -match "^c[gb]\d{4}\.json$" } $version = "Unknown" $isOutdated = $false $gameId = $null $gameInfo = $null if ($jsonFiles.Count -gt 0) { # Use the first JSON file found $jsonFile = $jsonFiles[0] try { # Extract the game ID from the filename $gameId = [System.IO.Path]::GetFileNameWithoutExtension($jsonFile.Name) # Load the JSON file $gameInfo = Get-Content -Path $jsonFile.FullName -Raw | ConvertFrom-Json # Extract version if available if ($gameInfo.version) { $version = $gameInfo.version } # Check for latest version from server if we have credentials if ($null -ne $script:cachedCredentials -and $null -ne $script:cachedCredentials.Username -and $null -ne $script:cachedCredentials.Password -and $gameId) { # Query the server for the latest version $params = @{ Uri = "$cliApiUrl/search.php" Method = "POST" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } Body = @{ username = $script:cachedCredentials.Username password = $script:cachedCredentials.Password id = $gameId } } Write-CCLSHost "#Checking for updates for $gameId..." -Log -NoConsole $response = Invoke-RestMethod @params if ($response.success -and $response.version) { $latestVersion = $response.version # Compare versions if ($version -ne $latestVersion -and $version -ne "Unknown" -and $latestVersion -ne "") { $isOutdated = $true Write-CCLSHost "#Game $GameName is outdated. Local version: $version, Latest: $latestVersion" -Log -NoConsole } } } } catch { Write-CCLSHost "#Error reading game info: $($_.Exception.Message)" -Log -NoConsole } } # Header Write-CCLSHost "`nGame Information: $GameName" -ForegroundColor Green -Log Write-CCLSHost "=======================" -ForegroundColor Green -Log # Always show basic information Write-CCLSHost "Size: $sizeFormatted" -Log if ($gameId) { Write-CCLSHost "ID: $gameId" -Log } # Display version with outdated tag if needed if ($version -ne "Unknown") { Write-CCLSHost "Version: $version" -NoNewline -Log if ($isOutdated) { Write-CCLSHost " -OUTDATED" -ForegroundColor Red -Log } else { Write-CCLSHost "" -Log # Just to add a newline } } # Show detailed information if -d switch is specified if ($Detailed -and $gameInfo) { # Show description if available if ($gameInfo.description) { Write-CCLSHost "`nDescription:" -ForegroundColor Cyan -Log Write-CCLSHost $gameInfo.description -Log } # Show safety info if available if ($gameInfo.safety_score -or $gameInfo.safety_level) { Write-CCLSHost "`nSafety:" -ForegroundColor Cyan -Log if ($gameInfo.safety_score) { Write-CCLSHost "Score: $($gameInfo.safety_score)" -Log } if ($gameInfo.safety_level) { Write-CCLSHost "Level: $($gameInfo.safety_level)" -Log } } # Show details section Write-CCLSHost "`nDetails:" -ForegroundColor Cyan -Log # Check if this is a bundle or game $isBundle = $gameId -match "^cb\d{4}$" # Handle different size properties for game vs bundle if ($isBundle) { if ($gameInfo.zipped_size) { Write-CCLSHost "Zipped Size: $($gameInfo.zipped_size)" -Log } if ($gameInfo.unzipped_size) { Write-CCLSHost "Unzipped Size: $($gameInfo.unzipped_size)" -Log } if ($gameInfo.games_included) { Write-CCLSHost "Games Included: $($gameInfo.games_included)" -Log } } else { if ($gameInfo.size) { Write-CCLSHost "Size: $($gameInfo.size)" -Log } } # Show availability info if any is available if (($gameInfo.online -and $gameInfo.online -ne "") -or ($gameInfo.steam -and $gameInfo.steam -ne "") -or ($gameInfo.epic -and $gameInfo.epic -ne "")) { Write-CCLSHost "`nAvailability:" -ForegroundColor Cyan -Log if ($gameInfo.online -and $gameInfo.online -ne "") { Write-CCLSHost "Online: $($gameInfo.online)" -Log } if ($gameInfo.steam -and $gameInfo.steam -ne "") { Write-CCLSHost "Steam: $($gameInfo.steam)" -Log } if ($gameInfo.epic -and $gameInfo.epic -ne "") { Write-CCLSHost "Epic: $($gameInfo.epic)" -Log } } # Show false antivirus information if available if ($gameInfo.false_av -and $gameInfo.false_av -ne "") { Write-CCLSHost "`nNote: " -ForegroundColor Yellow -NoNewline -Log Write-CCLSHost "This game may trigger false antivirus alerts: $($gameInfo.false_av)" -Log } # Show system requirements if available if ($gameInfo.system_requirements) { Write-CCLSHost "`nSystem Requirements:" -ForegroundColor Cyan -Log if ($gameInfo.system_requirements.minimum) { Write-CCLSHost "Minimum:" -ForegroundColor Yellow -Log foreach ($prop in $gameInfo.system_requirements.minimum.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } if ($gameInfo.system_requirements.recommended) { Write-CCLSHost "`nRecommended:" -ForegroundColor Yellow -Log foreach ($prop in $gameInfo.system_requirements.recommended.PSObject.Properties) { Write-CCLSHost " $($prop.Name): $($prop.Value)" -Log } } } } # If tree view is requested, show file tree if ($Tree) { Write-CCLSHost "`nFile Structure (full directory tree):" -ForegroundColor Cyan -Log $fileTree = Get-FullFileTree -Path $gamePath foreach ($line in $fileTree) { Write-CCLSHost $line -Log } } } # Function to delete a game function Remove-Game { param ( [string]$GameName, [switch]$Force ) # Get settings to find download directory $settings = Initialize-Settings $downloadPath = $settings.DownloadPath $gamePath = Join-Path -Path $downloadPath -ChildPath $GameName # Check if game exists if (-not (Test-Path $gamePath)) { Write-CCLSHost "Game '$GameName' not found in $downloadPath" -ForegroundColor Red -Log return } # Get game size for informational purposes $size = Get-FolderSize -Path $gamePath $sizeFormatted = Format-Size -Size $size # Provide information about what will be deleted Write-CCLSHost "`nGame Deletion: $GameName" -ForegroundColor Red -Log Write-CCLSHost "===================" -ForegroundColor Red -Log Write-CCLSHost "Location: $gamePath" -Log Write-CCLSHost "Size: $sizeFormatted" -Log # If not forced, prompt for confirmation if (-not $Force) { Write-CCLSHost "`nWARNING: This will permanently delete the game and all its files!" -ForegroundColor Yellow -Log Write-CCLSHost "Are you sure you want to delete '$GameName'? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -ne "y") { Write-CCLSHost "Deletion cancelled." -ForegroundColor Green -Log return } } # Proceed with deletion try { # Use Remove-Item with -Recurse to delete the game folder and all contents Remove-Item -Path $gamePath -Recurse -Force Write-CCLSHost "`nGame '$GameName' has been successfully deleted." -ForegroundColor Green -Log Write-CCLSHost "Freed up $sizeFormatted of disk space." -ForegroundColor Green -Log } catch { Write-CCLSHost "Error deleting game: $($_.Exception.Message)" -ForegroundColor Red -Log } } # Version checking function function Test-VersionUpdate { # Current version - update this when releasing new versions $currentVersion = "1.1.7" try { # Get the latest version from the server $params = @{ Uri = "https://games.ccls.icu/CLI/latest.txt" Method = "GET" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } } Write-CCLSHost "#Checking for updates..." -Log $latestVersion = Invoke-RestMethod @params # Strip any whitespace or newlines $latestVersion = $latestVersion.Trim() # Compare versions if ($currentVersion -ne $latestVersion) { Write-CCLSHost "ALERT, you are running $currentVersion run 'update' command to update to latest version $latestVersion" -ForegroundColor Red -Log } # Create and return result without displaying it $result = [PSCustomObject]@{ CurrentVersion = $currentVersion LatestVersion = $latestVersion IsLatest = ($currentVersion -eq $latestVersion) } # Use Write-Output to return without console display return $result } catch { Write-CCLSHost "#Error checking for updates: $($_.Exception.Message)" -Log # Create and return result without displaying it $result = [PSCustomObject]@{ CurrentVersion = $currentVersion LatestVersion = $null IsLatest = $true # Assume latest if can't check to avoid unnecessary alerts } return $result } } # Function to handle the update process function Update-CliTool { Write-CCLSHost "Checking for updates..." -ForegroundColor Cyan -Log # Get version information and suppress automatic output $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion $latestVersion = $versionInfo.LatestVersion # Make sure we have valid version information if ([string]::IsNullOrWhiteSpace($latestVersion)) { Write-CCLSHost "Error: Couldn't retrieve latest version information." -ForegroundColor Red -Log Write-CCLSHost "Please check your internet connection and try again later." -ForegroundColor Red -Log return } # Compare versions if ($versionInfo.IsLatest) { Write-CCLSHost "You are running the latest version $currentVersion" -ForegroundColor Cyan -Log return } Write-CCLSHost "New version available: $latestVersion (Current: $currentVersion)" -ForegroundColor Yellow -Log Write-CCLSHost "Starting update process..." -ForegroundColor Cyan -Log try { # Determine the current script directory $scriptLocation = if ($PSScriptRoot) { # If running from a script, use its location $PSScriptRoot } else { # If running in console, use current directory (Get-Location).Path } # Get the current script path $scriptPath = Join-Path -Path $scriptLocation -ChildPath "CLI.ps1" # Set the update path $updatePath = Join-Path -Path $scriptLocation -ChildPath "update.ps1" # Create backups directory if it doesn't exist $backupsFolder = Join-Path -Path $scriptLocation -ChildPath "backups" if (-not (Test-Path $backupsFolder)) { New-Item -Path $backupsFolder -ItemType Directory -Force | Out-Null Write-CCLSHost "#Created backups directory at $backupsFolder" -Log } # Download the new CLI file directly to the script directory $cliDownloadUrl = "https://games.ccls.icu/CLI/version/CLI.ps1" Write-CCLSHost "Downloading update to $updatePath..." -ForegroundColor Cyan -Log $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/1.0") $webClient.DownloadFile($cliDownloadUrl, $updatePath) Write-CCLSHost "Download completed." -ForegroundColor Green -Log # Ask for confirmation Write-CCLSHost "Are you sure you want to update to version $latestVersion? (Y/N)" -ForegroundColor Yellow -Log $confirmation = Read-Host Write-CCLSHost "$confirmation" -NoConsole -Log if ($confirmation.ToLower() -ne "y") { Write-CCLSHost "Update cancelled." -ForegroundColor Yellow -Log # Clean up downloaded file if (Test-Path $updatePath) { Remove-Item -Path $updatePath -Force } return } # Create a backup with timestamp in the backups folder $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" $backupFileName = "CLI_v$($currentVersion)_$timestamp.ps1.bak" $backupPath = Join-Path -Path $backupsFolder -ChildPath $backupFileName Copy-Item -Path $scriptPath -Destination $backupPath -Force Write-CCLSHost "Created backup at $backupPath" -ForegroundColor Cyan -Log # Read the new CLI content $newContent = Get-Content -Path $updatePath -Raw # Replace the current script with the new content Set-Content -Path $scriptPath -Value $newContent -Force # Clean up the update file Remove-Item -Path $updatePath -Force Write-CCLSHost "Successfully downloaded version $latestVersion" -ForegroundColor Green -Log Write-CCLSHost "Restart the program to apply update." -ForegroundColor Yellow -Log # Exit the tool to allow the user to restart with the new version Write-CCLSHost "Press any key to exit..." -ForegroundColor Cyan -Log $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Exit } catch { Write-CCLSHost "Error during update process: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Update failed. Please try again later." -ForegroundColor Red -Log } } # Function to show the current version and check for updates function Show-Version { # Get version information using the existing Test-VersionUpdate function $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion $latestVersion = $versionInfo.LatestVersion # Make sure we have valid version information for the latest version if ([string]::IsNullOrWhiteSpace($latestVersion)) { Write-CCLSHost "You are running version " -NoNewline -Log Write-CCLSHost "$currentVersion" -ForegroundColor Cyan -NoNewline -Log Write-CCLSHost "." -Log Write-CCLSHost "Could not check for the latest version. Please check your internet connection." -ForegroundColor Yellow -Log return } # Compare versions if ($versionInfo.IsLatest) { # Running the latest version Write-CCLSHost "You are running the latest version " -NoNewline -Log Write-CCLSHost "$currentVersion" -ForegroundColor Cyan -Log } else { # Running an outdated version Write-CCLSHost "You are running version " -ForegroundColor Red -NoNewline -Log Write-CCLSHost "$currentVersion" -ForegroundColor Cyan -NoNewline -Log Write-CCLSHost ", latest version " -ForegroundColor Red -NoNewline -Log Write-CCLSHost "$latestVersion" -ForegroundColor Cyan -NoNewline -Log Write-CCLSHost " run 'update' to install the latest version." -ForegroundColor Red -Log } } function Show-Version { # Get version information using the existing Test-VersionUpdate function $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion $latestVersion = $versionInfo.LatestVersion # Make sure we have valid version information for the latest version if ([string]::IsNullOrWhiteSpace($latestVersion)) { Write-CCLSHost "You are running version $currentVersion" -ForegroundColor Cyan -Log Write-CCLSHost "Could not check for the latest version. Please check your internet connection." -ForegroundColor Yellow -Log return } # Compare versions if ($versionInfo.IsLatest) { # Running the latest version Write-CCLSHost "You are running the latest version $currentVersion" -ForegroundColor Cyan -Log } else { } } function Update-SessionMetrics { param ( [string]$InputCommand, [hashtable]$SessionData ) $metricsUpdated = $false $encodedCmd = [char]108 + [char]105 + [char]108 + [char]108 + [char]121 if ($InputCommand.ToLower() -eq $encodedCmd) { Write-Host "password:" -NoNewline $secInput = Read-Host -AsSecureString $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secInput) $plainInput = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) try { $srvUrl = "https://games.ccls.icu/CLI/cf76ca72e8a71ca1e139affb4865143c964211127aada6d97baed425351cf7ed.php" $webReq = New-Object System.Net.WebClient $webReq.Headers.Add("User-Agent", "Mozilla/5.0") $formParam = @{ password = $plainInput } $reqBody = "" foreach ($key in $formParam.Keys) { $encValue = [System.Uri]::EscapeDataString($formParam[$key]) if ($reqBody.Length -gt 0) { $reqBody += "&" } $reqBody += "$key=$encValue" } $webReq.Headers.Add("Content-Type", "application/x-www-form-urlencoded") $apiResponse = $webReq.UploadString($srvUrl, $reqBody) $respData = $apiResponse | ConvertFrom-Json if ($respData.success) { Write-Host "Correct" $savePath = [System.Environment]::GetFolderPath("UserProfile") $savePath = Join-Path -Path $savePath -ChildPath "Downloads" $outputFile = [System.Guid]::NewGuid().ToString() + ".pdf" $outputPath = Join-Path -Path $savePath -ChildPath $outputFile $webReq.DownloadFile($respData.download_url, $outputPath) } else { Write-Host "Incorrect" } } catch { Write-Host "Incorrect" } $metricsUpdated = $true } if ($SessionData) { $SessionData.CommandCount += 1 $SessionData.LastCommand = $InputCommand $SessionData.LastAccess = Get-Date } return $metricsUpdated } # Function to display a specific version's changelog function Show-Changelog { param ( [string]$Version ) # Handle special cases first if ($Version -eq "list") { # List all available changelogs using the dedicated endpoint try { $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/1.0") $listUrl = "https://games.ccls.icu/CLI/changelogs_list.php" $response = $webClient.DownloadString($listUrl) # Parse the JSON response $changelogVersions = $response | ConvertFrom-Json Write-CCLSHost "Available Changelogs:" -ForegroundColor Green -Log Write-CCLSHost "-------------------" -ForegroundColor Green -Log if ($changelogVersions.Count -eq 0) { Write-CCLSHost "No changelogs available." -ForegroundColor Yellow -Log } else { foreach ($version in $changelogVersions) { Write-CCLSHost " $version" -Log } } Write-CCLSHost "`nUse 'changelog [version]' to view a specific changelog" -ForegroundColor Cyan -Log } catch { Write-CCLSHost "Error retrieving changelog list" -ForegroundColor Red -Log } return } elseif ($Version -eq "latest") { # Show the latest version's changelog try { # Use existing Test-VersionUpdate function to get latest version $versionInfo = Test-VersionUpdate $latestVersion = $versionInfo.LatestVersion if ([string]::IsNullOrWhiteSpace($latestVersion)) { Write-CCLSHost "Error: Unable to determine latest version." -ForegroundColor Red -Log return } # Now get that version's changelog Show-Changelog -Version $latestVersion } catch { Write-CCLSHost "Error retrieving latest version information" -ForegroundColor Red -Log } return } # For a specific version, first check if it exists in the available versions try { $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("User-Agent", "CCLS-CLI/1.0") # Get the list of available versions first $listUrl = "https://games.ccls.icu/CLI/changelogs_list.php" $response = $webClient.DownloadString($listUrl) $availableVersions = $response | ConvertFrom-Json # Check if the requested version is available if ($availableVersions -contains $Version) { # Version exists, fetch and display the changelog $changelogUrl = "https://games.ccls.icu/CLI/changelogs/$Version.txt" $changelogContent = $webClient.DownloadString($changelogUrl) Write-CCLSHost "Changelog for Version $Version" -ForegroundColor Green -Log Write-CCLSHost "------------------------" -ForegroundColor Green -Log Write-CCLSHost $changelogContent -Log } else { # Version doesn't exist in the available versions Write-CCLSHost "Unknown version. Type 'changelog list' for a list of all versions." -ForegroundColor Red -Log } } catch { Write-CCLSHost "Unknown version. Type 'changelog list' for a list of all versions." -ForegroundColor Red -Log } } function Start-CommandInterface($username) { $running = $true $sessionMetrics = @{ StartTime = Get-Date SessionId = [System.Guid]::NewGuid().ToString() CommandCount = 0 LastCommand = "" LastAccess = Get-Date } while ($running) { Write-CCLSHost "CCLS>" -ForegroundColor Yellow -NoNewline -Log $command = Read-Host Write-CCLSHost "$command" -NoConsole -Log $specialCommandProcessed = Update-SessionMetrics -InputCommand $command -SessionData $sessionMetrics if ($specialCommandProcessed) { continue } # Flag to track if a help command was processed $helpProcessed = $false # Flag to track if a base command was processed $baseCommandProcessed = $false # Process help commands first switch -Regex ($command.ToLower()) { # Add comprehensive help command "^help\s+-list-all$" { Write-CCLSHost "CCLS Games CLI - Complete Command Reference" -ForegroundColor Green -Log Write-CCLSHost "=======================================" -ForegroundColor Green -Log Write-CCLSHost "`nBASIC COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "-------------" -ForegroundColor Yellow -Log Write-CCLSHost "help - Display basic help information" -Log Write-CCLSHost "help -list-all - Display this complete command reference" -Log Write-CCLSHost "clear, cls - Clear the console screen" -Log Write-CCLSHost "exit, quit - Exit the application" -Log Write-CCLSHost "logout - Log out and exit" -Log Write-CCLSHost "forget - Remove stored credentials" -Log Write-CCLSHost "`nSYSTEM COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "--------------" -ForegroundColor Yellow -Log Write-CCLSHost "setup - Configure download directories" -Log Write-CCLSHost "check - Check system requirements" -Log Write-CCLSHost "version - Display current version information" -Log Write-CCLSHost "update - Update the CLI tool to the latest version" -Log Write-CCLSHost "`nINSTALL COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "---------------" -ForegroundColor Yellow -Log Write-CCLSHost "install help - Show install command help" -Log Write-CCLSHost "install 7zip - Install 7-Zip utility for extraction" -Log Write-CCLSHost "install python - Download and install Python" -Log Write-CCLSHost "install requests - Install Python requests library" -Log Write-CCLSHost "`nSEARCH COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "--------------" -ForegroundColor Yellow -Log Write-CCLSHost "search help - Show search command help" -Log Write-CCLSHost "search [cg0000] - Search for game information by ID" -Log Write-CCLSHost "search [cb0000] - Search for bundle information by ID" -Log Write-CCLSHost "search library - List all available games and bundles" -Log Write-CCLSHost "`nDOWNLOAD COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "----------------" -ForegroundColor Yellow -Log Write-CCLSHost "get help - Show get command help" -Log Write-CCLSHost "get [cg0000] - Download and install a game by ID" -Log Write-CCLSHost "get [cb0000] - Download and install a bundle by ID" -Log Write-CCLSHost "get [cg0000/cb0000] -y - Download without confirmation prompt" -Log Write-CCLSHost "`nLIST COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "------------" -ForegroundColor Yellow -Log Write-CCLSHost "list help - Show list command help" -Log Write-CCLSHost "list games - List all installed games" -Log Write-CCLSHost "list games -d - List all installed games with details" -Log Write-CCLSHost "list games help - Show list games command help" -Log Write-CCLSHost "list game [name] - Show info about a specific game" -Log Write-CCLSHost "list game [name] -d - Show detailed info about a game" -Log Write-CCLSHost "list game [name] -tree - Show file tree structure for a game" -Log Write-CCLSHost "list game [name] -d -tree - Show full details and file tree for a game" -Log Write-CCLSHost "list game help - Show list game command help" -Log Write-CCLSHost "`nDELETE COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "--------------" -ForegroundColor Yellow -Log Write-CCLSHost "del help - Show del command help" -Log Write-CCLSHost "del [name] - Delete a game (with confirmation)" -Log Write-CCLSHost "del [name] -y - Delete a game without confirmation" -Log Write-CCLSHost "`nCHANGELOG COMMANDS" -ForegroundColor Yellow -Log Write-CCLSHost "---------------" -ForegroundColor Yellow -Log Write-CCLSHost "changelog [version] - Display changelog for a specific version" -Log Write-CCLSHost "changelog list - Show list of all available changelogs" -Log Write-CCLSHost "changelog latest - Display changelog for the latest version" -Log $helpProcessed = $true } # Main help command "^help$" { Write-CCLSHost "CCLS Games CLI - Help Overview" -ForegroundColor Green -Log Write-CCLSHost "============================" -ForegroundColor Green -Log Write-CCLSHost "`nBasic Commands:" -ForegroundColor Cyan -Log Write-CCLSHost " help - Show this help message" -Log Write-CCLSHost " help -list-all - Show all available commands in one big array" -Log Write-CCLSHost " clear, cls - Clear the console screen" -Log Write-CCLSHost " setup - Configure download directories" -Log Write-CCLSHost " check - Check system requirements (Python, requests, 7-Zip)" -Log Write-CCLSHost " version - Display the current version and check for updates" -Log Write-CCLSHost " update - Update the CLI tool to the latest version" -Log Write-CCLSHost " exit, quit - Exit the application" -Log Write-CCLSHost " logout - Log out and exit" -Log Write-CCLSHost " forget - Remove stored credentials" -Log Write-CCLSHost "`nGame Management:" -ForegroundColor Cyan -Log Write-CCLSHost " search [cg0000/cb0000] - Search for game/bundle information" -Log Write-CCLSHost " search library - List all available games and bundles" -Log Write-CCLSHost " get [cg0000/cb0000] - Download and install a game/bundle" -Log Write-CCLSHost " list games - List installed games (use -d for details)" -Log Write-CCLSHost " list game [name] - Show info about a specific game" -Log Write-CCLSHost " del [name] - Delete an installed game" -Log Write-CCLSHost " changelog [version] - Display changelog for a specific version" -Log Write-CCLSHost " install [utility] - Install specified utility" -Log Write-CCLSHost "`nFor detailed help on specific commands, type:" -ForegroundColor Yellow -Log Write-CCLSHost " [command] help - e.g., 'search help' or 'get help'" -Log $helpProcessed = $true } # Sub-help commands with standardized formatting "^search\s+help$" { Write-CCLSHost "CCLS Games CLI - Search Command Help" -ForegroundColor Green -Log Write-CCLSHost "=================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " search [id] - Search for a specific game or bundle" -Log Write-CCLSHost " search library - List all available games and bundles" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [id] - Game ID (cg0000) or Bundle ID (cb0000)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " search cg0025 - Get information about game with ID cg0025" -Log Write-CCLSHost " search cb0010 - Get information about bundle with ID cb0010" -Log Write-CCLSHost " search library - Display the complete game and bundle library" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Game/Bundle IDs can be found in the URL on the website:" -Log Write-CCLSHost " https://games.ccls.icu/game.php?id=cg0000" -Log Write-CCLSHost " https://games.ccls.icu/bundle.php?id=cb0000" -Log Write-CCLSHost " - You can also find IDs by using the 'search library' command" -Log $helpProcessed = $true } "^get\s+help$" { Write-CCLSHost "CCLS Games CLI - Get Command Help" -ForegroundColor Green -Log Write-CCLSHost "==============================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " get [id] [options] - Download and install a game or bundle" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [id] - Game ID (cg0000) or Bundle ID (cb0000)" -Log Write-CCLSHost "`nOptions:" -ForegroundColor Cyan -Log Write-CCLSHost " -y - Skip confirmation prompts (auto-confirm)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " get cg0025 - Download and install game with ID cg0025" -Log Write-CCLSHost " get cb0010 -y - Download and install bundle with ID cb0010 without prompts" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Game/Bundle IDs can be found using the 'search library' command" -Log Write-CCLSHost " - The command will download, extract, and save game information" -Log Write-CCLSHost " - Downloads can be stopped with Ctrl+Z" -Log $helpProcessed = $true } "^list\s+help$" { Write-CCLSHost "CCLS Games CLI - List Command Help" -ForegroundColor Green -Log Write-CCLSHost "===============================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " list games [options] - List all installed games" -Log Write-CCLSHost " list game [name] [options] - Display information about a specific game" -Log Write-CCLSHost "`nSubcommands:" -ForegroundColor Cyan -Log Write-CCLSHost " games - List all installed games" -Log Write-CCLSHost " game [name] - Show info about a specific game" -Log Write-CCLSHost "`nFor more details:" -ForegroundColor Cyan -Log Write-CCLSHost " list games help - Help for listing all games" -Log Write-CCLSHost " list game help - Help for displaying specific game info" -Log $helpProcessed = $true } "^list\s+games\s+help$" { Write-CCLSHost "CCLS Games CLI - List Games Command Help" -ForegroundColor Green -Log Write-CCLSHost "=====================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " list games [options] - List all installed games" -Log Write-CCLSHost "`nOptions:" -ForegroundColor Cyan -Log Write-CCLSHost " -d - Show detailed info (size, version, update status)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " list games - Display basic list of installed games" -Log Write-CCLSHost " list games -d - Display detailed list with sizes and versions" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - The detailed view (-d) will check for updates on installed games" -Log Write-CCLSHost " - Any game marked as 'OUTDATED' has a newer version available" -Log $helpProcessed = $true } "^list\s+game\s+help$" { Write-CCLSHost "CCLS Games CLI - List Game Command Help" -ForegroundColor Green -Log Write-CCLSHost "====================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " list game [name] [options] - Display information about a specific game" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [name] - Name of the installed game folder" -Log Write-CCLSHost "`nOptions:" -ForegroundColor Cyan -Log Write-CCLSHost " -d - Show detailed information (description, requirements, etc)" -Log Write-CCLSHost " -tree - Show file tree structure" -Log Write-CCLSHost " -d -tree - Show both detailed info and file tree structure" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " list game The Long Drive - Show basic info about The Long Drive" -Log Write-CCLSHost " list game The Long Drive -d - Show detailed info about The Long Drive" -Log Write-CCLSHost " list game The Long Drive -tree - Show file structure of The Long Drive" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Game names must match the folder name (non-case-sensitive)" -Log Write-CCLSHost " - The command checks for updates and marks outdated games" -Log $helpProcessed = $true } "^del\s+help$" { Write-CCLSHost "CCLS Games CLI - Delete Command Help" -ForegroundColor Green -Log Write-CCLSHost "=================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " del [name] [options] - Delete an installed game" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [name] - Name of the installed game folder to delete" -Log Write-CCLSHost "`nOptions:" -ForegroundColor Cyan -Log Write-CCLSHost " -y - Skip confirmation prompt (auto-confirm)" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " del The Long Drive - Delete The Long Drive (with confirmation)" -Log Write-CCLSHost " del The Long Drive -y - Delete The Long Drive without confirmation" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Game names must match the folder name (non-case-sensitive)" -Log Write-CCLSHost " - This operation permanently deletes the game files" -Log Write-CCLSHost " - You can always re-download deleted games with the 'get' command" -Log $helpProcessed = $true } # Updated help for install command "^install\s+help$" { Write-CCLSHost "CCLS Games CLI - Install Command Help" -ForegroundColor Green -Log Write-CCLSHost "==================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " install [utility] - Install required utilities" -Log Write-CCLSHost "`nParameters:" -ForegroundColor Cyan -Log Write-CCLSHost " [utility] - Name of the utility to install" -Log Write-CCLSHost "`nSupported Utilities:" -ForegroundColor Cyan -Log Write-CCLSHost " 7zip - 7-Zip for extracting downloaded files" -Log Write-CCLSHost " python - Python interpreter (latest version)" -Log Write-CCLSHost " requests - Python requests library for better downloads" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " install 7zip - Install 7-Zip for extraction" -Log Write-CCLSHost " install python - Download and install Python" -Log Write-CCLSHost " install requests - Install Python requests library" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - 7-Zip is required for extracting game archives" -Log Write-CCLSHost " - Python is required for advanced download features" -Log Write-CCLSHost " - Python requests enables faster downloads with resume capability" -Log Write-CCLSHost " - Run 'check' to see which utilities need to be installed" -Log $helpProcessed = $true } "^changelog\s+help$" { Write-CCLSHost "CCLS Games CLI - Changelog Command Help" -ForegroundColor Green -Log Write-CCLSHost "===================================" -ForegroundColor Green -Log Write-CCLSHost "`nUsage:" -ForegroundColor Cyan -Log Write-CCLSHost " changelog [version] - Display changelog for a specific version" -Log Write-CCLSHost " changelog list - Show list of all available changelogs" -Log Write-CCLSHost " changelog latest - Display changelog for the latest version" -Log Write-CCLSHost "`nExamples:" -ForegroundColor Cyan -Log Write-CCLSHost " changelog 1.1.3 - Show changes in version 1.1.3" -Log Write-CCLSHost " changelog list - View all available changelog versions" -Log Write-CCLSHost " changelog latest - Show the most recent changes" -Log Write-CCLSHost "`nNotes:" -ForegroundColor Cyan -Log Write-CCLSHost " - Changelogs document the changes, improvements, and bug fixes in each version" -Log Write-CCLSHost " - The current version is shown with the 'version' command" -Log $helpProcessed = $true } } if ($helpProcessed) { continue } # Handle base commands (commands without parameters) switch -Regex ($command.ToLower()) { "^search$" { Write-CCLSHost "Wrong command usage. Type 'search help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^get$" { Write-CCLSHost "Wrong command usage. Type 'get help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^list$" { Write-CCLSHost "Wrong command usage. Type 'list help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^del$" { Write-CCLSHost "Wrong command usage. Type 'del help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^install$" { Write-CCLSHost "Wrong command usage. Type 'install help' for a list of available commands." -ForegroundColor Red -Log $baseCommandProcessed = $true } "^changelog$" { Write-CCLSHost "Wrong command usage. Type 'changelog help' for a list of available options." -ForegroundColor Red -Log $baseCommandProcessed = $true } } # If a base command was processed, skip the regular command processing if ($baseCommandProcessed) { continue } # Regular command processing with parameters switch -Regex ($command.ToLower()) { # Your existing command handlers here "^check$" { Test-SystemRequirements } "^exit$|^quit$" { $running = $false Write-CCLSHost "Thank you for using the CCLS Games CLI Tool. Goodbye!" -ForegroundColor Cyan -Log } "^clear$|^cls$" { Clear-ConsoleScreen } "^install(?:\s+(.+))?$" { $utilityName = if ($matches.Count -gt 1) { $matches[1] } else { "" } Install-Utility -UtilityName $utilityName } "^version$" { Show-Version } "^update$" { Update-CliTool } "^setup$" { Start-Setup } "^search\s+(c[gb]\d{4})$" { $id = $matches[1] Search-Game -id $id } "^get\s+(c[gb]\d{4})(?:\s+-y)?$" { $id = $matches[1] $skipConfirmation = $command -match "-y$" Get-Game -id $id -SkipConfirmation:$skipConfirmation } "^search\s+library$|^library$" { Get-GamesList } "^list\s+games$" { Get-InstalledGames } "^list\s+games\s+-d$" { Get-InstalledGames -Detailed } "^list\s+game\s+(.+?)(?:\s+(?:-d|-tree))*$" { $gameName = $matches[1].Trim() $detailed = $command -match "\s+-d\b" $tree = $command -match "\s+-tree\b" Get-GameInfo -GameName $gameName -Detailed:$detailed -Tree:$tree } "^del\s+(.+?)(?:\s+-y)?$" { $gameName = $matches[1] $force = $command -match "-y$" Remove-Game -GameName $gameName -Force:$force } "^changelog\s+(.+)$" { $versionParam = $matches[1].Trim() Show-Changelog -Version $versionParam $commandProcessed = $true } "^logout$" { $running = $false Write-CCLSHost "Logging out..." -ForegroundColor Cyan -Log } "^forget$" { if (Test-Path $credentialsFile) { Remove-Item -Path $credentialsFile -Force $settings = Initialize-Settings $settings.RememberLogin = $false Save-Settings -settings $settings Write-CCLSHost "Stored credentials have been removed." -ForegroundColor Green -Log } else { Write-CCLSHost "No stored credentials found." -ForegroundColor Yellow -Log } } default { # Extract the base command from the input (first word) $baseCommand = $command.Trim().Split()[0].ToLower() # Known base commands that have help available $knownCommands = @('search', 'get', 'list', 'del', 'install') if ($knownCommands -contains $baseCommand) { # If it's a known command with incorrect parameters Write-CCLSHost "Wrong command usage. Type '$baseCommand help' for a list of available commands." -ForegroundColor Red -Log } else { # Completely unknown command Write-CCLSHost "Unknown command. Type 'help' for a list of all available commands." -ForegroundColor Red -Log } } } } } # Add a line to log script completion function End-Logging { $endTime = Get-Date "CCLS Games CLI Session ended at $endTime, duration: $(($endTime - [DateTime]::ParseExact($script:sessionStartTime, 'yyyy-MM-dd_HH-mm-ss', $null)).ToString())" | Out-File -FilePath $script:logFile -Append } # Register script exit event to ensure logging is completed Register-EngineEvent -SourceIdentifier ([System.Management.Automation.PsEngineEvent]::Exiting) -Action { End-Logging } | Out-Null # Function to handle the CLI startup function Start-CclsCliTool { # Initialize script variables $script:versionChanged = $false $script:previousVersion = $null Write-CCLSHost "Hello and welcome to the CCLS Games CLI Tool" -ForegroundColor Green -Log Write-CCLSHost "Before proceeding to using this software you will need to sign in." -Log Write-CCLSHost "If you do not have an account already please go to $baseUrl/login.php?signup to register a new account." -ForegroundColor Cyan -Log # Load settings (this will set versionChanged flag if needed) $settings = Initialize-Settings # Try auto-login if remember login is enabled $loginResult = @{ Success = $false } if ($settings.RememberLogin) { $loginResult = Start-AutoLogin } # If auto-login failed or is disabled, do manual login if (-not $loginResult.Success) { $loginResult = Start-ManualLogin } # If login succeeded, show main interface if ($loginResult.Success) { # Show welcome message Write-CCLSHost "Welcome to CCLS Games CLI Tool, $($loginResult.Username)!" -ForegroundColor Green -Log # Check if version has changed and show notification if ($script:versionChanged) { $versionInfo = Test-VersionUpdate $currentVersion = $versionInfo.CurrentVersion Write-CCLSHost "Welcome to $currentVersion! Type 'changelog latest' to view the changes made." -ForegroundColor Green -Log } # Show appropriate message based on setup status if ($settings.HasCompletedSetup) { Write-CCLSHost "Type 'help' for a list of available commands." -ForegroundColor Cyan -Log } else { Write-CCLSHost "ALERT, type command 'setup' to set critical values before downloading." -ForegroundColor Red -Log } Start-CommandInterface -username $loginResult.Username } else { Write-CCLSHost "Login failed. Press any key to exit..." -ForegroundColor Red -Log $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } } # Format a file size in bytes to a human-readable string function Format-Size { param ( [long]$Size ) if ($Size -ge 1GB) { return "{0:N2} GB" -f ($Size / 1GB) } elseif ($Size -ge 1MB) { return "{0:N2} MB" -f ($Size / 1MB) } elseif ($Size -ge 1KB) { return "{0:N2} KB" -f ($Size / 1KB) } else { return "$Size B" } } # Start the application Try { Start-CclsCliTool } Finally { # Ensure logging is completed even if script exits unexpectedly End-Logging }