Skip to content

Instantly share code, notes, and snippets.

@darkliquid
Last active February 10, 2025 11:25
Show Gist options
  • Save darkliquid/69db089694756c07b322102227daaff7 to your computer and use it in GitHub Desktop.
Save darkliquid/69db089694756c07b322102227daaff7 to your computer and use it in GitHub Desktop.
# DLCs specifies how we want to handle DLCs when updating games.
enum DLCs {
All # Update all DLCs
Installed # Only update installed DLCs
None # Don't update any DLCs
}
# legendaryUpdateCustomisations is a custom object that we use to store update settings
# to the update process for a specific app.
class legendaryUpdateCustomisations {
[string[]] $Except
[string[]] $Only
[string[]] $Tags
[DLCs] $DLCs
legendaryUpdateCustomisations([string]$file) {
$data = ConvertFrom-Json (Get-Content -Raw $file)
$this.Except = $data.Except
$this.Only = $data.Only
$this.Tags = $data.Tags
switch ($data.DLCs) {
'All' {
$this.DLCs = [DLCs]::All
break
}
'Installed' {
$this.DLCs = [DLCs]::Installed
break
}
'None' {
$this.DLCs = [DLCs]::None
break
}
default {
Write-Warning "Invalid DLCs value $($data.DLCs), using Installed"
$this.DLCs = [DLCs]::Installed
break
}
}
}
}
function log {
if ($VerbosePreference -eq 'continue') {
Write-Information "$($args -join ' ')" -InformationAction Continue
}
}
function humanReadableSize {
param(
[Parameter(ValueFromPipeline = $true)][ValidateNotNullOrEmpty()][float]$number
)
$sizes = @('KB', 'MB', 'GB', 'TB', 'PB')
for ($x = 0; $x -lt $sizes.count; $x++) {
# PS magically coerces the a number with its byte unit
# in a string to a numeric byte count, so we can easily
# compare the number to a byte unit.
if ($number -lt [int64]"1$($sizes[$x])") {
if ($x -eq 0) {
return "$number B"
}
else {
$num = $number / [int64]"1$($sizes[$x-1])"
$num = '{0:N2}' -f $num
return "$num $($sizes[$x-1])"
}
}
}
}
# GetLegendaryFiles gets a list of files for a specific app from legendary.
function getLegendaryFiles {
param(
[Parameter(Mandatory)][string]$appName
)
# Get the list of files for the app, and add annotations for excluded, only and tagged files.
$files = legendary list-files $appName --csv 2>$errOutput | ConvertFrom-Csv
if (![string]::IsNullOrEmpty($errOutput)) {
log $errOutput
}
$files | ForEach-Object {
$_ | Add-Member -Force -MemberType NoteProperty -Name 'exclude' -Value $false
$_ | Add-Member -Force -MemberType NoteProperty -Name 'only' -Value $false
$_ | Add-Member -Force -MemberType NoteProperty -Name 'tagged' -Value $false
}
return $files
}
function updateLegendaryApp {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'low')]
param(
[Parameter(Mandatory)]$app,
[Parameter][string[]]$ExceptFiles,
[Parameter][string[]]$OnlyFiles,
[Parameter][string[]]$OnlyTags,
[Parameter][DLCs]$DLCs
)
$appName = $app.'App name'
$appTitle = $app.'App title'
if ([string]::IsNullOrEmpty($app.'App title')) {
$appTitle = $appName
}
# Build up our parameter list for the legendary command.
$params = @('-y')
# DLCs
$prompt = "Update $($appTitle)"
switch ($DLCs) {
All {
$params += '--with-dlcs'
$prompt += ', including all missing and installed DLCs'
break
}
None {
$params += '--skip-dlcs'
$prompt += ', ignoring all DLCs'
break
}
default {
$prompt += ', including all installed DLCs'
break
}
}
# Onlys
if ($OnlyFiles.Count -gt 0) {
log "[$($appTitle)] Only updating the following files (if they match the selected install tags):"
$OnlyFiles | ForEach-Object {
log "[$($appTitle)] $($_)"
$params += '--prefix'
$params += $_
}
}
# Except
if ($ExceptFiles.Count -gt 0) {
log "[$($appTitle)] Excluding the following files:"
$ExceptFiles | ForEach-Object {
log "[$($appTitle)] $($_)"
$params += '--exclude'
$params += $_
}
}
# Tags
if ($OnlyTags.Count -gt 0) {
log "[$($appTitle)] Only updating for the following install tags:"
$OnlyTags | ForEach-Object {
log "[$($appTitle)] $($_)"
$params += '--install-tag'
$params += $_
}
}
# Confirmation
if ($PSCmdlet.ShouldProcess($prompt, $appTitle, 'Update')) {
# Run the legendary command to update the app.
legendary install $appName --update-only @params 2>$errOutput
if (![string]::IsNullOrEmpty($errOutput)) {
log $errOutput
}
}
}
# Get-LegendaryUpdates gets a list of apps that have updates available from legendary.
function Get-LegendaryUpdates {
$updates = legendary list-installed --check-updates --csv 2>$errOutput
if (![string]::IsNullOrEmpty($errOutput)) {
log $errOutput
}
return $updates | ConvertFrom-Csv | Where-Object -Property 'Update available' -eq 'True'
}
# applyLegendaryCustomisations loads update settings for a specific app from an update settings file and
# applies them to the legendary command invocation
function applyLegendaryCustomisations($app) {
$appName = $app.'App name'
$appTitle = $app.'App title'
if ([string]::IsNullOrEmpty($app.'App title')) {
$appTitle = $appName
}
# Determine the path to our update settings json for the specific app.
$updatesSettingsFile = "$($env:APPDATA)\Legendary\$($appName)\updates.json"
# If no file exists, return.
if (!(Test-Path $updatesSettingsFile)) {
log "[$($appTitle)] No update settings file found at $($updatesSettingsFile)"
return @{}
}
log "[$($appTitle)] Processing update settings file $($updatesSettingsFile)"
$settings = [legendaryUpdateCustomisations]::new($updatesSettingsFile)
if (!$settings) {
Write-Error "[$($appTitle)] update settings file is corrupt or invalid. Continue without update settings?" -ErrorAction Inquire
}
if ($settings.Only.Count -gt 0 -and $settings.Except.Count -gt 0) {
Write-Warning "[$($appTitle)] Only and Except are mutally exclusive, ignoring Except."
$settings.Except = @()
}
# Get the list of files for the app and annotate each file so we
# can filter them based on the update settings.
$appFiles = getLegendaryFiles($appName)
$tags = @()
$appFiles | ForEach-Object {
$file = $_
# Mark file as excludes if it's path matches any of the Except patterns.
foreach ($pattern in $settings.Except) {
if ($_.path -match $pattern) {
$file.exclude = $true
break
}
}
foreach ($pattern in $settings.Only) {
if ($file.path -match $pattern) {
$file.only = $true
break
}
}
foreach ($pattern in $settings.Tags) {
foreach ($tag in $file.install_tags) {
if ($tag -match $pattern) {
$file.tagged = $true
$tags += $tag
}
}
}
}
$exceptFiles = ($appFiles | Where-Object -Property exclude -EQ $true)
$onlyFiles = ($appFiles | Where-Object -Property only -EQ $true)
$taggedFiles = ($appFiles | Where-Object -Property tagged -EQ $true)
# If the verbose flag is passed, then show estimated maximum download savings/sizes.
if ($VerbosePreference -eq 'continue') {
if ($exceptFiles.Count -gt 0) {
$totalSize = ($exceptFiles | Measure-Object -Property size -Sum).Sum
log "[$($appTitle)] Excluding $($exceptFiles.Count) files totalling $($totalSize | humanReadableSize) (max)."
}
if ($onlyFiles.Count -gt 0) {
$totalSize = ($onlyFiles | Measure-Object -Property size -Sum).Sum
# If we've also specified tags, then it's possible that some of the only files are excluded by the tag filter.
if ($taggedFiles.Count -gt 0) {
$totalSize = ($onlyFiles | Where-Object -Property tagged -EQ $true | Measure-Object -Property size -Sum).Sum
}
log "[$($appTitle)] Only downloading $($exceptFiles.Count) files totalling $($totalSize | humanReadableSize) (max)."
}
if ($taggedFiles.Count -gt 0 -and $onlyFiles.Count -eq 0) {
$totalSize = ($taggedFiles | Measure-Object -Property size -Sum).Sum
log "[$($appTitle)] Only downloading files with the following tags $($tags -join ', ') totalling $($totalSize | humanReadableSize) (max)."
}
}
return @{
ExceptFiles = ($exceptFiles | ForEach-Object { $_.path })
OnlyFiles = ($onlyFiles | ForEach-Object { $_.path })
OnlyTags = ($tags | Sort-Object -Unique)
DLCs = $settings.DLCs
}
}
# Update-LegendaryApps updates all apps that have updates available from legendary.
function Update-LegendaryApps {
<#
.SYNOPSIS
Updates all installed legendary apps.
.DESCRIPTION
Finds apps installed by legendary that need updating and updates them.
Apps can have additional settings applied to them by creating an updates.json file in the
in the $env:APPDATA\Legendary\$appName directory, where $appName is the internal name of the app.
This file should contain a JSON object with the following properties:
- Only: An array of regex patterns to match files to be downloaded. Only files matching one or more patterns will be downloaded.
- Except: An array of regex patterns to match files to be downloaded. If a pattern matches, then that file will be excluded from the update.
- Tags: An array of regex patterns to match against the install tags of the app. If a pattern matches, then only files with that tag will be updated.
- DLCs: A value that determines how to handle DLCs when updating the app. Can be one of the following values (default is Installed):
- All: Update all DLCs and install any missing ones
- Installed: Only update installed DLCs
- None: Don't update any DLCs
.NOTES
To discover the internal name of an app, you can use the legendary list-installed command
or pass the -Verbose flag to this function which will show the file path it is using to
find the customisation file.
.PARAMETER AppName
Allows you to specify a regex to filter the apps by application name (the internal name).
.PARAMETER AppTitle
Allows you to specify a regex to filter the apps by application title (the friendly name).
.PARAMETER NoConfirm
Skips asking for confirmation and just does it.
.LINK
https://github.com/derrod/legendary
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'low')]
param(
[Parameter][string]$AppName,
[Parameter][string]$AppTitle
)
$updates = Get-LegendaryUpdates
if (![string]::IsNullOrEmpty($AppName)) {
$updates = $updates | Where-Object -Property 'App name' -Match $AppName
}
if (![string]::IsNullOrEmpty($AppTitle)) {
$updates = $updates | Where-Object -Property 'App title' -Match $AppTitle
}
if ($updates.Count -eq 0) {
log 'No apps with updates available'
return
}
log "Found $($updates.Count) apps with updates available"
$updates | ForEach-Object {
$name = $_.'App title'
if ([string]::IsNullOrEmpty($name)) {
$name = $_.'App name'
}
log " $($name)"
}
foreach ($update in $updates) {
$splat = applyLegendaryCustomisations $update
updateLegendaryApp $update @splat
}
}
# Export our functions so they can be used in other scripts.
Export-ModuleMember -Function Update-LegendaryApps
Export-ModuleMember -Function Get-LegendaryUpdates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment