Created
November 15, 2024 20:47
-
-
Save GodSaveEarth/9b8d574d0f5ab789aebdfdb2db59b8df to your computer and use it in GitHub Desktop.
PowerShell script to download hi-res photos from Apple iCloud sharedalbum
@augusthaering please update the post markdown
here is the updated post in correct markdown
I added a custom file location variable to make it easier to manage the location of the downloads
Insert your local path and your icloud link!
# PowerShell script to download hi-res photos from Apple iCloud shared album with a custom save path
# The script saves JSONs and TXT files to the specified directory for debugging purposes
# Please remove them manually later
# INSERT custom save path
$SavePath = "C:\Pfad\zu\deinem\Ordner"
# Ensure the directory exists
if (!(Test-Path -Path $SavePath)) {
New-Item -ItemType Directory -Path $SavePath
}
# INSERT ICLOUD LINK
$target_url = "https://www.icloud.com/sharedalbum/#B1e5ZhsdsdsdN2vMlt0Z"
# Required function to split large arrays into chunks
function Create-Batch {
[CmdletBinding()]
param (
[Int]$Size,
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]$InputObject
)
begin {
$Batch = [Collections.Generic.List[object]]::new()
}
process {
if ($Size -and $Batch.get_Count() -ge $Size) {
,$Batch
$Batch = [Collections.Generic.List[object]]::new()
}
$Batch.Add($_)
}
End {
if ($Batch.get_Count()) { , $Batch }
}
}
# Script begin
$folder_id = $target_url.Substring($target_url.IndexOf("#") + 1)
# First, make a fake request to get a valid download host
$webstream_default_url = "https://sharedstreams.icloud.com/$($folder_id)/sharedstreams/webstream"
try {
Invoke-RestMethod -Uri $webstream_default_url -Method 'Post' -Body '{"streamCtag":null}'
} catch {
# If HTTP code is 330 redirect, extract valid host ID from response
if ($_.Exception.Response.StatusCode.value__ -eq 330) {
if ($_.ErrorDetails.Message -match 'X-Apple-MMe-Host":"(.+)"') {
$url_host = $Matches[1]
Write-Output "Received server name: $($url_host)"
}
} else {
$_.Exception.Response
exit
}
}
# Generate download URLs and filenames for JSONs
$webstream_url = "https://$($url_host)/$($folder_id)/sharedstreams/webstream"
$webstream_file = "$($SavePath)\webstream.json"
$webassets_url = "https://$($url_host)/$($folder_id)/sharedstreams/webasseturls"
$webassets_file = "$($SavePath)\webasseturls.json"
# Download webstream_url if not already downloaded
if (!(Test-Path $webstream_file -PathType Leaf)) {
Write-Output "Get $($webstream_url)"
Write-Output "JSON save to $($webstream_file)"
Invoke-RestMethod -Uri $webstream_url -Method 'Post' -Body '{"streamCtag":null}' -OutFile $webstream_file
}
# Read downloaded file into JSON
Write-Output "JSON load $($webstream_file)"
$webstream_obj = Get-Content $webstream_file | ConvertFrom-Json
Write-Output "Total photos: $($webstream_obj.photos.Count)"
# Iterate through all photos in batches (limit to first 5000 objects for debug)
$webstream_obj.photos | Select-Object -First 5000 | Create-Batch -Size 500 | ForEach-Object {
Write-Output "Processing batch of: $($_.Count)"
# Create request body for /webassets (expects POST request with {"photoGuids":["...", "..."]})
$body = @{ photoGuids = $_ | Select -ExpandProperty photoGuid } | ConvertTo-Json
# Download the list
Invoke-RestMethod -Uri $webassets_url -Method 'Post' -Body $body -OutFile $webassets_file
$webassets_obj = Get-Content $webassets_file | ConvertFrom-Json
Write-Output "Assets for this batch found: $($webassets_obj.items.psobject.Properties.Value.Count)"
# Filter files to download (only largest files per photoGuid)
$largest_checksums = @()
$_ | ForEach-Object {
$largest_file = $_.derivatives.PSObject.Properties | Select-Object -ExpandProperty Value | Sort-Object {[int]$_.filesize} -Descending | Select-Object -First 1
$largest_checksums += $largest_file.checksum
}
$i=0
$webassets_obj.items.PSObject.Properties | ForEach-Object {
Write-Output "Processing file: $($_.Name)"
# Skip low-resolution images
if ($largest_checksums -NotContains $_.Name.ToString()) {
Write-Output "Skipping low-res image"
return
}
$url = (-join("https://", $_.Value.url_location, $_.Value.url_path))
# Save URL to file
$url | Out-File -FilePath "$($SavePath)\urls.txt" -Append
Write-Output "Downloading..."
# Download file with headers
$Response = Invoke-WebRequest $Url
# Extract original filename
$ContentDisposition = $Response.Headers.'Content-Disposition'
$i1 = $ContentDisposition.IndexOf('filename="')+10
$i2 = $ContentDisposition.IndexOf('"', $i1)
$FileName = $ContentDisposition.Substring($i1, $i2-$i1)
$FilePath = "$($SavePath)\$($FileName)"
# Save file and cleanup
[IO.File]::WriteAllBytes($FilePath, $Response.Content)
Remove-Variable Response -Force
[GC]::Collect()
Write-Output "Saved: $($FileName)"
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
works 27.11.20204, thank you so much!!!
I added custom file location variable to make it more easy to manage the location of the downloads:
`
``
PowerShell script to download hi-res photos from
Apple iCloud shared album with a custom save path
The script saves jsons and txt to the specified directory for debug reasons
Please remove them manually later
Define custom save path
$SavePath = "C:\Pfad\zu\deinem\Ordner"
Ensure the directory exists
if (!(Test-Path -Path $SavePath)) {
New-Item -ItemType Directory -Path $SavePath
}
Define the target URL
$target_url = "https://www.icloud.com/sharedalbum/#B1e5ZhN2vMlt0Z"
Required function to split the large arrays to chunks
function Create-Batch {
[CmdletBinding()]
param (
[Int]$Size,
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]$InputObject
)
begin {
$Batch = [Collections.Generic.List[object]]::new()
}
process {
if ($Size -and $Batch.get_Count() -ge $Size) {
,$Batch
$Batch = [Collections.Generic.List[object]]::new()
}
$Batch.Add($_)
}
End {
if ($Batch.get_Count()) { , $Batch }
}
}
script begin
$folder_id = $target_url.Substring($target_url.IndexOf("#") + 1)
First make a fake request and get a valid download host
$webstream_default_url = "https://sharedstreams.icloud.com/$($folder_id)/sharedstreams/webstream"
try {
Invoke-RestMethod -Uri $webstream_default_url -Method 'Post' -Body '{"streamCtag":null}'
} catch {
# if http code was 330 redirect, we lookup take valid host id from response
if ($.Exception.Response.StatusCode.value__ -eq 330) {
if($.ErrorDetails.Message -match 'X-Apple-MMe-Host":"(.+)"') {
$url_host = $Matches[1]
Write-Output "Received server name: $($url_host)"
}
} else {
$_.Exception.Response
exit
}
}
generate download urls and filename for jsons
i download jsons to files first for debug purposes
download webstream_url if not before
webstream_url is main jsons with all photos data
if(!(Test-Path $webstream_file -PathType Leaf)) {
Write-Output "Get $($webstream_url)"
Write-Output "Json save to $($webstream_file)"
Invoke-RestMethod -Uri $webstream_url -Method 'Post' -Body '{"streamCtag":null}' -OutFile $webstream_file
}
read downloaded file to json
Write-Output "Json load $($webstream_file)"
$webstream_obj = Get-Content $webstream_file | ConvertFrom-Json
Write-Output "Total photos: $($webstream_obj.photos.Count)"
Iterate through all photos in batches
for debug needs you may want tos limit to -First x objects
$webstream_obj.photos | Select-Object -First 5000 | Create-Batch -Size 500 | ForEach-Object {
Write-Output "Process batch of: $($_.Count)"
}
`