pubg/update/get_matches.ps1
2025-04-15 17:12:33 +02:00

429 lines
No EOL
23 KiB
PowerShell

# --- Script Setup ---
Start-Transcript -Path '/var/log/dtch/get_matches.log' -Append
Write-Output "Starting get_matches script at $(Get-Date)"
Write-Output "Running from: $(Get-Location)"
# Determine script root directory reliably
if ($PSScriptRoot) {
$scriptRoot = $PSScriptRoot
} else {
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
}
Write-Output "Script root identified as: $scriptRoot"
# Define paths using Join-Path
$includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$configPath = Join-Path -Path $scriptRoot -ChildPath "..\config"
$dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
$matchesPath = Join-Path -Path $dataPath -ChildPath "matches"
$matchesArchivePath = Join-Path -Path $matchesPath -ChildPath "archive"
$playerDataJsonPath = Join-Path -Path $dataPath -ChildPath "player_data.json"
$playerMatchesJsonPath = Join-Path -Path $dataPath -ChildPath "player_matches.json"
$cachedMatchesJsonPath = Join-Path -Path $dataPath -ChildPath "cached_matches.json"
# Ensure required directories exist
@( $dataPath, $matchesPath, $matchesArchivePath ) | ForEach-Object {
if (-not (Test-Path -Path $_ -PathType Container)) {
Write-Warning "Directory not found at '$_'. Attempting to create."
try {
New-Item -Path $_ -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Output "Successfully created directory: $_"
} catch {
Write-Error "Failed to create directory '$_'. Please check permissions. Error: $($_.Exception.Message)"
Stop-Transcript
exit 1
}
}
}
# --- Locking ---
$lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
Stop-Transcript
exit 1
}
. $lockFilePath
New-Lock -by "get_matches" -ErrorAction Stop # Stop if locking fails
# --- Main Logic in Try/Finally for Lock Removal ---
try {
# --- Configuration Loading ---
$apiKey = $null
$clanMembers = @() # Renamed from $players for clarity
# Load API Key from config.php
$phpConfigPath = Join-Path -Path $configPath -ChildPath "config.php"
if (Test-Path -Path $phpConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $phpConfigPath -Raw -ErrorAction Stop
# Corrected regex for apiKey
if ($fileContent -match '^\s*\$apiKey\s*=\s*''([^'']+)''') {
$apiKey = $matches[1]
Write-Output "API Key loaded successfully."
} else {
Write-Warning "API Key pattern not found in '$phpConfigPath'."
}
} catch {
Write-Warning "Failed to read '$phpConfigPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Config file not found at '$phpConfigPath'."
}
if (-not $apiKey) {
Write-Error "API Key could not be loaded. Cannot proceed without API Key."
throw "Missing API Key" # Throw to trigger finally block
}
# Load Clan Members from clanmembers.json
$clanMembersJsonPath = Join-Path -Path $configPath -ChildPath "clanmembers.json"
if (Test-Path -Path $clanMembersJsonPath -PathType Leaf) {
try {
$clanMembersData = Get-Content -Path $clanMembersJsonPath -Raw | ConvertFrom-Json -ErrorAction Stop
if ($clanMembersData -is [PSCustomObject] -and $clanMembersData.PSObject.Properties.Name -contains 'clanMembers' -and $clanMembersData.clanMembers -is [array]) {
$clanMembers = $clanMembersData.clanMembers
Write-Output "Clan members loaded successfully. Count: $($clanMembers.Count)"
} else {
Write-Warning "Invalid structure in '$clanMembersJsonPath'. Expected an object with a 'clanMembers' array."
}
} catch {
Write-Warning "Failed to read or parse '$clanMembersJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Clan members file not found at '$clanMembersJsonPath'."
}
if ($clanMembers.Count -eq 0) {
Write-Warning "No clan members loaded. Proceeding, but cached matches might be incomplete."
# Decide if this is a fatal error or not
}
# --- Helper Function for API Calls (Copied from update_clan_members.ps1) ---
function Invoke-PubgApi {
param(
[Parameter(Mandatory=$true)]
[string]$Uri,
[Parameter(Mandatory=$true)]
[hashtable]$Headers,
[int]$RetryCount = 1,
[int]$RetryDelaySeconds = 61
)
for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) {
try {
Write-Verbose "Attempting API call (Attempt $($attempt)): $Uri"
$response = Invoke-RestMethod -Uri $Uri -Method GET -Headers $Headers -ErrorAction Stop
if ($null -ne $response) { Write-Verbose "API call successful."; return $response }
else { Write-Warning "API call to $Uri returned null or empty response."; return $null }
} catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $($attempt)). Status: $statusCode. Error: $errorMessage"
if ($attempt -le $RetryCount -and $statusCode -eq 429) {
Write-Warning "Rate limit hit. Sleeping for $RetryDelaySeconds seconds before retry..."
Start-Sleep -Seconds $RetryDelaySeconds
} elseif ($attempt -gt $RetryCount) { Write-Error "API call failed after $($attempt) attempts. URI: $Uri. Last Error: $errorMessage"; return $null }
else { Write-Error "Non-retryable API error. URI: $Uri. Error: $errorMessage"; return $null }
}
}
return $null
}
# --- Load Player Data (IDs and Match Lists) ---
$playerData = $null
if (Test-Path -Path $playerDataJsonPath -PathType Leaf) {
try {
# Assuming player_data.json contains an object with a 'data' array property
$playerDataWrapper = Get-Content -Path $playerDataJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -ne $playerDataWrapper -and $playerDataWrapper.PSObject.Properties.Name -contains 'data' -and $playerDataWrapper.data -is [array]) {
$playerData = $playerDataWrapper.data
Write-Output "Successfully loaded player data. Count: $($playerData.Count)"
} else {
Write-Error "Invalid structure in '$playerDataJsonPath'. Expected object with 'data' array. Cannot proceed."
throw "Invalid player data structure."
}
} catch {
Write-Error "Error reading '$playerDataJsonPath': $($_.Exception.Message). Cannot proceed."
throw $_
}
} else {
Write-Error "Player data file not found at '$playerDataJsonPath'. Run update_clan_members.ps1 first. Cannot proceed."
throw "Missing player data file."
}
# --- Fetch and Process Matches for Each Player ---
Write-Output "Fetching and processing matches for $($playerData.Count) players..."
$allPlayerMatchDetails = @() # Store processed match details for all players
$apiHeaders = @{
'accept' = 'application/vnd.api+json'
'Authorization' = "Bearer $apiKey"
}
$matchesFetched = 0
$matchesCached = 0
foreach ($player in $playerData) {
# Validate player structure
if ($null -eq $player.attributes -or $null -eq $player.attributes.name -or $null -eq $player.relationships.matches.data) {
Write-Warning "Skipping player due to missing attributes, name, or match data: $($player | Out-String)"
continue
}
$playerName = $player.attributes.name
$playerMatchIds = $player.relationships.matches.data.id
Write-Output "Processing player: $playerName ($($playerMatchIds.Count) recent matches)"
$currentPlayerMatches = @()
foreach ($matchId in $playerMatchIds) {
if (-not $matchId) { Write-Warning "Skipping null/empty match ID for player $playerName."; continue }
Write-Verbose "Getting match details for $playerName, Match ID: $matchId"
$matchJsonPath = Join-Path -Path $matchesPath -ChildPath "$matchId.json"
$matchStats = $null
# Check cache first
if (Test-Path -Path $matchJsonPath -PathType Leaf) {
Write-Verbose "Getting $matchId from cache."
try {
$matchStats = Get-Content -Path $matchJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -eq $matchStats) { Write-Warning "Failed to parse cached match file: $matchJsonPath" }
else { $matchesCached++ }
} catch {
Write-Warning "Error reading cached match file '$matchJsonPath': $($_.Exception.Message)"
}
}
# Fetch from API if not cached or cache failed
if ($null -eq $matchStats) {
$apiUrl = "https://api.pubg.com/shards/steam/matches/$matchId"
Write-Output "Fetching match $matchId from API..."
$matchStats = Invoke-PubgApi -Uri $apiUrl -Headers $apiHeaders
if ($null -ne $matchStats) {
$matchesFetched++
# Sort included participants by winPlace before saving (optional, but done in original)
try {
if ($null -ne $matchStats.included -and $matchStats.included -is [array]) {
$matchStats.included = $matchStats.included | Sort-Object -Property { $_.attributes.stats.winPlace } -ErrorAction SilentlyContinue
}
} catch { Write-Warning "Could not sort 'included' array for match $matchId." }
# Save to cache
try {
$matchStats | ConvertTo-Json -Depth 100 | Out-File -FilePath $matchJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Verbose "Saved match $matchId to cache."
} catch {
Write-Warning "Failed to save match $matchId to cache '$matchJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Failed to fetch match $matchId from API for player $playerName."
continue # Skip this match if API fetch failed
}
}
# Process the retrieved/cached match stats
if ($null -ne $matchStats -and $null -ne $matchStats.data -and $null -ne $matchStats.included) {
# Find the specific player's stats within the 'included' array
$playerSpecificStats = $null
if ($matchStats.included -is [array]) {
$playerSpecificStats = $matchStats.included |
Where-Object { $_.type -eq 'participant' -and ($_.attributes.stats.name -eq $playerName) } |
Select-Object -First 1 -ExpandProperty attributes | Select-Object -ExpandProperty stats
}
# Find telemetry URL
$telemetryUrl = $null
if ($matchStats.included -is [array]) {
$telemetryAsset = $matchStats.included | Where-Object { $_.type -eq 'asset' -and $_.attributes.name -eq 'telemetry' } | Select-Object -First 1
if ($telemetryAsset) { $telemetryUrl = $telemetryAsset.attributes.URL }
}
if ($null -ne $playerSpecificStats) {
$currentPlayerMatches += [PSCustomObject]@{
stats = $playerSpecificStats # Just the stats object for this player
matchType = $matchStats.data.attributes.matchType
gameMode = $matchStats.data.attributes.gameMode
createdAt = $matchStats.data.attributes.createdAt
mapName = $matchStats.data.attributes.mapName
telemetry_url = $telemetryUrl
id = $matchStats.data.id
}
} else {
Write-Warning "Could not find stats for player $playerName within match $matchId data."
}
} else {
Write-Warning "Match data structure for $matchId is invalid or incomplete after retrieval/caching."
}
} # End foreach matchId
# Add the processed matches for the current player to the main list
$allPlayerMatchDetails += [PSCustomObject]@{
playername = $playerName
player_matches = $currentPlayerMatches # Array of processed match objects for this player
}
Write-Output "Finished processing matches for $playerName. Found $($currentPlayerMatches.Count) valid entries."
} # End foreach player
Write-Output "Finished fetching/processing all matches. API Fetches: $matchesFetched, Cache Hits: $matchesCached."
# --- Compare with Old Data & Identify New Wins/Losses ---
Write-Output "Comparing current matches with old data..."
$oldPlayerMatchData = $null
$newWinMatchesList = @()
$newLossMatchesList = @()
if (Test-Path -Path $playerMatchesJsonPath -PathType Leaf) {
try {
$oldPlayerMatchData = Get-Content -Path $playerMatchesJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -eq $oldPlayerMatchData) {
Write-Warning "Failed to parse old player matches file: $playerMatchesJsonPath. Cannot compare for new wins/losses."
} else {
Write-Output "Successfully loaded old player matches data for comparison."
# Extract all current match IDs and old match IDs safely
$currentMatchIds = ($allPlayerMatchDetails.player_matches.id | Select-Object -Unique)
$oldMatchIds = $null
if ($oldPlayerMatchData -is [array]) {
$oldMatchIds = ($oldPlayerMatchData.player_matches.id | Select-Object -Unique)
} else {
Write-Warning "Old player matches data is not in the expected array format."
}
if ($null -ne $oldMatchIds) {
# Compare IDs to find newly added matches
$newMatchIds = (Compare-Object -ReferenceObject $oldMatchIds -DifferenceObject $currentMatchIds | Where-Object { $_.SideIndicator -eq '=>' }).InputObject | Select-Object -Unique
Write-Output "Found $($newMatchIds.Count) new match IDs."
# Identify new wins and losses among the new matches
$newMatchesDetails = $allPlayerMatchDetails.player_matches | Where-Object { $newMatchIds -contains $_.id }
$newWinMatchesList = ($newMatchesDetails | Where-Object { $_.stats.winPlace -eq 1 }).id | Select-Object -Unique
$newLossMatchesList = ($newMatchesDetails | Where-Object { $_.stats.winPlace -ne 1 }).id | Select-Object -Unique
Write-Output "Identified $($newWinMatchesList.Count) new wins and $($newLossMatchesList.Count) new losses."
# Combine with potentially existing lists from old data (if format was correct)
$oldWinMatches = $oldPlayerMatchData | Where-Object { $_.PSObject.Properties.Name -eq 'new_win_matches' } | Select-Object -ExpandProperty new_win_matches
$oldLossMatches = $oldPlayerMatchData | Where-Object { $_.PSObject.Properties.Name -eq 'new_loss_matches' } | Select-Object -ExpandProperty new_loss_matches
if ($oldWinMatches -is [array]) { $newWinMatchesList = ($oldWinMatches + $newWinMatchesList) | Select-Object -Unique }
if ($oldLossMatches -is [array]) { $newLossMatchesList = ($oldLossMatches + $newLossMatchesList) | Select-Object -Unique }
}
}
} catch {
Write-Warning "Error reading or processing old player matches file '$playerMatchesJsonPath': $($_.Exception.Message)"
}
} else {
Write-Output "Old player matches file not found. Cannot compare for new wins/losses."
# If no old file, all current wins/losses are "new" for the first run
$newWinMatchesList = ($allPlayerMatchDetails.player_matches | Where-Object { $_.stats.winPlace -eq 1 }).id | Select-Object -Unique
$newLossMatchesList = ($allPlayerMatchDetails.player_matches | Where-Object { $_.stats.winPlace -ne 1 }).id | Select-Object -Unique
Write-Output "Treating all current wins ($($newWinMatchesList.Count)) and losses ($($newLossMatchesList.Count)) as new."
}
# Add the lists of new wins/losses to the data structure
$allPlayerMatchDetails += [PSCustomObject]@{ new_win_matches = $newWinMatchesList }
$allPlayerMatchDetails += [PSCustomObject]@{ new_loss_matches = $newLossMatchesList }
# Add update timestamp
$currentDateTime = Get-Date
$currentTimezone = (Get-TimeZone).Id
$formattedString = "$currentDateTime - Time Zone: $currentTimezone"
$allPlayerMatchDetails += [PSCustomObject]@{ updated = $formattedString }
Write-Output "Added update timestamp and new win/loss lists."
# --- Save Updated Player Matches Data ---
try {
$allPlayerMatchDetails | ConvertTo-Json -Depth 100 | Out-File -FilePath $playerMatchesJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Updated player matches data saved to '$playerMatchesJsonPath'"
} catch {
Write-Error "Failed to save updated player matches data to '$playerMatchesJsonPath'. Error: $($_.Exception.Message)"
}
# --- Clean Old Match Files & Create Cached Summary ---
Write-Output "Cleaning old match files and creating cached summary..."
$cachedMatches = @()
$archivedMatchFiles = 0
$processedMatchFiles = 0
$monthsToKeepMatches = -3 # How long to keep individual match files
try {
$matchFiles = Get-ChildItem -Path $matchesPath -Filter *.json -File -ErrorAction SilentlyContinue
if ($matchFiles) {
$archiveThreshold = (Get-Date).AddMonths($monthsToKeepMatches)
Write-Output "Archiving match files older than: $archiveThreshold"
foreach ($file in $matchFiles) {
$processedMatchFiles++
try {
$fileContent = Get-Content -Path $file.FullName | ConvertFrom-Json -Depth 100 -ErrorAction Stop
# Validate essential structure
if ($null -eq $fileContent -or $null -eq $fileContent.data.attributes.createdAt -or $null -eq $fileContent.included) {
Write-Warning "Skipping invalid match file: $($file.Name)"
continue
}
$matchFileDate = $null
try { $matchFileDate = [datetime]$fileContent.data.attributes.createdAt } catch { Write-Warning "Could not parse date in match file $($file.Name)" }
# Archive old files
if ($null -ne $matchFileDate -and $matchFileDate -lt $archiveThreshold) {
Write-Verbose "Archiving match file: $($file.Name)"
Move-Item -Path $file.FullName -Destination $matchesArchivePath -Force -ErrorAction SilentlyContinue
if ($?) { $archivedMatchFiles++ }
else { Write-Warning "Failed to archive match file: $($file.Name)" }
} else {
# Process file for cached summary if it's recent enough
$matchAttributes = $fileContent.data.attributes
$matchId = $fileContent.data.id
# Find stats for clan members within this match
$clanMemberStatsInMatch = @()
if ($fileContent.included -is [array]) {
$clanMemberStatsInMatch = $fileContent.included |
Where-Object { $_.type -eq 'participant' -and $clanMembers -contains $_.attributes.stats.name } |
Select-Object -ExpandProperty attributes | Select-Object -ExpandProperty stats
}
if ($clanMemberStatsInMatch.Count -gt 0) {
$cachedMatches += [PSCustomObject]@{
matchType = $matchAttributes.matchType
gameMode = $matchAttributes.gameMode
createdAt = $matchAttributes.createdAt
mapName = $matchAttributes.mapName
id = $matchId
stats = @($clanMemberStatsInMatch) # Ensure stats is always an array
}
}
}
} catch {
Write-Warning "Error processing match file '$($file.Name)': $($_.Exception.Message)"
}
} # End foreach file
Write-Output "Processed $processedMatchFiles match files. Archived: $archivedMatchFiles."
# Save the cached summary object
if ($cachedMatches.Count -gt 0) {
try {
$cachedMatches | Sort-Object createdAt -Descending | ConvertTo-Json -Depth 100 | Out-File -FilePath $cachedMatchesJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Cached matches summary saved to '$cachedMatchesJsonPath'. Count: $($cachedMatches.Count)"
} catch {
Write-Error "Failed to save cached matches summary to '$cachedMatchesJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Output "No recent matches found containing clan members for cached summary."
# Optionally clear or save an empty array to the cache file
# @() | ConvertTo-Json | Out-File -FilePath $cachedMatchesJsonPath -Encoding UTF8
}
} else {
Write-Output "No match files found in '$matchesPath' to process or clean."
}
} catch {
Write-Warning "Error during match file cleaning/caching process: $($_.Exception.Message)"
}
} # End Main Try Block
finally {
# --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock # Ensure lock is always removed
Stop-Transcript
}