View Raw # CCLS Games CLI Tool
# Complete Version with Setup, Search, Get functionality, and Logging
# Configuration
$baseUrl = "https://games.ccls.icu"
$cliApiUrl = "$baseUrl/CLI"
$settingsFolder = ".\settings"
$credentialsFile = "$settingsFolder\credentials.dat"
$settingsFile = "$settingsFolder\settings.json" # Using JSON instead of INI for simplicity
$logsFolder = ".\logs" # Logs folder path
# Script-level variables for progress display 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 <download_url> <output_file> <game_name> <game_id> [<total_size_bytes>]")
return 1
try:
download_url = sys.argv[1]
output_file = sys.argv[2]
game_name = sys.argv[3]
game_id = sys.argv[4]
# 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
}