281 lines
12 KiB
PowerShell
281 lines
12 KiB
PowerShell
<#
|
|
.DESCRIPTION
|
|
This script attempts to authenticate to OneDrive as client https://photos.onedrive.com
|
|
(which has permissions to download LivePhotos) and downloads all LivePhotos at a given
|
|
location within your personal OneDrive.
|
|
.PARAMETER SaveTo
|
|
Target path where to save live photos.
|
|
.PARAMETER PathToScan
|
|
DOS-Style path on your OneDrive that should be scanned. Most likely '\Pictures\Camera Roll' or any other shared Camera Roll folder.
|
|
|
|
.EXAMPLE
|
|
.\Download-ODLivePhotos.ps1 'C:\Live Photos'
|
|
|
|
.NOTES
|
|
Author: Petr Vyskocil
|
|
|
|
There is no error checking, so it is recommended to re-run the command on bigger libraries.
|
|
If there are any errors during the download (OneDrive sometimes fails randomly with error "Our
|
|
services aren't available right now. We're working to restore all services as soon as possible.
|
|
Please check back soon."), next run will download the missing files and skip already downloaded
|
|
ones.
|
|
#>
|
|
param (
|
|
[Parameter(Mandatory)]
|
|
[string] $SaveTo,
|
|
[string] $PathToScan = '/Photos/Auto-saved'
|
|
)
|
|
|
|
|
|
function Get-ODPhotosToken
|
|
{
|
|
<#
|
|
.DESCRIPTION
|
|
Connect to OneDrive for authentication with a OneDrive web Photos client id. Adapted from https://github.com/MarcelMeurer/PowerShellGallery-OneDrive to mimic OneDrive Photos web OIDC login.
|
|
Unfortunately using custom ClientId seems impossible - generic OD client IDs are missing the ability to download Live Photos.
|
|
.PARAMETER ClientId
|
|
ClientId of OneDrive Photos web app (073204aa-c1e0-4e66-a200-e5815a0aa93d)
|
|
.PARAMETER Scope
|
|
Comma-separated string defining the authentication scope (https://dev.onedrive.com/auth/msa_oauth.htm). Default: "OneDrive.ReadWrite,offline_access,openid,profile".
|
|
.PARAMETER RedirectURI
|
|
Code authentication requires a correct URI. Must be https://photos.onedrive.com/auth/login.
|
|
|
|
.EXAMPLE
|
|
$access_token=Get-ODPhotosToken
|
|
Connect to OneDrive for authentication and save the token to $access_token
|
|
.NOTES
|
|
Author: Petr Vyskocil
|
|
#>
|
|
PARAM(
|
|
[string]$ClientId = "073204aa-c1e0-4e66-a200-e5815a0aa93d",
|
|
[string]$Scope = "OneDrive.ReadWrite,offline_access,openid,profile",
|
|
[string]$RedirectURI ="https://photos.onedrive.com/auth/login",
|
|
[switch]$DontShowLoginScreen=$false,
|
|
[switch]$LogOut
|
|
)
|
|
$Authentication=""
|
|
|
|
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | out-null
|
|
[Reflection.Assembly]::LoadWithPartialName("System.Drawing") | out-null
|
|
[Reflection.Assembly]::LoadWithPartialName("System.Web") | out-null
|
|
if ($Logout)
|
|
{
|
|
$URIGetAccessToken="https://login.live.com/logout.srf"
|
|
}
|
|
else
|
|
{
|
|
$URIGetAccessToken="https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id="+$ClientId+"&nonce=uv."+(New-Guid).Guid+"&response_mode=form_post&scope="+$Scope+"&response_type=code&redirect_URI="+$RedirectURI
|
|
}
|
|
$form = New-Object Windows.Forms.Form
|
|
if ($DontShowLoginScreen)
|
|
{
|
|
write-debug("Logon screen suppressed by flag -DontShowLoginScreen")
|
|
$form.Opacity = 0.0;
|
|
}
|
|
$form.text = "Authenticate to OneDrive"
|
|
$form.size = New-Object Drawing.size @(700,600)
|
|
$form.Width = 660
|
|
$form.Height = 775
|
|
$web=New-object System.Windows.Forms.WebBrowser
|
|
$web.IsWebBrowserContextMenuEnabled = $true
|
|
$web.Width = 600
|
|
$web.Height = 700
|
|
$web.Location = "25, 25"
|
|
$web.ScriptErrorsSuppressed = $true
|
|
$DocComplete = {
|
|
if ($web.Url.AbsoluteUri -match "access_token=|error|code=|logout|/auth/login") {$form.Close() } #
|
|
}
|
|
$web.Add_DocumentCompleted($DocComplete)
|
|
$form.Controls.Add($web)
|
|
$web.navigate($URIGetAccessToken)
|
|
$form.showdialog() | out-null
|
|
$Authentication = New-Object PSObject
|
|
# The returned code=XXXX is irrelevant, the actual secrets are sent as cookies:
|
|
$web.Document.Cookie -split ';' | % {
|
|
$cookie = $_ -split '='
|
|
$cookieValue = [uri]::UnescapeDataString($cookie[1])
|
|
$Authentication | Add-Member NoteProperty $cookie[0].Trim() $cookieValue
|
|
}
|
|
|
|
if (-Not $Authentication.'AccessToken-OneDrive.ReadWrite') {
|
|
write-error("Cannot get authentication token. This program does not suport token refresh at the moment. Try again, if you fail a few times, restart and try again.")
|
|
# TODO: Refresh token should be handled here : GET https://photos.onedrive.com/auth/refresh?scope=OneDrive.ReadWrite&refresh_token=
|
|
# Unfortunately it seems that this page hangs up the WebBrowser control, and we need all the cookies transferred to use...
|
|
# Deemed not worth debugging for utility that runs for a short period of time
|
|
return $web
|
|
|
|
}
|
|
|
|
return $Authentication
|
|
}
|
|
|
|
function Download-LivePhotosAuth
|
|
{
|
|
<#
|
|
.DESCRIPTION
|
|
Download all Live Photos from a given OneDrive path
|
|
.PARAMETER AccessToken
|
|
Access token for OneDrive API that has ability to download live photos.
|
|
.PARAMETER SaveTo
|
|
Target path where to save live photos.
|
|
.PARAMETER PathToScan
|
|
DOS-Style path on your OneDrive that should be scanned. Most likely '\Pictures\Camera Roll' or any other shared Camera Roll filder you see.
|
|
.PARAMETER CurrentPath
|
|
Internal, used for recursion
|
|
.PARAMETER ElementId
|
|
Start at folder id (probably only useful internally for recursion too)
|
|
.PARAMETER Uri
|
|
Start processing on this URI (again for recursion)
|
|
|
|
.EXAMPLE
|
|
Download-LivePhotosAuth -AccessToken $token -SaveTo 'C:\LivePhotos' -PathToScan '\Pictures\Camera Roll'
|
|
.NOTES
|
|
Author: Petr Vyskocil
|
|
#>
|
|
PARAM(
|
|
[Parameter(Mandatory=$True)]
|
|
[string]$AccessToken,
|
|
[Parameter(Mandatory=$True)]
|
|
[string]$SaveTo,
|
|
[string]$PathToScan='\',
|
|
[string]$CurrentPath='',
|
|
[string]$ElementId='',
|
|
[string]$Uri=''
|
|
)
|
|
if (!$PathToScan.EndsWith('\')) { $PathToScan = $PathToScan + '\' }
|
|
if (!$PathToScan.StartsWith('\')) { $PathToScan = '\' + $PathToScan }
|
|
if ($Uri -eq '') {
|
|
if ($ElementId -eq '') {
|
|
$CurrentPath='\'
|
|
$Location='root'
|
|
} else {
|
|
$Location='items/' + $ElementId
|
|
}
|
|
$Uri = 'https://api.onedrive.com/v1.0/drive/' + $Location + '/children?%24filter=photo%2FlivePhoto+ne+null+or+folder+ne+null+or+remoteItem+ne+null&select=fileSystemInfo%2Cphoto%2Cid%2Cname%2Csize%2Cfolder%2CremoteItem'
|
|
}
|
|
Write-Output("Calling OneDrive API")
|
|
Write-Output($Uri)
|
|
$WebRequest=Invoke-WebRequest -Method 'GET' -Header @{ Authorization = "BEARER "+$AccessToken} -ErrorAction SilentlyContinue -Uri $Uri
|
|
$Response = ConvertFrom-Json $WebRequest.Content
|
|
$Response.value | % {
|
|
$FolderPath = $CurrentPath + $_.name + '\'
|
|
if ([bool]$_.PSObject.Properties['folder']) {
|
|
if ($FolderPath.StartsWith($PathToScan) -or $PathToScan.StartsWith($FolderPath)) { # We're traversing the target folder or we're getting into it
|
|
Write-Output("Checking folder $($_.id) - $($FolderPath)")
|
|
Download-LivePhotosAuth -AccessToken $AccessToken -SaveTo $SaveTo -PathToScan $PathToScan -CurrentPath $FolderPath -ElementId $_.id
|
|
}
|
|
}
|
|
if ([bool]$_.PSObject.Properties['remoteItem']) {
|
|
if ($FolderPath.StartsWith($PathToScan) -or $PathToScan.StartsWith($FolderPath)) { # We're traversing the target folder or we're getting into it
|
|
Write-Output("Checking shared folder $($_.remoteItem.id) - $($FolderPath)")
|
|
Download-LivePhotosAuth -AccessToken $AccessToken -SaveTo $SaveTo -PathToScan $PathToScan -CurrentPath $FolderPath -ElementId $_.remoteItem.id
|
|
}
|
|
}
|
|
if ([bool]$_.PSObject.Properties['photo']) {
|
|
if ([bool]$_.photo.PSObject.Properties['livePhoto']) {
|
|
if ($CurrentPath.StartsWith($PathToScan)) {
|
|
$TargetPath = $SaveTo + '\' + $CurrentPath.Substring($PathToScan.Length)
|
|
if ( (Test-Path($TargetPath+$_.name)) -and # Target image exists
|
|
(Test-Path($TargetPath+([io.fileinfo]$_.name).basename+'.mov')) -and # Target video exists
|
|
(((Get-Item($TargetPath+$_.name)).Length + (Get-Item($TargetPath+([io.fileinfo]$_.name).basename+'.mov')).Length) -eq $_.size) # size of image and video together is onedrive's size
|
|
) {
|
|
Write-Output "Live photo $($_.id) - $($CurrentPath + $_.name) already exists at $($TargetPath) - skipping."
|
|
} else {
|
|
Write-Output("Detected live photo $($_.id) - $($CurrentPath + $_.name). Saving image/video pair to $($TargetPath)")
|
|
Download-SingleLivePhoto -AccessToken $AccessToken -ElementId $_.id -SaveTo $TargetPath -ExpectedSize $_.size -LastModified $_.fileSystemInfo.lastModifiedDateTime
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ([bool]$Response.PSobject.Properties["@odata.nextLink"])
|
|
{
|
|
write-debug("Getting more elements form service (@odata.nextLink is present)")
|
|
Download-LivePhotosAuth -AccessToken $AccessToken -SaveTo $SaveTo -PathToScan $PathToScan -CurrentPath $CurrentPath -Uri $Response.'@odata.nextLink'
|
|
}
|
|
}
|
|
|
|
function Download-SingleLivePhoto
|
|
{
|
|
<#
|
|
.DESCRIPTION
|
|
Download single LivePhoto given it's ElementId and static data.
|
|
.PARAMETER AccessToken
|
|
Access token for OneDrive API that has ability to download live photos.
|
|
.PARAMETER ElementId
|
|
OneDrive ElementId of a LivePhoto
|
|
.PARAMETER SaveTo
|
|
Target path where to save live photos.
|
|
.PARAMETER ExpectedSize
|
|
Sum of photo and video file sizes, as reported in the containing folder
|
|
.PARAMETER LastModified
|
|
Date to set on a created file.
|
|
|
|
.NOTES
|
|
Author: Petr Vyskocil
|
|
#>
|
|
PARAM(
|
|
[Parameter(Mandatory=$True)]
|
|
[string]$AccessToken,
|
|
[Parameter(Mandatory=$True)]
|
|
[string]$ElementId,
|
|
[Parameter(Mandatory=$True)]
|
|
[string]$SaveTo,
|
|
[Parameter(Mandatory=$True)]
|
|
[int]$ExpectedSize,
|
|
[Parameter(Mandatory=$True)]
|
|
[datetime]$LastModified
|
|
)
|
|
|
|
if (!(Test-Path $SaveTo)) { New-Item -ItemType Directory -Force $SaveTo | Out-Null }
|
|
|
|
# video part
|
|
$Uri = "https://api.onedrive.com/v1.0/drive/items/$($ElementId)/content?format=video"
|
|
Write-Output("Calling OneDrive API")
|
|
Write-Output($Uri)
|
|
$TmpFile = $SaveTo+'tmp-file.mov'
|
|
$WebRequest=Invoke-WebRequest -Method "GET" -Uri $Uri -Header @{ Authorization = "BEARER "+$AccessToken } -ErrorAction SilentlyContinue -OutFile $TmpFile -PassThru
|
|
$ActualSize = $WebRequest.RawContentLength
|
|
$FileName = ($WebRequest.Headers.'Content-Disposition'.Split('=',2)[-1]).Trim('"')
|
|
if ($FileName) {
|
|
Write-Debug("Renaming $TmpFile to $FileName")
|
|
if (Test-Path($SaveTo+$FileName)) { Remove-Item ($SaveTo+$FileName) }
|
|
Rename-Item -Path $TmpFile -NewName $FileName
|
|
(Get-Item ($SaveTo+$FileName)).LastWriteTime = $LastModified
|
|
}
|
|
|
|
|
|
# image part
|
|
$Uri = "https://api.onedrive.com/v1.0/drive/items/$($ElementId)/content"
|
|
Write-Output("Calling OneDrive API")
|
|
Write-Output($Uri)
|
|
$TmpFile = $SaveTo+'tmp-file.img'
|
|
$WebRequest=Invoke-WebRequest -Method "GET" -Uri $Uri -Header @{ Authorization = "BEARER "+$AccessToken } -ErrorAction SilentlyContinue -OutFile $TmpFile -PassThru
|
|
$ActualSize = $ActualSize + $WebRequest.RawContentLength
|
|
$FileName = ($WebRequest.Headers.'Content-Disposition'.Split('=',2)[-1]).Trim('"')
|
|
if ($FileName) {
|
|
Write-Debug("Renaming $TmpFile to $FileName")
|
|
if (Test-Path($SaveTo+$FileName)) { Remove-Item ($SaveTo+$FileName) }
|
|
Rename-Item -Path $TmpFile -NewName $FileName
|
|
(Get-Item ($SaveTo+$FileName)).LastWriteTime = $LastModified
|
|
}
|
|
|
|
if ($ActualSize -ne $ExpectedSize) { Write-Error("Error saving live photo $ElementId. Got $ActualSize bytes, expected $ExpectedSize bytes.") }
|
|
|
|
}
|
|
|
|
|
|
Write-Output "Live Photo downloader - Downloads Live Photos from OneDrive camera roll as saved by OneDrive iOS app."
|
|
Write-Output "(C) 2024 Petr Vyskocil. Licensed under MIT license."
|
|
Write-Output ""
|
|
|
|
|
|
# This disables powershell progress indicators, speeding up Invoke-WebRequest with big results by a factor of 10 or so
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
Write-Output "Getting OneDrive Authentication token..."
|
|
$auth=Get-ODPhotosToken
|
|
|
|
Write-Output "Downloading Live Photos..."
|
|
Write-Output $auth
|
|
Download-LivePhotosAuth -AccessToken $auth.'AccessToken-OneDrive.ReadWrite' -PathToScan $PathToScan -SaveTo $SaveTo |