Export-OneNote.ps1


<#PSScriptInfo

.VERSION 1.4.1

.GUID b9742b5d-5b71-4a08-bbfe-635827e34076

.AUTHOR Jan-Hendrik Peters

.COMPANYNAME Jan-Hendrik Peters

.COPYRIGHT Jan-Hendrik Peters, 2023

.TAGS OneNote Markdown Graph

.LICENSEURI https://raw.githubusercontent.com/nyanhp/freeing-onenote/main/LICENSE

.PROJECTURI https://github.com/nyanhp/freeing-onenote

.ICONURI

.EXTERNALMODULEDEPENDENCIES MiniGraph,MarkdownPrince

.REQUIREDSCRIPTS

.EXTERNALSCRIPTDEPENDENCIES

.RELEASENOTES


.PRIVATEDATA

#>
 

#Requires -Module MiniGraph
#Requires -Module MarkdownPrince


<#
.SYNOPSIS
 This script exports one or more OneNote notebooks to markdown

.DESCRIPTION
 This script exports one or more OneNote notebooks to markdown
 using the Graph API and an Entra App Registration.

 The script requires the following modules to be installed:
    - MiniGraph
    - MarkdownPrince

.PARAMETER TenantId
    The tenant id to use for the Graph API. Defaults to 'Common'.

.PARAMETER OneNoteAppClientId
    The client id of the app registration to use for the Graph API. Defaults to '812899b7-584c-4812-8aee-11d3e164d58b', which is managed by the author.

.PARAMETER User
    The user to use for the Graph API. Defaults to 'me'. Use string like user/GUID to request notebooks from other users if the permissions are set correctly.

.PARAMETER Notebook
    The name of the notebook to export. Can be specified multiple times. If not specified, all notebooks will be exported.

.PARAMETER All
    If specified, all notebooks will be exported.

.PARAMETER Path
    The path to export the notebooks to. If the path does not exist, it will be created.

.EXAMPLE
    ./Export-OneNote.ps1 -Notebook 'My Notebook' -Path "$home/freedomfromproprietaryformats"

#>


[CmdletBinding(DefaultParameterSetName = 'Notebook')]
param
(
    [Parameter(ParameterSetName = 'Notebook')]
    [Parameter(ParameterSetName = 'All')]
    [string]
    $TenantId = 'Common',

    # Use mine or create your own
    [Parameter(ParameterSetName = 'Notebook')]
    [Parameter(ParameterSetName = 'All')]
    [string]
    $OneNoteAppClientId = '812899b7-584c-4812-8aee-11d3e164d58b',

    [string]
    $User = 'me',

    [Parameter(Mandatory = $true, ParameterSetName = 'Notebook')]
    [string[]]
    $Notebook,

    [Parameter(Mandatory = $true, ParameterSetName = 'All')]
    [switch]
    $All,

    [Parameter(Mandatory = $true, ParameterSetName = 'Notebook')]
    [Parameter(Mandatory = $true, ParameterSetName = 'All')]
    $Path,

    [switch]
    $UseDeviceCode
)

if ($UseDeviceCode.IsPresent) {
    Connect-GraphDeviceCode -TenantId $TenantId -ClientId $OneNoteAppClientId
}
else {
    Connect-GraphBrowser -TenantId $TenantId -ClientId $OneNoteAppClientId -Scopes User.Read, Notes.Read, Notes.Read.All
}

Set-GraphEndpoint -Type beta

$notebooks = if ($All.IsPresent) {
    Invoke-GraphRequest -Query "$($User)/onenote/notebooks"
}
else {
    $Notebook | ForEach-Object {
        Invoke-GraphRequest -Query "$($User)/onenote/notebooks?`$filter=displayName eq '$($_ -replace "'", "''")'"        
    }
}

if (-not (Test-Path $Path)) {
    $null = New-Item -Path $Path -ItemType Directory -Force
}

$mg = Get-Module -Name MiniGraph
$token = & $mg { $script:token }

Write-Verbose -Message "Exporting $($notebooks.count) notebooks to $Path"

$allPages = [System.Collections.ArrayList]::new()
$apiCount = 1
foreach ($book in $notebooks) {
    $apiCount++
    Write-Verbose -Message "Exporting notebook $($book.displayName)"
    $sections = Invoke-GraphRequest -Query "$($User)/onenote/notebooks/$($book.id)/sections"


    foreach ($section in $sections) {
        $apiCount++
        Write-Verbose -Message "Exporting section $($section.displayName)"

        [array]$pages = Invoke-GraphRequest -Query "$($User)/onenote/sections/$($section.id)/pages" | ForEach-Object { $_ | Add-Member -NotePropertyName Notebook -NotePropertyValue $book.displayName -PassThru | Add-Member -NotePropertyName Section -NotePropertyValue $section.displayName -PassThru }
        $allPages.AddRange($pages)
    }
}

# One call per page at least,
if ($allPages.Count -gt (400 - $apiCount)) {
    $duration = [timespan]::FromHours($allPages.Count / 400)
    Write-Warning -Message "Microsoft imposes a limit of 400 requests per hour. You have $($allPages.Count) pages to export, so it will likely take $duration to finish."
}

foreach ($page in $allPages) {
    $bookPath = Join-Path -Path $Path -ChildPath ($page.Notebook -replace '[\\\/\:\*\?\"\<\>\|]', '_')
    $sectionPath = Join-Path -Path $bookPath -ChildPath ($page.Section -replace '[\\\/\:\*\?\"\<\>\|]', '_')
    if (-not (Test-Path -Path $sectionPath)) {
        $null = New-Item -Path $sectionPath -ItemType Directory -Force
    }

    Write-Verbose -Message "Exporting page $($page.title)"
    $sanitizedTitle = $page.title -replace '[\\\/\:\*\?\"\<\>\|]', '_'
    $pagePath = Join-Path -Path $sectionPath -ChildPath "$($page.createdDateTime.ToString('yyyy-MM-dd'))_$($sanitizedTitle).md"
    $content = Invoke-GraphRequest -Query "$($User)/onenote/pages/$($page.id)/content"
    $imgCount = 0
    foreach ($image in $content.SelectNodes("//img")) {
        $header = @{
            Authorization = "Bearer $token"
        }
        $imgName = '{0}_{1:d10}.png' -f $sanitizedTitle, $imgCount
        $imgPath = Join-Path -Path $sectionPath -ChildPath resources
        if (-not (Test-Path -Path $imgPath)) {
            $null = New-Item -Path $imgPath -ItemType Directory -Force
        }

        Invoke-RestMethod -Method Get -Uri $image.'data-fullres-src' -Headers $header -OutFile (Join-Path $imgPath $imgName)

        $image.src = [uri]::EscapeUriString(('./resources/{1}' -f $section.displayName, $imgName))
        $imgCount++
    }

    $content.OuterXml | ConvertFrom-HTMLToMarkdown -DestinationPath $pagePath -Format
}