View Raw # 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 <download_url> <output_file> <game_name> <game_id> [<total_size_bytes>]")
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
}