# 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 and logging $script:progressInitialized = $false $script:topProgressLine = $null $script:logFile = $null $script:sessionStartTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" # 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 { if (-not (Test-Path $settingsFile)) { # Create default settings file $defaultSettings = @{ RememberLogin = $false DownloadPath = ".\downloads" TempDownloadPath = ".\tmp" HasCompletedSetup = $false } $defaultSettings | ConvertTo-Json | Set-Content -Path $settingsFile } # Load settings $settings = Get-Content -Path $settingsFile | ConvertFrom-Json 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 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 # 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 } } # Search for game information by CG number function Search-Game($cgNumber) { # Validate CG number format if ($cgNumber -notmatch "^cg\d{4}$") { Write-CCLSHost "Invalid CG number format. Please use format 'cg0000'." -ForegroundColor Red -Log return } try { # Construct the URL for the game JSON file $gameJsonUrl = "$cliApiUrl/search.php?id=$cgNumber" # Attempt to fetch the game information $params = @{ Uri = $gameJsonUrl Method = "GET" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } } Write-CCLSHost "#Searching for game information..." -Log # Fetch game 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 } $gameInfo = $response } catch { Write-CCLSHost "Error fetching game information: $($_.Exception.Message)" -ForegroundColor Red -Log return } # Display game information in a formatted way Write-CCLSHost "`n==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Game Information for $($gameInfo.name) ($($gameInfo.id))" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "`nDescription:" -ForegroundColor Cyan -Log Write-CCLSHost $gameInfo.description -Log 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 } } Write-CCLSHost "`nDetails:" -ForegroundColor Cyan -Log if ($gameInfo.size) { Write-CCLSHost "Size: $($gameInfo.size)" -Log } if ($gameInfo.version -and $gameInfo.version -ne "") { Write-CCLSHost "Version: $($gameInfo.version)" -Log } 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 } } 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 } 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 } } } 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 game 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]$cgNumber, [switch]$SkipConfirmation ) # Validate CG number format if ($cgNumber -notmatch "^cg\d{4}$") { Write-CCLSHost "Invalid CG number format. Please use format 'cg0000'." -ForegroundColor Red -Log return } # Check if setup has been completed $settings = Initialize-Settings if (-not $settings.HasCompletedSetup) { Write-CCLSHost "ALERT, you must run the 'setup' command before downloading games." -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 } } # Fetch game download information try { $gameInfoUrl = "$cliApiUrl/get.php?id=$cgNumber" $params = @{ Uri = $gameInfoUrl Method = "GET" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } } Write-CCLSHost "#Fetching game download information..." -Log $response = Invoke-RestMethod @params if (-not $response.success) { Write-CCLSHost "Error: $($response.message)" -ForegroundColor Red -Log return } $gameName = $response.name $gameId = $response.id $downloadUrl = $response.download_url $gameSize = $response.size # Create download file name from URL $fileName = Split-Path -Path $downloadUrl -Leaf if ([string]::IsNullOrEmpty($fileName)) { $fileName = "$gameId.7z" } $downloadPath = Join-Path -Path $settings.TempDownloadPath -ChildPath $fileName # Confirm download with user Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Game Download: $gameName ($gameId)" -ForegroundColor Green -Log Write-CCLSHost "==========================================================" -ForegroundColor DarkGray -Log Write-CCLSHost "Size: $gameSize" -ForegroundColor Yellow -Log Write-CCLSHost "Download URL: $downloadUrl" -ForegroundColor Cyan -Log $proceed = $true if (-not $SkipConfirmation) { Write-CCLSHost "ALERT, the get command will start the downloading process of the game specified, to stop it do 'Ctrl C'" -ForegroundColor Red -Log 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 } try { # Try to invoke Python to check if it's available $pythonVersion = python --version 2>&1 $pythonAvailable = $true Write-CCLSHost "#Python detected: $pythonVersion" -Log # 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 # Check if the Python script exists if (-not (Test-Path $pythonScript)) { Write-CCLSHost "#Python script not found at $pythonScript, creating it..." -Log # You need to paste the content of ccls_downloader.py here as a multiline string $pythonCode = @' #!/usr/bin/env python3 """ CCLS High-Speed Downloader (Improved Version) -------------------------------------------- A high-performance Python script for downloading games from CCLS with improved connection handling, retry logic, and resume capabilities. """ import os import sys import time import requests import signal import random from datetime import datetime, timedelta # 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 # Setup signal handling for clean exit on Ctrl+C def signal_handler(sig, frame): print("\nDOWNLOAD_CANCELLED") sys.exit(1) signal.signal(signal.SIGINT, signal_handler) 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 get_file_size(url, headers): """Get the size of the file to be downloaded""" for attempt in range(MAX_RETRIES): try: # Only get the headers, dont download the content yet response = requests.head(url, headers=headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT)) 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: print(f"Error getting file size: {str(e)}. Retrying in {delay:.1f} seconds...") time.sleep(delay) else: 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""" try: # Add range header to check if the server supports it resume_headers = headers.copy() resume_headers['Range'] = 'bytes=0-0' response = requests.head(url, headers=resume_headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT)) # If we get a 206 status, the server supports resume return response.status_code == 206 except: # 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""" headers = { "User-Agent": USER_AGENT, "Connection": "keep-alive", "Accept-Encoding": "gzip, deflate" } # Get the file size if not provided total_size = expected_size if total_size is None: total_size = get_file_size(url, headers) if total_size is None: print("Warning: Unable to determine file size in advance. Progress reporting may be inaccurate.") # Check if we can resume supports_resume = can_resume(url, headers) # 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}") 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): try: # Open the file in append mode if resuming, otherwise write mode file_mode = 'ab' if downloaded > 0 else 'wb' with requests.get(url, headers=headers, stream=True, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT)) 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: 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"\n[Download Progress - {game_name} ({game_id})]") with open(destination, file_mode) as f: for chunk in response.iter_content(chunk_size=CHUNK_SIZE): 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)}") return True except KeyboardInterrupt: print("\nDownload cancelled by user.") # Dont delete the file - we might want to resume later return False except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: # Handle connection timeout with exponential backoff retry 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"Downloaded: {format_size(downloaded)}. You can try running the command again to resume.") return False except requests.exceptions.RequestException as e: # Handle other request exceptions 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"Downloaded: {format_size(downloaded)}. You can try running the command again to resume.") return False except IOError as e: # Handle file I/O errors print(f"\nFile I/O error: {str(e)}") return False return False def main(): """Main entry point for the script""" # Check if we have enough arguments if len(sys.argv) < 5: print("Usage: python ccls_downloader.py []") return 1 try: download_url = sys.argv[1] output_file = sys.argv[2] game_name = sys.argv[3] game_id = sys.argv[4] # Optional size parameter expected_size = None if len(sys.argv) > 5: try: expected_size = int(sys.argv[5]) except ValueError: 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) 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)}") return 1 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: print("\nDownload cancelled by user.") sys.exit(1) '@ # Write the Python script to file Set-Content -Path $pythonScript -Value $pythonCode -Encoding UTF8 Write-CCLSHost "#Created Python script at $pythonScript" -Log } # If we have Python and the downloader script, use it if ($pythonAvailable -and (Test-Path $pythonScript)) { Write-CCLSHost "#Using Python high-speed downloader" -Log # Convert the game size to bytes if possible (for progress calculation) $sizeInBytes = $null if ($gameSize -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 += "`"$gameName`"" $argList += "`"$gameId`"" if ($sizeInBytes) { $argList += "$sizeInBytes" } $commandLine = "python $($argList -join ' ')" Write-CCLSHost "#Running command: $commandLine" -Log # 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) { Write-CCLSHost "Python downloader failed with exit code $($process.ExitCode)" -ForegroundColor Red -Log Write-CCLSHost "#Falling back to PowerShell download method" -Log Use-PowerShellDownload -downloadUrl $downloadUrl -downloadPath $downloadPath -gameName $gameName -gameId $gameId } } catch { Write-CCLSHost "Error running Python downloader: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "#Falling back to PowerShell download method" -Log Use-PowerShellDownload -downloadUrl $downloadUrl -downloadPath $downloadPath -gameName $gameName -gameId $gameId } } else { # Fall back to the original PowerShell download method Write-CCLSHost "#Python downloader not available, using PowerShell download method" -Log Use-PowerShellDownload -downloadUrl $downloadUrl -downloadPath $downloadPath -gameName $gameName -gameId $gameId } } catch { $pythonAvailable = $false Write-CCLSHost "#Python not found, falling back to PowerShell download method" -Log Use-PowerShellDownload -downloadUrl $downloadUrl -downloadPath $downloadPath -gameName $gameName -gameId $gameId } # Start extraction process Write-CCLSHost "#Starting extraction process..." -Log try { # Define the self-contained extraction function function Start-GameExtraction { # Load settings independently (similar to ass.ps1 but with adjusted path) $scriptPath = $settingsFolder $settingsPath = Join-Path -Path $scriptPath -ChildPath "settings.json" Write-CCLSHost "#Loading settings from: $settingsPath" -Log # Check if settings file exists if (-not (Test-Path -Path $settingsPath)) { Write-CCLSHost "Error: Settings file not found at '$settingsPath'." -ForegroundColor Red -Log return } # Load and parse the settings.json file $extractSettings = Get-Content -Path $settingsPath -ErrorAction Stop | ConvertFrom-Json # Get paths from settings $tempDownloadPath = $extractSettings.TempDownloadPath $downloadPath = $extractSettings.DownloadPath Write-CCLSHost "#Loaded TempDownloadPath: $tempDownloadPath" -Log Write-CCLSHost "#Loaded DownloadPath: $downloadPath" -Log # Verify paths exist if (-not (Test-Path -Path $tempDownloadPath)) { Write-CCLSHost "Error: TempDownloadPath '$tempDownloadPath' does not exist." -ForegroundColor Red -Log return } if (-not (Test-Path -Path $downloadPath)) { Write-CCLSHost "Error: DownloadPath '$downloadPath' does not exist." -ForegroundColor Red -Log return } # Check if 7-Zip is installed $7zipPath = "C:\Program Files\7-Zip\7z.exe" if (-not (Test-Path -Path $7zipPath)) { # Try alternative paths $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 or ensure it's in one of these locations:" -ForegroundColor Yellow -Log Write-CCLSHost " - C:\Program Files\7-Zip\7z.exe" -ForegroundColor Yellow -Log Write-CCLSHost " - C:\Program Files (x86)\7-Zip\7z.exe" -ForegroundColor Yellow -Log Write-CCLSHost " - .\7zip\7z.exe" -ForegroundColor Yellow -Log return } } # Get all .7z files in the temp download path Write-CCLSHost "#Searching for .7z files in: $tempDownloadPath" -Log $7zFiles = Get-ChildItem -Path $tempDownloadPath -Filter "*.7z" # If no .7z files found, exit if ($7zFiles.Count -eq 0) { Write-CCLSHost "No .7z files found in '$tempDownloadPath'." -ForegroundColor Yellow -Log return } Write-CCLSHost "#Found $($7zFiles.Count) .7z files to process." -Log # Process each .7z file foreach ($file in $7zFiles) { $filePath = $file.FullName # Extract the file Write-CCLSHost "Extracting: $filePath to $downloadPath" -ForegroundColor Cyan -Log & "$7zipPath" x "$filePath" -o"$downloadPath" -y # Check if extraction was successful if ($LASTEXITCODE -eq 0) { # Delete the original .7z file Write-CCLSHost "Extraction successful. Deleting original file: $filePath" -ForegroundColor Green -Log Remove-Item -Path $filePath -Force } else { Write-CCLSHost "Failed to extract: $filePath. Skipping deletion." -ForegroundColor Red -Log } } Write-CCLSHost "#All .7z files have been processed." -Log Write-CCLSHost "Go to $downloadPath to play the game." -ForegroundColor Cyan -Log } # Call the extraction function Start-GameExtraction } catch { Write-CCLSHost "An error occurred during extraction: $($_.Exception.Message)" -ForegroundColor Red -Log Write-CCLSHost "Error details: $($_.Exception.StackTrace)" -ForegroundColor Red -Log } } catch { Write-CCLSHost "An error occurred: $($_.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 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 to list all available games function Get-GamesList { try { # Construct the URL for the game list API $gamesListUrl = "$cliApiUrl/list.php" $params = @{ Uri = $gamesListUrl Method = "GET" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } } Write-CCLSHost "#Fetching game library..." -Log # Fetch game 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 return } # 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 } Write-CCLSHost "Use 'search [cgnumber]' to get detailed information about a specific game." -ForegroundColor Yellow -Log # REMOVED: The following lines that were causing the extra keypress requirement # Write-CCLSHost "#Press any key to return to the main menu..." -Log # $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } catch { Write-CCLSHost "Error fetching game library: $($_.Exception.Message)" -ForegroundColor Red -Log # Also removed the keypress requirement here } } catch { Write-CCLSHost "An error occurred while processing game library: $($_.Exception.Message)" -ForegroundColor Red -Log # Also removed the keypress requirement here } } function Start-CclsCliTool { 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 $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) { # Check for version updates Test-VersionUpdate # Notice we removed the "`n`n" at the beginning of the welcome message Write-CCLSHost "Welcome to CCLS Games CLI Tool, $($loginResult.Username)!" -ForegroundColor Green -Log # Show appropriate message based on setup status $settings = Initialize-Settings 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") } } 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 format file size in a human-readable way 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" } } # 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 } # Function to list games in the download directory 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) { if ($Detailed) { # Get folder size for detailed view $size = Get-FolderSize -Path $folder.FullName $totalSize += $size $sizeFormatted = Format-Size -Size $size # Display with size Write-CCLSHost "$($folder.Name) - $sizeFormatted" -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 unlimited file tree depth) function Get-GameInfo { param ( [string]$GameName, [switch]$Detailed ) # 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 Yellow -Log return } # Get game size $size = Get-FolderSize -Path $gamePath $sizeFormatted = Format-Size -Size $size # Header Write-CCLSHost "`nGame Information: $GameName" -ForegroundColor Green -Log Write-CCLSHost "=======================" -ForegroundColor Green -Log Write-CCLSHost "Location: $gamePath" -Log Write-CCLSHost "Size: $sizeFormatted" -Log # If detailed view, show file tree if ($Detailed) { 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 Yellow -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.0" 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 already running the latest version ($currentVersion)." -ForegroundColor Green -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 } } # Modified Start-CommandInterface function to include the secret command function Start-CommandInterface($username) { $running = $true while ($running) { Write-CCLSHost "CCLS>" -ForegroundColor Yellow -NoNewline -Log $command = Read-Host Write-CCLSHost "$command" -NoConsole -Log switch -Regex ($command.ToLower()) { "^exit|^quit" { $running = $false Write-CCLSHost "Thank you for using the CCLS Games CLI Tool. Goodbye!" -ForegroundColor Cyan -Log } "^clear|^cls" { # Call the clear screen function Clear-ConsoleScreen } "^help" { Write-CCLSHost "Available Commands:" -ForegroundColor Green -Log Write-CCLSHost " help - Show this help message" -Log Write-CCLSHost " clear, cls - Clear the console screen" -Log Write-CCLSHost " setup - Configure download directories" -Log Write-CCLSHost " search [cg0000] - Search for game information by CG number" -Log Write-CCLSHost " search library - List all available games for download" -Log Write-CCLSHost " get [cg0000] - Download and install a game by CG number" -Log Write-CCLSHost " get [cg0000] -y - Download and install a game by CG number without confirmation" -Log Write-CCLSHost " list games - List installed games (names only)" -Log Write-CCLSHost " list games -d - List installed games with sizes" -Log Write-CCLSHost " list game [name] - Show info about a specific game" -Log Write-CCLSHost " list game [name] -d - Show detailed info about a specific game with file tree" -Log Write-CCLSHost " del [name] - Delete a game (with confirmation)" -Log Write-CCLSHost " del [name] -y - Delete a game without confirmation" -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 } "^update$" { Update-CliTool } "^setup" { Start-Setup } "^search$" { Write-CCLSHost "To use the search command please provide a valid CG number in this format 'search cg0000'" -ForegroundColor Cyan -Log Write-CCLSHost "To get the CG number of a game, go to the main games page and click on any game, then check" -ForegroundColor Cyan -Log Write-CCLSHost "the URL 'https://games.ccls.icu/game.php?id=cg0000' and the 'cg0000' part in URL is the CG number" -ForegroundColor Cyan -Log } "^search\s+(cg\d{4})$" { $cgNumber = $matches[1] Search-Game -cgNumber $cgNumber } "^get$" { Write-CCLSHost "To use the get command please provide a valid CG number in this format 'get cg0000'" -ForegroundColor Cyan -Log Write-CCLSHost "To get the CG number of a game, go to the main games page and click on any game, then check" -ForegroundColor Cyan -Log Write-CCLSHost "the URL 'https://games.ccls.icu/game.php?id=cg0000' and the 'cg0000' part in URL is the CG number." -ForegroundColor Cyan -Log Write-CCLSHost "You can use 'get cg0000 -y' to skip confirmation prompt." -ForegroundColor Cyan -Log Write-CCLSHost "ALERT, the get command will start the downloading process of the game specified, to stop it do 'Ctrl C'" -ForegroundColor Red -Log } "^get\s+(cg\d{4})(?:\s+-y)?$" { $cgNumber = $matches[1] $skipConfirmation = $command -match "-y$" Get-Game -cgNumber $cgNumber -SkipConfirmation:$skipConfirmation } "^search\s+library$|^library$" { Get-GamesList } "^list\s+games$" { # List games with minimal detail Get-InstalledGames } "^list\s+games\s+-d$" { # List games with detailed information Get-InstalledGames -Detailed } "^list\s+game\s+(.+?)(?:\s+-d)?$" { $gameName = $matches[1] $detailed = $command -match "-d$" # Show information about a specific game Get-GameInfo -GameName $gameName -Detailed:$detailed } "^del\s+(.+?)(?:\s+-y)?$" { $gameName = $matches[1] $force = $command -match "-y$" # Delete the specified game Remove-Game -GameName $gameName -Force:$force } "^logout" { $running = $false Write-CCLSHost "Logging out..." -ForegroundColor Cyan -Log # Clear the current session but keep credentials if they exist } "^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 { Write-CCLSHost "Unknown command. Type 'help' for a list of 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 # Start the application Try { Start-CclsCliTool } Finally { # Ensure logging is completed even if script exits unexpectedly End-Logging }