Add scripts for downloading Live Photos from OneDrive and syncing files
This commit is contained in:
parent
4f547af149
commit
cfdb93a91e
|
|
@ -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-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
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
<#
|
||||
.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 AccessToken
|
||||
OneDrive application access token.
|
||||
.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-ODLivePhotos2.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] $AccessToken,
|
||||
[string] $PathToScan = '\Pictures\Camera Roll'
|
||||
)
|
||||
|
||||
function Register-WebView2Type
|
||||
{
|
||||
<#
|
||||
.DESCRIPTION
|
||||
Check that WebView2 exists, if not obtain it from nuget.
|
||||
|
||||
.EXAMPLE
|
||||
Register-WebView2Type
|
||||
.NOTES
|
||||
Author: Petr Vyskocil
|
||||
#>
|
||||
$Package = "Microsoft.Web.WebView2"
|
||||
$Version = "1.0.3124.44"
|
||||
$BasePath = "$env:temp\ODLivePhotos\"
|
||||
|
||||
Write-Host "Checking for WebView2 assemblies..."
|
||||
if (!("Microsoft.Web.WebView2.WinForms.WebView2" -as [type])) {
|
||||
Write-Host "WebView2 type not found, attempting to load from local cache..."
|
||||
if (!(Test-Path "$($BasePath)\Microsoft.Web.WebView2.WinForms.dll")) {
|
||||
Write-Host " Downloading nuget package $($Package) $($Version)"
|
||||
Install-Package -Source "https://www.nuget.org/api/v2" -Name $Package -RequiredVersion $Version -Scope CurrentUser -Destination $BasePath -Force
|
||||
Write-Host " Copying package files to script directory"
|
||||
foreach ( $Framework in (Get-ChildItem "$BasePath\$($Package).$($Version)\lib" -Directory) ) {
|
||||
Write-Host " Processing framework: $Framework"
|
||||
Copy-Item -Recurse -Path "$BasePath\$($Package).$($Version)\lib\$($Framework)\*.dll" -Destination $BasePath -Force
|
||||
}
|
||||
$Arch = (Get-CimInstance Win32_operatingsystem).OSArchitecture
|
||||
Write-Host " Detected architecture: $Arch"
|
||||
if ($Arch -eq "32-bit") {
|
||||
Copy-Item -Recurse -Path "$BasePath\$($Package).$($Version)\runtimes\win-x86\native\*.dll" -Destination $BasePath -Force
|
||||
}
|
||||
if ($Arch -eq "64-bit") {
|
||||
Copy-Item -Recurse -Path "$BasePath\$($Package).$($Version)\runtimes\win-x64\native\*.dll" -Destination $BasePath -Force
|
||||
}
|
||||
if ($Arch -eq "ARM 64-bit Processor") {
|
||||
Copy-Item -Recurse -Path "$BasePath\$($Package).$($Version)\runtimes\win-arm64\native\*.dll" -Destination $BasePath -Force
|
||||
}
|
||||
Remove-Item -Recurse -Force "$BasePath\$($Package).$($Version)"
|
||||
}
|
||||
Write-Host " Registering WebView2 type from: $BasePath\Microsoft.Web.WebView2.WinForms.dll"
|
||||
Add-Type -Path "$BasePath\Microsoft.Web.WebView2.WinForms.dll"
|
||||
}
|
||||
Write-Host "WebView2 type registration complete"
|
||||
}
|
||||
|
||||
function Get-ODPhotosToken
|
||||
{
|
||||
Write-Host "Initializing WebView2 component..."
|
||||
$Hash = [hashtable]::Synchronized(@{})
|
||||
$Env:WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = "--enable-features=msSingleSignOnOSForPrimaryAccountIsShared"
|
||||
|
||||
Write-Host "Creating WebView2 control..."
|
||||
$Web = New-Object Microsoft.Web.WebView2.WinForms.WebView2
|
||||
$Web.CreationProperties = New-Object Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties
|
||||
$Web.CreationProperties.UserDataFolder = "$env:temp\ODLivePhotos\"
|
||||
|
||||
Write-Host "Setting up WebView2 event handlers..."
|
||||
$Web.add_NavigationStarting({
|
||||
param($sender, $e)
|
||||
Write-Host "Navigation starting to: $($e.Uri)"
|
||||
})
|
||||
|
||||
$Web.add_NavigationCompleted({
|
||||
param($sender, $e)
|
||||
Write-Host "Navigation completed to: $($e.Uri)"
|
||||
if ($e.IsSuccess) {
|
||||
Write-Host "Navigation successful"
|
||||
} else {
|
||||
Write-Host "Navigation failed with WebErrorStatus: $($e.WebErrorStatus)"
|
||||
}
|
||||
})
|
||||
|
||||
$Web.add_CoreWebView2InitializationCompleted({
|
||||
param($sender, $e)
|
||||
if ($e.IsSuccess) {
|
||||
Write-Host "CoreWebView2 initialized successfully"
|
||||
Write-Host "Setting up user agent and response handlers..."
|
||||
$Web.CoreWebView2.Settings.UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.3124.85'
|
||||
$Web.CoreWebView2.add_WebResourceResponseReceived({
|
||||
param($WebView2, $E)
|
||||
if ($E.Request.Uri.StartsWith('https://my.microsoftpersonalcontent.com/_api/') -or $E.Request.Uri.StartsWith('https://api.onedrive.com/')) {
|
||||
Write-Host "Detected API request: $($E.Request.Uri)"
|
||||
if ($E.Request.Headers.Contains('Authorization')) {
|
||||
Write-Host "Found authorization token"
|
||||
$Hash.AuthToken = $E.Request.Headers.GetHeader('Authorization')
|
||||
$Form.Close()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Write-Host "CoreWebView2 initialization failed"
|
||||
Write-Host "Please ensure Microsoft Edge WebView2 Runtime is installed"
|
||||
Write-Host "Download from: https://developer.microsoft.com/en-us/microsoft-edge/webview2/"
|
||||
}
|
||||
})
|
||||
|
||||
Write-Host "Creating form..."
|
||||
$Form = New-Object System.Windows.Forms.Form -Property @{
|
||||
Width=800
|
||||
Height=800
|
||||
Text="OneDrive Live Photo Downloader - Authentication Token capture dialog"
|
||||
} -ErrorAction Stop
|
||||
|
||||
Write-Host "Setting up form..."
|
||||
$Web.Dock = "Fill"
|
||||
$Form.Controls.Add($web)
|
||||
|
||||
Write-Host "Setting initial URL..."
|
||||
$Web.source = "https://onedrive.live.com/?qt=allmyphotos&photosData=%2F&sw=bypassConfig&v=photos"
|
||||
|
||||
Write-Host "Showing authentication dialog..."
|
||||
$Form.Add_Shown( { $form.Activate() } )
|
||||
$Form.ShowDialog() | Out-Null
|
||||
|
||||
Write-Host "Dialog closed, cleaning up..."
|
||||
$Web.Dispose()
|
||||
|
||||
if ($Hash.AuthToken) {
|
||||
Write-Host "Authentication token obtained successfully"
|
||||
} else {
|
||||
Write-Host "No authentication token was obtained"
|
||||
}
|
||||
|
||||
return $Hash.AuthToken
|
||||
}
|
||||
|
||||
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]$DriveId='',
|
||||
[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
|
||||
}
|
||||
if ($DriveId -eq '') {
|
||||
$Uri = 'https://my.microsoftpersonalcontent.com/_api/v2.1/drive/' + $Location + '/children?%24filter=photo%2FlivePhoto+ne+null+or+folder+ne+null+or+remoteItem+ne+null&select=fileSystemInfo%2Cphoto%2Cid%2Cname%2Csize%2Cfolder%2CremoteItem'
|
||||
} else {
|
||||
$Uri = 'https://my.microsoftpersonalcontent.com/_api/v2.1/drives/' +$DriveId + '/' + $Location + '/children?%24filter=photo%2FlivePhoto+ne+null+or+folder+ne+null+or+remoteItem+ne+null&select=fileSystemInfo%2Cphoto%2Cid%2Cname%2Csize%2Cfolder%2CremoteItem'
|
||||
}
|
||||
}
|
||||
Write-Debug("Calling OneDrive API")
|
||||
Write-Debug($Uri)
|
||||
$WebRequest=Invoke-WebRequest -Method 'GET' -Header @{ Authorization = $AccessToken; Prefer = "Include-Feature=AddToOneDrive"} -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 -DriveId $DriveId
|
||||
}
|
||||
}
|
||||
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 -DriveId $_.remoteItem.parentReference.driveId
|
||||
}
|
||||
}
|
||||
if ([bool]$_.PSObject.Properties['photo']) {
|
||||
if ([bool]$_.photo.PSObject.Properties['livePhoto']) {
|
||||
if ($CurrentPath.StartsWith($PathToScan)) {
|
||||
$TargetPath = $SaveTo + '\' + $CurrentPath.Substring($PathToScan.Length)
|
||||
$VideoName = ([io.fileinfo]$_.name).basename+'.mov'
|
||||
$VideoLen = $_.photo.livePhoto.totalStreamSize - $_.size
|
||||
if ( (Test-Path($TargetPath+$_.name)) -and # Target image exists
|
||||
(Test-Path($TargetPath+$VideoName)) -and # Target video exists
|
||||
((Get-Item($TargetPath+$_.name)).Length -eq $_.size) -and # size of image is onedrive's size
|
||||
((Get-Item($TargetPath+$VideoName)).Length -eq $VideoLen) # size of video 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 -DriveId $DriveId -SaveTo $TargetPath -ImageName $_.name -VideoName $VideoName -ExpImgLen $_.size -ExpVidLen $VideoLen -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 DriveId
|
||||
OneDrive DriveId of a LivePhoto
|
||||
.PARAMETER SaveTo
|
||||
Target path where to save live photos.
|
||||
.PARAMETER ExpImgLen
|
||||
Expected length of image file, as reported in containing folder.
|
||||
.PARAMETER ExpVidLen
|
||||
Expected length of video file, as reported in 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,
|
||||
[string]$DriveId,
|
||||
[Parameter(Mandatory=$True)]
|
||||
[string]$SaveTo,
|
||||
[Parameter(Mandatory=$True)]
|
||||
[string]$ImageName,
|
||||
[Parameter(Mandatory=$True)]
|
||||
[string]$VideoName,
|
||||
[Parameter(Mandatory=$True)]
|
||||
[int]$ExpImgLen,
|
||||
[Parameter(Mandatory=$True)]
|
||||
[int]$ExpVidLen,
|
||||
[Parameter(Mandatory=$True)]
|
||||
[datetime]$LastModified
|
||||
)
|
||||
|
||||
if (!(Test-Path $SaveTo)) { New-Item -ItemType Directory -Force $SaveTo | Out-Null }
|
||||
|
||||
# video part
|
||||
if ($DriveId -eq '') {
|
||||
$Uri = "https://my.microsoftpersonalcontent.com/_api/v2.1/drive/items/$($ElementId)/content?format=video"
|
||||
} else {
|
||||
$Uri = "https://my.microsoftpersonalcontent.com/_api/v2.1/drives/$($DriveId)/items/$($ElementId)/content?format=video"
|
||||
}
|
||||
Write-Debug("Calling OneDrive API")
|
||||
Write-Debug($Uri)
|
||||
$FileName = $SaveTo+$VideoName
|
||||
If (Test-Path($FileName)) { Remove-Item ($FileName) }
|
||||
$WebRequest=Invoke-WebRequest -Method "GET" -Uri $Uri -Header @{ Authorization = $AccessToken } -ErrorAction SilentlyContinue -OutFile $FileName -PassThru
|
||||
$ActualLen = $WebRequest.RawContentLength
|
||||
(Get-Item ($FileName)).LastWriteTime = $LastModified
|
||||
|
||||
If ($ActualLen -ne $ExpVidLen) { Write-Error("Error saving video part of live photo $ElementId. Got $ActualLen bytes, expected $ExpVidLen bytes.") }
|
||||
|
||||
# image part
|
||||
if ($DriveId -eq '') {
|
||||
$Uri = "https://my.microsoftpersonalcontent.com/_api/v2.1/drive/items/$($ElementId)/content"
|
||||
} else {
|
||||
$Uri = "https://my.microsoftpersonalcontent.com/_api/v2.1/drives/$($DriveId)/items/$($ElementId)/content"
|
||||
}
|
||||
Write-Debug("Calling OneDrive API")
|
||||
Write-Debug($Uri)
|
||||
$FileName = $SaveTo+$ImageName
|
||||
If (Test-Path($FileName)) { Remove-Item ($FileName) }
|
||||
$WebRequest=Invoke-WebRequest -Method "GET" -Uri $Uri -Header @{ Authorization = $AccessToken } -ErrorAction SilentlyContinue -OutFile $FileName -PassThru
|
||||
$ActualLen = $WebRequest.RawContentLength
|
||||
(Get-Item ($FileName)).LastWriteTime = $LastModified
|
||||
|
||||
if ($ActualLen -ne $ExpImgLen) { Write-Error("Error saving photo part of live photo $ElementId. Got $ActualLen bytes, expected $ExpImgLen bytes.") }
|
||||
|
||||
}
|
||||
|
||||
|
||||
Write-Output "Live Photo downloader - Downloads Live Photos from OneDrive camera roll as saved by OneDrive iOS app."
|
||||
Write-Output "(C) 2024-2025 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'
|
||||
|
||||
if ($AccessToken -eq '') {
|
||||
Write-Output "Creating WebView2 component..."
|
||||
Register-WebView2Type
|
||||
Write-Output "Getting OneDrive Authentication token..."
|
||||
$AccessToken = Get-ODPhotosToken
|
||||
}
|
||||
|
||||
Write-Output "Downloading Live Photos..."
|
||||
Download-LivePhotosAuth -AccessToken $AccessToken -PathToScan $PathToScan -SaveTo $SaveTo
|
||||
|
|
@ -0,0 +1 @@
|
|||
.\Download-ODLivePhotosV2.ps1 -SaveTo 'D:\Photos\OneDrive-Photo\2025\04' -PathToScan '\Photos\Auto-saved\2025\04' -AccessToken 'bearer EwAIBK1DBAAUnIIyw3fu//VwydnOZRl0mzVDtSEAAZEJfUS4ycN7p2NsXfOVvL8URvOVZ/IJfhK6zremxPOoWwvBNw94siJNRuMahNWVE0nHlmojzGY5W9U1yZ46wt8LhbuHGp7flzFmctyWNiVjOSv6DFHwqxeR2MNWp/fJw7wAgOoqyvYnX0M/Fi4fn3xCwYQKEGQIPfTAA0bo0BY/s1B2sNjGxJHnoHO%2bpmJPAQkOzBroyzL0XDmacrg8Yq73vwfpoAKrldY2c8U4IRLp56n4IY5Dkm/IT7jvseV5zpWBZ7kGPT7bvQEJny3FLyXhRcn/tJWUcaygTi%2b8Ag86CNx/1HIfmY0PxgDgM/bGMTCACeSwn1IrDqXtuHxuXQ8QZgAAEBGJudb6NkqUwifou2E5lUnQAokpbHEJ4NRBDgYygdm/iue5F8RHd25td4fbhK7rMtqGG2/qDcCG3Zc/l8bq%2bpJO1i/K/r4RCeekeYV8KpFsqH2VHOPX2bkl6wFNh34OtDYlFMyj2aGqDp9Q4kRPjf9MEDxM6Upv2FNZtccjT9HV25nyPOAAPmc4JBY7bUFRYlky60yKH0gg%2br0ybqPEN0/e2voTPAZvZR6LYmb7BtFHW6dfTU7/c56NTEOvvv2ae4WDoxacQ9C4vAaCEiY36NS6i3jT5qhu/2Aj3uxCGJ0CsNNPgAZa384h/nQ4iv3gbtSGy/x5SAkK3yxxvkMOepQpU16HOSVPgE5v0pSFWwny9O/H5d6eG7jDWf%2bbyLdJUcFtUi5ROmkm5uT3LObUsqL0yZSyRRkGwGGcv%2b6QdcoAj%2bxwalHVyfebRa0iDZFDbI1VmplIGkJIoranZDQxafkH7m8f4mMIoh%2bZ7h/MX1DCXnY2COJqc9zWZCcsHk2Sx8UAtiCINKPUCiQp1VIw2CDnVDLIO866TuSdDtIqTfTeS9RkSeuGVK8sx9Ezttsbyo97fq2ocLirCXmYRVuh4JvgmFIDLwLwNv3ZgrSUjxOY/elG/QVV%2bwHHG9TfeF7d4UaYdlNIcRnx/%2bF6S1O%2bVKiWrCAFBVqVxiN8AmEl2BQ79lkKkOIngapKUgfWklEEUuYfyDkaVyqBljyXFCY6K6SsQM7FRCr9mtwy3PmP6NFKkKaZnn5DvcR6xad/wHHXNLFSfm4JzsiEtmO19m/EnUZzzuQMvl3oybqtl0Ojp/SnFH6u48PXKvFDVdXjW4I8Q9eMMqCJqkMxmbI64bmX/rPlCJbsZjro3FcjGQB1dpHIE9d%2b00816sydZ1NSc%2bp78reRAQ8P2xr0/A9tao6a23yzAp8FAWpuXhNVgdwNElBkB7rqs60lLDx/NXUkRes0sTnacz9NpeBst04g/P7As2qvSvEC'
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import os
|
||||
import requests
|
||||
import webbrowser
|
||||
import http.server
|
||||
import socketserver
|
||||
import threading
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict
|
||||
import uuid
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from webview import create_window, start
|
||||
|
||||
class OAuthCallbackHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
self.server.oauth_response = post_data
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Authentication complete. You can close this window.")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# Suppress logging
|
||||
pass
|
||||
|
||||
def get_onedrive_token() -> Dict[str, str]:
|
||||
"""
|
||||
Authenticate with OneDrive using the Photos web client ID.
|
||||
Returns authentication tokens in a dictionary.
|
||||
"""
|
||||
client_id = "073204aa-c1e0-4e66-a200-e5815a0aa93d"
|
||||
redirect_uri = "https://photos.onedrive.com/auth/login"
|
||||
scope = "OneDrive.ReadWrite,offline_access,openid,profile"
|
||||
|
||||
# 创建一个简单的 Web 浏览器窗口
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from webview import create_window, start
|
||||
|
||||
def on_loaded():
|
||||
# 监听 cookies 变化
|
||||
cookies = webview.get_cookies()
|
||||
for cookie in cookies:
|
||||
if cookie['name'] == 'AccessToken-OneDrive.ReadWrite':
|
||||
window.destroy()
|
||||
return cookie['value']
|
||||
|
||||
auth_url = (
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"
|
||||
f"?client_id={client_id}"
|
||||
f"&nonce=uv.{str(uuid.uuid4())}"
|
||||
"&response_mode=form_post"
|
||||
f"&scope={scope}"
|
||||
"&response_type=code"
|
||||
f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
|
||||
)
|
||||
|
||||
# 创建浏览器窗口
|
||||
window = create_window('OneDrive Authentication', auth_url,
|
||||
width=660, height=775)
|
||||
|
||||
# 启动窗口并等待认证完成
|
||||
start(func=on_loaded)
|
||||
|
||||
return window.get_cookies()
|
||||
|
||||
def download_live_photos(access_token: str, save_to: str, path_to_scan: str = "/Pictures/Camera Roll"):
|
||||
"""
|
||||
Download all Live Photos from the specified OneDrive path.
|
||||
"""
|
||||
if not path_to_scan.startswith('/'):
|
||||
path_to_scan = '/' + path_to_scan
|
||||
if not path_to_scan.endswith('/'):
|
||||
path_to_scan += '/'
|
||||
|
||||
def process_folder(folder_id: str = 'root', current_path: str = '/'):
|
||||
url = f"https://api.onedrive.com/v1.0/drive/items/{folder_id}/children"
|
||||
params = {
|
||||
"$filter": "photo/livePhoto ne null or folder ne null or remoteItem ne null",
|
||||
"select": "fileSystemInfo,photo,id,name,size,folder,remoteItem"
|
||||
}
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
while url:
|
||||
response = requests.get(url, params=params, headers=headers).json()
|
||||
|
||||
for item in response.get('value', []):
|
||||
folder_path = current_path + item['name'] + '/'
|
||||
|
||||
# Handle folders
|
||||
if 'folder' in item:
|
||||
if folder_path.startswith(path_to_scan) or path_to_scan.startswith(folder_path):
|
||||
print(f"Checking folder {item['id']} - {folder_path}")
|
||||
process_folder(item['id'], folder_path)
|
||||
|
||||
# Handle live photos
|
||||
if 'photo' in item and 'livePhoto' in item['photo']:
|
||||
if current_path.startswith(path_to_scan):
|
||||
target_path = os.path.join(save_to, current_path[len(path_to_scan):])
|
||||
download_single_live_photo(
|
||||
access_token,
|
||||
item['id'],
|
||||
target_path,
|
||||
item['size'],
|
||||
item['fileSystemInfo']['lastModifiedDateTime']
|
||||
)
|
||||
|
||||
url = response.get('@odata.nextLink')
|
||||
params = {} # Clear params as nextLink includes them
|
||||
|
||||
process_folder()
|
||||
|
||||
def download_single_live_photo(access_token: str, element_id: str, save_to: str,
|
||||
expected_size: int, last_modified: str):
|
||||
"""
|
||||
Download a single Live Photo (both video and image components).
|
||||
"""
|
||||
os.makedirs(save_to, exist_ok=True)
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
actual_size = 0
|
||||
|
||||
# Download video component
|
||||
video_url = f"https://api.onedrive.com/v1.0/drive/items/{element_id}/content?format=video"
|
||||
video_response = requests.get(video_url, headers=headers)
|
||||
if video_response.ok:
|
||||
filename = video_response.headers.get('Content-Disposition', '').split('filename=')[-1].strip('"')
|
||||
if filename:
|
||||
video_path = os.path.join(save_to, filename)
|
||||
with open(video_path, 'wb') as f:
|
||||
f.write(video_response.content)
|
||||
actual_size += len(video_response.content)
|
||||
os.utime(video_path, (datetime.fromisoformat(last_modified.replace('Z', '+00:00')).timestamp(),) * 2)
|
||||
|
||||
# Download image component
|
||||
image_url = f"https://api.onedrive.com/v1.0/drive/items/{element_id}/content"
|
||||
image_response = requests.get(image_url, headers=headers)
|
||||
if image_response.ok:
|
||||
filename = image_response.headers.get('Content-Disposition', '').split('filename=')[-1].strip('"')
|
||||
if filename:
|
||||
image_path = os.path.join(save_to, filename)
|
||||
with open(image_path, 'wb') as f:
|
||||
f.write(image_response.content)
|
||||
actual_size += len(image_response.content)
|
||||
os.utime(image_path, (datetime.fromisoformat(last_modified.replace('Z', '+00:00')).timestamp(),) * 2)
|
||||
|
||||
if actual_size != expected_size:
|
||||
print(f"Warning: Size mismatch for {element_id}. Expected {expected_size}, got {actual_size}")
|
||||
|
||||
def main():
|
||||
print("Live Photo downloader - Downloads Live Photos from OneDrive camera roll")
|
||||
print("(C) 2024 Petr Vyskocil. Licensed under MIT license.")
|
||||
print()
|
||||
|
||||
save_to = input("Enter the path to save Live Photos: ")
|
||||
path_to_scan = input("Enter OneDrive path to scan [/Photos/Auto-saved]: ") or "/Photos/Auto-saved"
|
||||
|
||||
print("Getting OneDrive Authentication token...")
|
||||
auth = get_onedrive_token()
|
||||
|
||||
if 'access_token' not in auth:
|
||||
print("Failed to get authentication token")
|
||||
return
|
||||
|
||||
print("Downloading Live Photos...")
|
||||
download_live_photos(auth['access_token'], save_to, path_to_scan)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
||||
def run_download_script(base_save_to: str, base_path_to_scan: str, target_date: datetime) -> bool:
|
||||
"""
|
||||
Execute the Download-ODLivePhotos.ps1 PowerShell script with year/month paths
|
||||
|
||||
Args:
|
||||
base_save_to: Base destination path
|
||||
base_path_to_scan: Base source path in OneDrive
|
||||
target_date: Date to determine year/month
|
||||
"""
|
||||
# Format year and month
|
||||
year = target_date.strftime("%Y")
|
||||
month = target_date.strftime("%m")
|
||||
|
||||
# Construct full paths
|
||||
save_to = str(Path(base_save_to) / year / month)
|
||||
path_to_scan = str(Path(base_path_to_scan) / year / month)
|
||||
|
||||
try:
|
||||
# Get the script path (assuming it's in the same directory)
|
||||
script_path = Path(__file__).parent / "Download-ODLivePhotosV2.ps1"
|
||||
|
||||
# Construct the PowerShell command
|
||||
cmd = [
|
||||
'powershell.exe',
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-File', str(script_path),
|
||||
'-SaveTo', save_to,
|
||||
'-PathToScan', path_to_scan
|
||||
]
|
||||
|
||||
print(f"Processing {year}/{month}")
|
||||
print(f"Executing: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
if result.stdout:
|
||||
print("Output:")
|
||||
print(result.stdout)
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error executing PowerShell script:", file=sys.stderr)
|
||||
print(f"Exit code: {e.returncode}", file=sys.stderr)
|
||||
if e.stdout:
|
||||
print("Output:", file=sys.stderr)
|
||||
print(e.stdout, file=sys.stderr)
|
||||
if e.stderr:
|
||||
print("Error:", file=sys.stderr)
|
||||
print(e.stderr, file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Process current month (and last month if today is the 1st)
|
||||
# python run_ps_download.py --base-save-to "D:\Photos\OneDrive-Photo" --base-path-to-scan "\Photos\Auto-saved"
|
||||
|
||||
# Process specific month (2 for last month, 3 for month before last, etc.)
|
||||
# python run_ps_download.py --base-save-to "D:\Photos\OneDrive-Photo" --base-path-to-scan "\Photos\Auto-saved" --months 2
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Execute Download-ODLivePhotos.ps1')
|
||||
parser.add_argument('--base-save-to', required=True,
|
||||
help='Base destination path (year/month will be appended)')
|
||||
parser.add_argument('--base-path-to-scan', required=True,
|
||||
help='Base source path in OneDrive (year/month will be appended)')
|
||||
parser.add_argument('--months', type=int, default=0,
|
||||
help='Optional: specific month to process (1: current month, 2: last month, etc.). '
|
||||
'If not set: processes current month, plus last month if today is first day of month')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get current date
|
||||
now = datetime.now()
|
||||
|
||||
if args.months > 0:
|
||||
# Specific month was requested
|
||||
months_ago = args.months - 1
|
||||
target_date = now.replace(day=1) - timedelta(days=months_ago * 28)
|
||||
success = run_download_script(args.base_save_to, args.base_path_to_scan, target_date)
|
||||
else:
|
||||
# Default behavior
|
||||
# Always process current month
|
||||
success = run_download_script(args.base_save_to, args.base_path_to_scan, now)
|
||||
|
||||
# If it's the first day of the month, also process last month
|
||||
if now.day == 1:
|
||||
last_month = now.replace(day=1) - timedelta(days=1)
|
||||
success = success and run_download_script(args.base_save_to, args.base_path_to_scan, last_month)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import requests
|
||||
import sys
|
||||
|
||||
def scan_library():
|
||||
"""
|
||||
Scan library using Jellyfin API
|
||||
"""
|
||||
url = 'http://192.168.2.216:2283/api/libraries/8ea04568-fd60-4960-8b54-11e7508af1c4/scan'
|
||||
headers = {
|
||||
'x-api-key': 'vIs1N0FLaPDCQz6CILc3fBzC7IcRgG5Tne4fyMrA'
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers)
|
||||
response.raise_for_status() # Raise an exception for bad status codes
|
||||
|
||||
print(f"Scan initiated successfully. Status code: {response.status_code}")
|
||||
if response.text:
|
||||
print("Response:", response.text)
|
||||
|
||||
return True
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error scanning library: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = scan_library()
|
||||
sys.exit(0 if success else 1)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
curl -L -X POST 'http://192.168.2.216:2283/api/libraries/8ea04568-fd60-4960-8b54-11e7508af1c4/scan' \
|
||||
-H 'x-api-key: vIs1N0FLaPDCQz6CILc3fBzC7IcRgG5Tne4fyMrA'
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
def sync_files(src_path: str, dest_path: str) -> None:
|
||||
"""
|
||||
Synchronize files from source to destination directory, skipping existing files.
|
||||
|
||||
Args:
|
||||
src_path: Source directory path
|
||||
dest_path: Destination directory path
|
||||
"""
|
||||
# Convert to Path objects for easier handling
|
||||
src = Path(src_path)
|
||||
dest = Path(dest_path)
|
||||
|
||||
# Ensure source directory exists
|
||||
if not src.exists():
|
||||
raise FileNotFoundError(f"Source directory {src_path} does not exist")
|
||||
|
||||
# Create destination directory if it doesn't exist
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Walk through source directory
|
||||
for root, _, files in os.walk(src):
|
||||
# Convert current root to Path object
|
||||
root_path = Path(root)
|
||||
|
||||
# Calculate relative path from source
|
||||
rel_path = root_path.relative_to(src)
|
||||
|
||||
# Create corresponding destination directory
|
||||
dest_dir = dest / rel_path
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy each file
|
||||
for file in files:
|
||||
src_file = root_path / file
|
||||
dest_file = dest_dir / file
|
||||
|
||||
# Skip if destination file exists
|
||||
if dest_file.exists():
|
||||
print(f"Skipping existing file: {dest_file}")
|
||||
continue
|
||||
|
||||
# Copy the file
|
||||
print(f"Copying: {src_file} -> {dest_file}")
|
||||
shutil.copy2(src_file, dest_file)
|
||||
|
||||
# Usage
|
||||
# python sync.py "C:\Users\petr\OneDrive\Photos\Auto-saved\2024\11" "D:\Photos\OneDrive-Photo\2024\11"
|
||||
# python sync.py /path/to/source /path/to/destination
|
||||
if __name__ == "__main__":
|
||||
# Set up argument parser
|
||||
parser = argparse.ArgumentParser(description='Synchronize files from source to destination directory')
|
||||
parser.add_argument('src', help='Source directory path')
|
||||
parser.add_argument('dest', help='Destination directory path')
|
||||
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
sync_files(args.src, args.dest)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def sync_heic_mov(src_folder, dest_folder):
|
||||
# Convert to Path objects for easier handling
|
||||
src_path = Path(src_folder)
|
||||
dest_path = Path(dest_folder)
|
||||
|
||||
# Walk through the source directory
|
||||
for root, dirs, files in os.walk(src_path):
|
||||
for file in files:
|
||||
if file.lower().endswith('.mov'):
|
||||
# Get the relative path from the source folder
|
||||
rel_path = Path(root).relative_to(src_path)
|
||||
|
||||
# Construct the full paths for source and destination
|
||||
src_file = Path(root) / file
|
||||
dest_file = dest_path / rel_path / file
|
||||
|
||||
# Create the destination directory if it doesn't exist
|
||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Check if the destination file already exists
|
||||
if not dest_file.exists():
|
||||
print(f"Copying: {src_file} -> {dest_file}")
|
||||
shutil.copy2(src_file, dest_file)
|
||||
else:
|
||||
print(f"Skipping (already exists): {dest_file}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
source_folder = input("Enter the source folder path: ")
|
||||
destination_folder = input("Enter the destination folder path: ")
|
||||
|
||||
sync_heic_mov(source_folder, destination_folder)
|
||||
print("Sync completed.")
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import os
|
||||
from datetime import datetime, timedelta
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def sync_monthly(base_src: str, base_dest: str, date: datetime) -> None:
|
||||
"""
|
||||
Sync files for a specific year/month from base_src to base_dest
|
||||
|
||||
Args:
|
||||
base_src: Base source directory
|
||||
base_dest: Base destination directory
|
||||
date: Date to process
|
||||
"""
|
||||
# Format year and month
|
||||
year = date.strftime("%Y")
|
||||
month = date.strftime("%m")
|
||||
|
||||
# Construct full paths
|
||||
src_path = Path(base_src) / year / month
|
||||
dest_path = Path(base_dest) / year / month
|
||||
|
||||
# Skip if source doesn't exist
|
||||
if not src_path.exists():
|
||||
print(f"Source path does not exist: {src_path}")
|
||||
return
|
||||
|
||||
print(f"Syncing {src_path} -> {dest_path}")
|
||||
|
||||
# Call sync.py
|
||||
# Assuming sync.py is in the same directory as this script
|
||||
sync_script = Path(__file__).parent / "sync.py"
|
||||
try:
|
||||
subprocess.run([
|
||||
"python",
|
||||
str(sync_script),
|
||||
str(src_path),
|
||||
str(dest_path)
|
||||
], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Sync failed for {year}/{month}: {e}")
|
||||
|
||||
# Sync OneDrive to Local(Backup)
|
||||
# python sync_task.py d:\OneDrive\Photos\Auto-saved\ D:\Photos\Auto-saved --months 1
|
||||
# Sync Photo to NAS
|
||||
# python sync_task.py d:\OneDrive\Photos\Auto-saved\ \\tiger-nas\Multimedia\Photos\ --months 1
|
||||
# Sync Local Motion to NAS
|
||||
# python sync_task.py D:\Photos\OneDrive-Photo\ \\tiger-nas\Multimedia\Photos\ --months 1
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Sync files based on year/month structure')
|
||||
parser.add_argument('base_src', help='Base source directory')
|
||||
parser.add_argument('base_dest', help='Base destination directory')
|
||||
parser.add_argument('--months', type=int, default=0,
|
||||
help='Optional: specific month to sync (1: current month, 2: last month, etc.). '
|
||||
'If not set: syncs current month, plus last month if today is first day of month')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get current date
|
||||
now = datetime.now()
|
||||
|
||||
if args.months > 0:
|
||||
# Specific month was requested
|
||||
months_ago = args.months - 1
|
||||
target_date = now.replace(day=1) - timedelta(days=months_ago * 28)
|
||||
sync_monthly(args.base_src, args.base_dest, target_date)
|
||||
else:
|
||||
# Default behavior
|
||||
# Always sync current month
|
||||
sync_monthly(args.base_src, args.base_dest, now)
|
||||
|
||||
# If it's the first day of the month, also sync last month
|
||||
if now.day == 1:
|
||||
last_month = now.replace(day=1) - timedelta(days=1)
|
||||
sync_monthly(args.base_src, args.base_dest, last_month)
|
||||
Loading…
Reference in New Issue