From 5109e1ec3becd14f06237816aeb72c9b684e06db Mon Sep 17 00:00:00 2001 From: tigeren Date: Mon, 23 Dec 2024 11:04:57 +0800 Subject: [PATCH] Init --- Download-ODLivePhotos.ps1 | 281 ++++++++++++++++++++++++++++++ Download-ODLivePhotosV2.ps1 | 334 ++++++++++++++++++++++++++++++++++++ Download.ps1 | 1 + DownloadV2.ps1 | 12 ++ 4 files changed, 628 insertions(+) create mode 100644 Download-ODLivePhotos.ps1 create mode 100644 Download-ODLivePhotosV2.ps1 create mode 100644 Download.ps1 create mode 100644 DownloadV2.ps1 diff --git a/Download-ODLivePhotos.ps1 b/Download-ODLivePhotos.ps1 new file mode 100644 index 0000000..57c4714 --- /dev/null +++ b/Download-ODLivePhotos.ps1 @@ -0,0 +1,281 @@ +<# +.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-Output("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 \ No newline at end of file diff --git a/Download-ODLivePhotosV2.ps1 b/Download-ODLivePhotosV2.ps1 new file mode 100644 index 0000000..2c50784 --- /dev/null +++ b/Download-ODLivePhotosV2.ps1 @@ -0,0 +1,334 @@ +<# +.DESCRIPTION +Debug script for accessing OneDrive Live Photos using Microsoft Graph API. +Requires Microsoft.Graph PowerShell module. + +.PARAMETER SaveTo +Target path where to save live photos. +.PARAMETER PathToScan +Path on OneDrive to scan (default: '/Photos/Camera Roll') + +.EXAMPLE +.\Debug-ODLivePhotos.ps1 -SaveTo 'C:\Live Photos' -PathToScan '/Photos/Camera Roll' +#> + +param ( + [Parameter(Mandatory)] + [string] $SaveTo, + [string] $PathToScan = '/Photos/Camera Roll', + [string] $ClientId, + [string] $TenantId +) + +# Check for Microsoft.Graph module and install if needed +if (-not (Get-Module -ListAvailable -Name Microsoft.Graph)) { + Write-Host "Installing Microsoft.Graph module..." + Install-Module Microsoft.Graph -Scope CurrentUser -Force +} + +function Connect-GraphWithDebug { + try { + # Required scopes for OneDrive access + $scopes = @( + "Files.Read.All", + "Files.ReadWrite.All", + "offline_access" + ) + + Write-Host "Connecting to Microsoft Graph..." + Connect-MgGraph -Scopes $scopes + + # Get and display current connection info + $context = Get-MgContext + Write-Host "Connected as: $($context.Account)" + Write-Host "Scopes: $($context.Scopes -join ', ')" + + return $true + } + catch { + Write-Error "Failed to connect to Microsoft Graph: $_" + return $false + } +} + +function Get-DriveItems { + param ( + [string]$FolderPath + ) + + try { + # Verify we're connected + $context = Get-MgContext + if ($null -eq $context) { + throw "Not connected to Microsoft Graph" + } + + # Get drive info + $drive = Get-MgDrive -Filter "driveType eq 'personal'" | Select-Object -First 1 + Write-Host "Using Drive ID: $($drive.Id)" + + # Clean up the path format + $cleanPath = $FolderPath.Replace('\', '/').Trim('/') + Write-Host "Searching in path: /$cleanPath" + + try { + # First try to get the folder itself + $folder = Get-MgDriveItem -DriveId $drive.Id -DriveItemId "root:/$cleanPath" -ErrorAction Stop + Write-Host "Found folder: $($folder.Name)" + + # Then get its children + $items = Get-MgDriveItemChild -DriveId $drive.Id -DriveItemId $folder.Id + + Write-Host "Found $($items.Count) items in specified folder" + + # Debug output for first few items + $items | Select-Object -First 50000 | ForEach-Object { + Write-Host "`nItem: $($_.Name)" + Write-Host "Type: $($_.File.MimeType)" + Write-Host "ID: $($_.Id)" + + # Additional debug info for photos + if ($_.Photo) { + Write-Host "Photo metadata found!" + $_.Photo | Format-List | Out-String | Write-Host + } + } + + return $items + } + catch { + Write-Error "Error accessing path '/$cleanPath': $_" + Write-Host "Full error details:" + $_ | Format-List -Force + return $null + } + } + catch { + Write-Error "Error in Get-DriveItems: $_" + Write-Host "Full error details:" + $_ | Format-List -Force + return $null + } +} + +function Test-DownloadItem { + param ( + [Parameter(Mandatory)] + [string]$ItemId, + [Parameter(Mandatory)] + [string]$SavePath, + [Parameter(Mandatory)] + $item + ) + + try { + $fileName = Join-Path $SavePath $item.Name + + # Check if file already exists + if (Test-Path $fileName) { + Write-Host "File already exists: $fileName" -ForegroundColor Yellow + return $true + } + + Write-Host "Downloading to: $fileName" + Get-MgDriveItemContent -DriveId $drive.Id -DriveItemId $ItemId -OutFile $fileName + + if (Test-Path $fileName) { + Write-Host "Successfully downloaded: $fileName" -ForegroundColor Green + return $true + } + return $false + } + catch { + Write-Error "Download failed: $_" + return $false + } +} + +function Get-LivePhotoBundle { + param ( + [Parameter(Mandatory)] + $item, + [Parameter(Mandatory)] + $drive + ) + + try { + if ($item.File.MimeType -eq "image/heic") { + Write-Host "Found HEIC file: $($item.Name)" -ForegroundColor Cyan + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($item.Name) + + # Get current auth context and token + $context = Get-MgContext + if (-not $context) { + throw "No authentication context found" + } + + $token = $context.AccessToken + Write-Host "Using token: $($token.Substring(0, 10))..." -ForegroundColor Gray + + # Get the item details with special fields + $itemEndpoint = "https://graph.microsoft.com/v1.0/drives/$($drive.Id)/items/$($item.Id)" + Write-Host "Getting item details..." + + $headers = @{ + 'Authorization' = "Bearer $token" + 'Accept' = 'application/json' + } + + # First try to get item metadata + try { + $response = Invoke-MgGraphRequest -Uri $itemEndpoint -Method GET + Write-Host "Got item metadata" + + # Try to get the video component using Graph API + $videoEndpoint = "$itemEndpoint/content" + $videoPath = Join-Path $SaveTo "$baseName.mov" + + if (-not (Test-Path $videoPath)) { + Write-Host "Attempting to download video component..." + + # Try different content types + $videoHeaders = @{ + 'Authorization' = "Bearer $token" + 'Accept' = 'video/quicktime' + 'Prefer' = 'respond-async' + } + + # Try with special query parameters + $queryParams = @( + "select=video", + "expand=video", + "format=mov" + ) + + foreach ($param in $queryParams) { + $tryUrl = "${videoEndpoint}?$param" + Write-Host "Trying URL: $tryUrl" + + try { + Invoke-MgGraphRequest -Uri $tryUrl -Headers $videoHeaders -Method GET -OutputFilePath $videoPath + if ((Test-Path $videoPath) -and (Get-Item $videoPath).Length -gt 0) { + Write-Host "Successfully downloaded video component" -ForegroundColor Green + return $true + } + } + catch { + Write-Host "Attempt failed with $param : $_" -ForegroundColor Yellow + Remove-Item $videoPath -ErrorAction SilentlyContinue + } + } + + # If all attempts failed, try one last time with direct Graph API call + try { + $finalEndpoint = "$itemEndpoint/video" + Write-Host "Trying final endpoint: $finalEndpoint" + Invoke-MgGraphRequest -Uri $finalEndpoint -Method GET -OutputFilePath $videoPath + if ((Test-Path $videoPath) -and (Get-Item $videoPath).Length -gt 0) { + Write-Host "Successfully downloaded video using final attempt" -ForegroundColor Green + return $true + } + } + catch { + Write-Host "Final attempt failed: $_" -ForegroundColor Red + Remove-Item $videoPath -ErrorAction SilentlyContinue + } + } else { + Write-Host "Video file already exists: $videoPath" -ForegroundColor Yellow + return $true + } + } + catch { + Write-Host "Failed to get item metadata: $_" -ForegroundColor Red + } + } + return $false + } + catch { + Write-Error "Error processing potential Live Photo: $_" + return $false + } +} + +# Add this helper function to get a Photos-specific token if needed +function Get-ODPhotosToken { + param( + [string]$ClientId = "073204aa-c1e0-4e66-a200-e5815a0aa93d" # OneDrive Photos client ID + ) + + $scopes = "OneDrive.ReadWrite offline_access" + $redirectUri = "https://photos.onedrive.com/auth/login" + + # Get token using device code flow + $deviceCode = Get-MgDeviceCode -ClientId $ClientId -Scopes $scopes + + Write-Host "Please visit: $($deviceCode.VerificationUri)" + Write-Host "Enter code: $($deviceCode.UserCode)" + + $token = Get-MgToken -DeviceCode $deviceCode -ErrorAction Stop + return $token.AccessToken +} + +# Main execution +Write-Host "OneDrive Live Photos Debug Script" +Write-Host "Save location: $SaveTo" +Write-Host "Scan path: $PathToScan" + +# Create save directory if it doesn't exist +if (!(Test-Path $SaveTo)) { + New-Item -ItemType Directory -Force -Path $SaveTo +} + +# Connect to Graph API +if (Connect-GraphWithDebug) { + Write-Host "`nQuerying OneDrive items..." + $items = Get-DriveItems -FolderPath $PathToScan + + # print the items to the console + # $items | Format-List | Out-String | Write-Host + + if ($items) { + # Get drive reference + $drive = Get-MgDrive -Filter "driveType eq 'personal'" | Select-Object -First 1 + if (-not $drive) { + Write-Error "Could not get drive reference" + return + } + + Write-Host "`nProcessing items for Live Photos..." + foreach ($item in $items) { + Write-Host "`nChecking item: $($item.Name)" + + # Process only HEIC files + if ($item.File.MimeType -eq "image/heic") { + Write-Host "Found HEIC file, checking for Live Photo components..." + + # Download the HEIC file if it doesn't exist + try { + $result = Test-DownloadItem -ItemId $item.Id -SavePath $SaveTo -item $item + if ($result) { + # Try to get the video component + $hasVideo = Get-LivePhotoBundle -item $item -drive $drive + if ($hasVideo) { + Write-Host "Successfully processed Live Photo bundle for: $($item.Name)" -ForegroundColor Green + } else { + Write-Host "No Live Photo video component found for: $($item.Name)" -ForegroundColor Yellow + } + } + } + catch { + Write-Host "Error processing $($item.Name): $_" -ForegroundColor Red + } + + # Add a small delay between items + Start-Sleep -Milliseconds 500 + } + } + + Write-Host "`nCompleted processing Live Photos" + } else { + Write-Host "No items found to process" + } +} + +Write-Host "`nDebug script completed" \ No newline at end of file diff --git a/Download.ps1 b/Download.ps1 new file mode 100644 index 0000000..cf381cc --- /dev/null +++ b/Download.ps1 @@ -0,0 +1 @@ +.\Download-ODLivePhotos.ps1 -SaveTo 'D:\Photos\OneDrive-Photo\2024\12' -PathToScan '\Photos\Auto-saved\2024\12' \ No newline at end of file diff --git a/DownloadV2.ps1 b/DownloadV2.ps1 new file mode 100644 index 0000000..ce607ae --- /dev/null +++ b/DownloadV2.ps1 @@ -0,0 +1,12 @@ +# Example values - you need to replace these with your own from Azure Portal +$params = @{ + SaveTo = "C:\Temp\2024\12" + PathToScan = "/Photos/Camera Roll" + ClientId = "d581ab07-3a21-44d3-84c4-16b06bef6266" # From Azure App Registration + TenantId = "common" # Use "common" for personal Microsoft accounts +} + +# '\Photos\Auto-saved\2024\12' +# /Photos/Camera Roll +.\Download-ODLivePhotosV2.ps1 -SaveTo 'C:\Temp\2024\12' -PathToScan '\Photos\Auto-saved\2024\12' -ClientId 'd581ab07-3a21-44d3-84c4-16b06bef6266' -TenantId 'common' +