# 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 { 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 # 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 } } 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 your script uses credentials 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 } } # Fetch download information try { # If your script uses credentials, use this block: if ($script:cachedCredentials -ne $null) { $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 } } } else { # Otherwise use this block: $params = @{ Uri = "$cliApiUrl/get.php?id=$id" Method = "GET" Headers = @{ "User-Agent" = "CCLS-CLI/1.0" } } } 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 # Confirm download with user 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 $proceed = $true 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 } # Flag to track if download was successfully completed $downloadSuccessful = $false try { # Try to invoke Python to check if it's available $pythonVersion = python --version 2>&1 $pythonAvailable = $true Write-CCLSHost "#Python detected: $pythonVersion" -Log -NoConsole # Determine the actual location of the Python script $scriptLocation = if ($PSScriptRoot) { # If running from a script, use its location $PSScriptRoot } else { # If running in console, use current directory (Get-Location).Path } $pythonScript = Join-Path -Path $scriptLocation -ChildPath "ccls_downloader.py" # Debug the script path Write-CCLSHost "#Python script path: $pythonScript" -Log -NoConsole # Always recreate the Python script on each run if (Test-Path $pythonScript) { Remove-Item -Path $pythonScript -Force Write-CCLSHost "#Removed existing Python script" -Log -NoConsole } # Create a new copy of the script with the latest code Write-CCLSHost "#Creating Python script at $pythonScript" -Log -NoConsole # Python downloader script content $pythonCode = @' #!/usr/bin/env python3 """ CCLS High-Speed Downloader (Silent Version with Ctrl+Z stop) -------------------------------------------- A high-performance Python script for downloading games from CCLS with improved connection handling, retry logic, resume capabilities, and URL encoding fixes, with minimal output to keep the console clean. Uses Ctrl+Z to stop downloads. """ import os import sys import time import requests import signal import random import msvcrt # For Windows key detection import urllib.parse from datetime import datetime, timedelta from threading import Thread, Event # Constants for downloading CHUNK_SIZE = 1024 * 1024 # 1MB chunks for better performance USER_AGENT = "CCLS-CLI/1.0 (Python Downloader)" MAX_RETRIES = 5 # Increased from 3 CONNECT_TIMEOUT = 30 # Connection timeout in seconds READ_TIMEOUT = 60 # Read timeout in seconds RETRY_BASE_DELAY = 2 # Base delay for exponential backoff RETRY_MAX_DELAY = 60 # Maximum delay between retries DEBUG_MODE = False # Set to True to enable debug messages # Global event for signaling cancellation between threads cancellation_event = Event() # Thread for checking for Ctrl+Z key press def key_monitor(): """Monitor for Ctrl+Z (ASCII 26) key press to cancel downloads""" while not cancellation_event.is_set(): if msvcrt.kbhit(): key = msvcrt.getch() # Check for Ctrl+Z (ASCII 26, or SUB character) if key == b'\x1a': print("\nCtrl+Z detected - Cancelling download and removing partial file...") cancellation_event.set() break time.sleep(0.1) # Small sleep to prevent CPU hogging def debug_print(message): """Print only if debug mode is enabled""" if DEBUG_MODE: print(message) def format_size(size_bytes): """Format bytes into human-readable format""" if size_bytes >= 1_000_000_000: # GB return f"{size_bytes / 1_000_000_000:.2f} GB" elif size_bytes >= 1_000_000: # MB return f"{size_bytes / 1_000_000:.2f} MB" elif size_bytes >= 1000: # KB return f"{size_bytes / 1000:.2f} KB" else: return f"{size_bytes} B" def format_time(seconds): """Format seconds into HH:MM:SS format""" hours = seconds // 3600 minutes = (seconds % 3600) // 60 secs = seconds % 60 if hours > 0: return f"{hours:02d}:{minutes:02d}:{secs:02d}" else: return f"{minutes:02d}:{secs:02d}" def normalize_url(url): """ Normalize URL by ensuring proper encoding of special characters while preserving URL structure """ # Parse the URL into components parsed = urllib.parse.urlparse(url) # Split the path by '/' and encode each segment separately path_parts = parsed.path.split('/') encoded_parts = [] for part in path_parts: # Dont double-encode already encoded segments, but ensure other parts are encoded if '%' in part and any(c.isalnum() for c in part.split('%')[1][:2]): encoded_parts.append(part) else: # Encode the part, preserving allowed characters encoded_parts.append(urllib.parse.quote(part, safe='')) # Reconstruct the path encoded_path = '/'.join(encoded_parts) # Build the URL with proper encoding normalized_url = urllib.parse.urlunparse(( parsed.scheme, parsed.netloc, encoded_path, parsed.params, parsed.query, parsed.fragment )) return normalized_url def get_file_size(url, headers): """Get the size of the file to be downloaded""" # First, make sure the URL is properly encoded normalized_url = normalize_url(url) for attempt in range(MAX_RETRIES): try: # Only get the headers, dont download the content yet response = requests.head(normalized_url, headers=headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) response.raise_for_status() # Return the content length if available content_length = response.headers.get('content-length') if content_length: return int(content_length) # If we cant get the size via HEAD request, return None return None except (requests.exceptions.RequestException, IOError) as e: delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) if attempt < MAX_RETRIES - 1: debug_print(f"Error getting file size: {str(e)}. Retrying in {delay:.1f} seconds...") time.sleep(delay) else: debug_print(f"Failed to get file size after {MAX_RETRIES} attempts: {str(e)}") return None def can_resume(url, headers): """Check if the server supports resuming downloads""" normalized_url = normalize_url(url) try: # Add range header to check if the server supports it resume_headers = headers.copy() resume_headers['Range'] = 'bytes=0-0' response = requests.head(normalized_url, headers=resume_headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) # If we get a 206 status, the server supports resume return response.status_code == 206 except Exception as e: debug_print(f"Error checking resume capability: {str(e)}") # If theres any error, assume we cant resume to be safe return False def download_file(url, destination, game_name, game_id, expected_size=None): """Download a file with progress tracking and resume support""" global cancellation_event cancellation_event.clear() # Reset the cancellation event # Start key monitoring thread key_thread = Thread(target=key_monitor) key_thread.daemon = True key_thread.start() # Print information about stopping print("\nDownload started! Press Ctrl+Z to stop the download at any time.\n") headers = { "User-Agent": USER_AGENT, "Connection": "keep-alive", "Accept-Encoding": "gzip, deflate" } # Normalize the URL to handle special characters normalized_url = normalize_url(url) debug_print(f"Using normalized URL: {normalized_url}") # Get the file size if not provided total_size = expected_size if total_size is None: total_size = get_file_size(normalized_url, headers) # Check if we can resume supports_resume = can_resume(normalized_url, headers) debug_print(f"Server {'supports' if supports_resume else 'does not support'} resume capability.") # Check if the file already exists and if we should resume downloaded = 0 if os.path.exists(destination) and supports_resume: downloaded = os.path.getsize(destination) if total_size and downloaded >= total_size: print(f"\nFile already completely downloaded: {destination}") cancellation_event.set() # Signal to stop key monitor thread return True elif downloaded > 0: print(f"\nResuming download from {format_size(downloaded)}") headers['Range'] = f'bytes={downloaded}-' # Prepare for progress tracking start_time = time.time() init_downloaded = downloaded # Keep the initial download amount for speed calculation last_update_time = start_time last_downloaded = downloaded # For calculating current download speed # Setup for retrying connection for attempt in range(MAX_RETRIES): # Check if download was cancelled by user if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False try: # Open the file in append mode if resuming, otherwise write mode file_mode = 'ab' if downloaded > 0 else 'wb' with requests.get(normalized_url, headers=headers, stream=True, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT), allow_redirects=True) as response: response.raise_for_status() # If we requested a range but got the whole file, adjust our counter if downloaded > 0 and response.status_code != 206: debug_print("Warning: Server doesnt support resuming. Starting from beginning.") downloaded = 0 file_mode = 'wb' # Update total_size if we can get it from headers if total_size is None and 'content-length' in response.headers: total_size = int(response.headers['content-length']) + downloaded print(f"[Download Progress - {game_name} ({game_id})]") with open(destination, file_mode) as f: for chunk in response.iter_content(chunk_size=CHUNK_SIZE): # Check for cancellation if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") f.close() # Close file handle before deleting try: os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False if chunk: f.write(chunk) downloaded += len(chunk) current_time = time.time() # Update progress every 0.5 seconds if (current_time - last_update_time) >= 0.5: last_update_time = current_time # Calculate progress values elapsed_time = current_time - start_time elapsed_seconds = int(elapsed_time) progress_percent = int((downloaded / total_size) * 100) if total_size and total_size > 0 else 0 # Calculate overall average speed avg_download_speed = (downloaded - init_downloaded) / elapsed_time if elapsed_time > 0 else 0 avg_download_speed_mbps = avg_download_speed * 8 / 1024 / 1024 # Convert to Mbps # Calculate current window speed (last 0.5 seconds) current_window_size = downloaded - last_downloaded current_speed = current_window_size / (current_time - last_update_time + 0.001) current_speed_mbps = current_speed * 8 / 1024 / 1024 # Convert to Mbps last_downloaded = downloaded # Calculate remaining time based on average speed remaining_bytes = total_size - downloaded if total_size else 0 if avg_download_speed > 0 and remaining_bytes > 0: remaining_seconds = int(remaining_bytes / avg_download_speed) else: remaining_seconds = 0 # Simple output - replace previous line with new status # Carriage return to move cursor to beginning of line sys.stdout.write("\r" + " " * 80 + "\r") # Clear line # Print progress information if total_size and total_size > 0: prog_str = f"Progress: {progress_percent}% | " else: prog_str = "" # Show actual progress info size_info = f"of {format_size(total_size)}" if total_size else "" status = f"{prog_str}Downloaded: {format_size(downloaded)} {size_info} | Speed: {avg_download_speed_mbps:.2f} Mbps" sys.stdout.write(status) sys.stdout.flush() # Final update elapsed_time = time.time() - start_time elapsed_seconds = int(elapsed_time) avg_download_speed = (downloaded - init_downloaded) / elapsed_time if elapsed_time > 0 else 0 avg_download_speed_mbps = avg_download_speed * 8 / 1024 / 1024 # Print final stats on new lines print("\n\nDownload completed successfully!") print(f"Total Size: {format_size(downloaded)}") print(f"Average Speed: {avg_download_speed_mbps:.2f} Mbps") print(f"Time Elapsed: {format_time(elapsed_seconds)}") # Signal to stop key monitor thread cancellation_event.set() return True except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e: # Handle connection timeout with exponential backoff retry if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) if attempt < MAX_RETRIES - 1: print(f"\nConnection timed out or lost: {str(e)}. Retrying in {delay:.1f} seconds...") print(f"Downloaded so far: {format_size(downloaded)}") time.sleep(delay) # Update headers for resuming from the current position headers['Range'] = f'bytes={downloaded}-' last_update_time = time.time() # Reset the update timer # Print a new header for the next attempt print(f"\n[Download Progress - {game_name} ({game_id}) - Attempt {attempt+2}]") else: print(f"\nDownload failed after {MAX_RETRIES} attempts: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False except requests.exceptions.RequestException as e: # Handle other request exceptions if cancellation_event.is_set(): print("\nDownload cancelled by user. Removing partial file...") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as e: print(f"Note: Could not delete partial file: {str(e)}") return False if attempt < MAX_RETRIES - 1: delay = min(RETRY_BASE_DELAY * (2 ** attempt) + random.uniform(0, 1), RETRY_MAX_DELAY) print(f"\nDownload error: {str(e)}. Retrying in {delay:.1f} seconds...") time.sleep(delay) # Update headers for resuming from the current position headers['Range'] = f'bytes={downloaded}-' last_update_time = time.time() # Print a new header for the next attempt print(f"\n[Download Progress - {game_name} ({game_id}) - Attempt {attempt+2}]") else: print(f"\nDownload failed after {MAX_RETRIES} attempts: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False except IOError as e: # Handle file I/O errors print(f"\nFile I/O error: {str(e)}") print(f"Note: Partial file will be removed.") try: if os.path.exists(destination): os.remove(destination) print(f"Partial file deleted successfully.") except Exception as ex: print(f"Note: Could not delete partial file: {str(ex)}") cancellation_event.set() # Signal to stop key monitor thread return False cancellation_event.set() # Signal to stop key monitor thread return False def main(): """Main entry point for the script""" # Check if we have enough arguments if len(sys.argv) < 5: print("Usage: python ccls_downloader.py []") return 1 try: download_url = sys.argv[1] output_file = sys.argv[2] game_name = sys.argv[3] game_id = sys.argv[4] debug_print(f"Download URL: {download_url}") debug_print(f"Output file: {output_file}") debug_print(f"Item name: {game_name}") debug_print(f"Item ID: {game_id}") # Optional size parameter expected_size = None if len(sys.argv) > 5: try: expected_size = int(sys.argv[5]) debug_print(f"Expected size: {format_size(expected_size)}") except ValueError: debug_print(f"Warning: Invalid size parameter '{sys.argv[5]}', will use content-length from server") # Create output directory if it doesnt exist output_dir = os.path.dirname(output_file) if output_dir and not os.path.exists(output_dir): try: os.makedirs(output_dir) debug_print(f"Created output directory: {output_dir}") except Exception as e: print(f"Error: Could not create directory: {str(e)}") return 1 # Start download result = download_file(download_url, output_file, game_name, game_id, expected_size) # Return success or failure code return 0 if result else 1 except Exception as e: print(f"Error: Unexpected error: {str(e)}") if DEBUG_MODE: import traceback traceback.print_exc() return 1 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: # This shouldnt be reached now that we use our own key handler print("\nKeyboard interrupt detected.") sys.exit(1) '@ # Write the Python script to file 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 } # Only proceed with extraction if download was successful if ($downloadSuccessful) { # Start extraction process Write-CCLSHost "#Starting extraction process..." -Log -NoConsole 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 -NoConsole # 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 -NoConsole Write-CCLSHost "#Loaded DownloadPath: $downloadPath" -Log -NoConsole # 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 -NoConsole $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 -NoConsole # Process each .7z file foreach ($file in $7zFiles) { $filePath = $file.FullName # Extract the file Write-CCLSHost "Extracting: $filePath to $downloadPath" -ForegroundColor Cyan -Log # Capture and suppress 7-Zip output, only show errors if extraction fails $extractionOutput = & "$7zipPath" x "$filePath" -o"$downloadPath" -y 2>&1 $extractionSuccess = $? # Check if extraction was successful if ($extractionSuccess -and $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 -NoConsole Write-CCLSHost "Go to $downloadPath to access the $itemType." -ForegroundColor Cyan -Log } # Call the extraction function Start-GameExtraction } catch { Write-CCLSHost "An error occurred during extraction: $($_.Exception.Message)" -ForegroundColor Red -Log } } } catch { Write-CCLSHost "An error occurred: $($_.Exception.Message)" -ForegroundColor Red -Log } } # 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 "Downloading using PowerShell method..." -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 } } # 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 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 } # 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.2" 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 } } # 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 { } } # Modified Start-CommandInterface function with added 'version' 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 [cb0000] - Search for bundle information by CB number" -Log Write-CCLSHost " search library - List all available games and bundles for download" -Log Write-CCLSHost " get [cg0000] - Download and install a game by CG number" -Log Write-CCLSHost " get [cb0000] - Download and install a bundle by CB number" -Log Write-CCLSHost " get [id] -y - Download without confirmation" -Log Write-CCLSHost " list games - List installed games and bundles (names only)" -Log Write-CCLSHost " list games -d - List installed games and bundles with sizes" -Log Write-CCLSHost " list game [name] - Show info about a specific game or bundle" -Log Write-CCLSHost " list game [name] -d - Show detailed info with file tree" -Log Write-CCLSHost " del [name] - Delete a game or bundle (with confirmation)" -Log Write-CCLSHost " del [name] -y - Delete a game or bundle without confirmation" -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 } "^version$" { # Show version information Show-Version } "^update$" { Update-CliTool } "^setup" { Start-Setup } "^search$" { Write-CCLSHost "To use the search command please provide a valid ID in this format:" -ForegroundColor Cyan -Log Write-CCLSHost " - 'search cg0000' for games" -ForegroundColor Cyan -Log Write-CCLSHost " - 'search cb0000' for bundles" -ForegroundColor Cyan -Log Write-CCLSHost "To get the CG/CB number, check the URL 'https://games.ccls.icu/game.php?id=cg0000' or" -ForegroundColor Cyan -Log Write-CCLSHost "'https://games.ccls.icu/bundle.php?id=cb0000' and the ID part in URL is what you need" -ForegroundColor Cyan -Log } "^search\s+(c[gb]\d{4})$" { $id = $matches[1] Search-Game -id $id } "^get$" { Write-CCLSHost "To use the get command please provide a valid ID in this format:" -ForegroundColor Cyan -Log Write-CCLSHost " - 'get cg0000' for games" -ForegroundColor Cyan -Log Write-CCLSHost " - 'get cb0000' for bundles" -ForegroundColor Cyan -Log Write-CCLSHost "To get the CG/CB number, check the URL 'https://games.ccls.icu/game.php?id=cg0000' or" -ForegroundColor Cyan -Log Write-CCLSHost "'https://games.ccls.icu/bundle.php?id=cb0000' and the ID part in URL is what you need" -ForegroundColor Cyan -Log Write-CCLSHost "You can use 'get [id] -y' to skip confirmation prompt." -ForegroundColor Cyan -Log Write-CCLSHost "ALERT, the get command will start the downloading process, to stop it do 'Ctrl C'" -ForegroundColor Red -Log } "^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$" { # 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 # Function to handle the CLI startup 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 # Show 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") } } # 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 }