michelderooij / Get-EventSession

Script to assist in downloading Microsoft event contents or return session information.
MIT License
69 stars 29 forks source link

Can't download videos #28

Closed kevineverts closed 1 year ago

kevineverts commented 1 year ago

I am trying to download sessions from Ignite using the command ".\Get-EventSession.ps1 -Event Ignite -DownloadFolder C:\Temp\Ignite -ScheduleCode KEY02H"

I get the error "Problem downloading video KEY02H Fireside Chat with Scott Guthrie and Alysa Taylor: How Customers Build Agility and Drive Innovation with the Microsoft Cloud:" and "Problem downloading slidedeck KEY02H Fireside Chat with Scott Guthrie and Alysa Taylor: How Customers Build Agility and Drive Innovation with the Microsoft Cloud"

Any ideas on what the issue may be and how I can fix it?

seanknoll commented 1 year ago

I'm also experiencing the same issue. I thought I was going crazy! The powerpoint slides will download fine, but there's always a problem downloading the videos. I've tried on various devices (both managed by Intune and unmanaged), but get the same error every time.

jrassi06 commented 1 year ago

yeah, this script doesn't work anymore, its keying in on a download link but the sessions don't list a download link in the json return. also, it's incredibly slow to download files, bits should be used instead of a basic wget.

Edit: there must be an error in some of his logic handling because it kept defaulting to the else statement. try this code, should also be faster.

.\Get-EventSession.ps1 -Event Ignite2022 -DownloadFolder D:\Ignite22 -PreferDirect -MaxDownloadJobs 15

`<#
    .SYNOPSIS
    Script to assist in downloading Microsoft Ignite, Inspire, Build or MEC contents, or return 
    session information for easier digesting. Video downloads will leverage external utilities, 
    depending on the used video format. To prevent retrieving session information for every run,
    the script will cache session information.

    Be advised that downloading of OnDemand contents from Azure Media Services is throttled to real-time
    speed. To lessen the pain, the script performs simultaneous downloads of multiple videos streams. Those
    downloads will each open in their own (minimized) window so you can track progress. Finally, CTRL-C
    is catched by the script because we need to stop download jobs when aborting the script.

    THIS CODE IS MADE AVAILABLE AS IS, WITHOUT WARRANTY OF ANY KIND. THE ENTIRE
    RISK OF THE USE OR THE RESULTS FROM THE USE OF THIS CODE REMAINS WITH THE USER.

    Michel de Rooij             http://eightwone.com
    Version 3.80, October 14th, 2022

    Special thanks to:
    Mattias Fors            http://deploywindows.info
    Scott Ladewig           http://ladewig.com
    Tim Pringle                 http://www.powershell.amsterdam
    Andy Race                   https://github.com/AndyRace
    Richard van Nieuwenhuizen

    .DESCRIPTION
    This script can download Microsoft Ignite, Inspire, Build and MEC session information and available 
    slidedecks and videos using MyIgnite/MyInspire/MyBuild techcommunity portal.

    Video downloads will leverage one or more utilities:
    - YouTube-dl, which can be downloaded from https://yt-dl.org/latest/youtube-dl.exe. This utility
      needs to reside in the same folder as the script. The script itself will try to download this 
      utility when the utility is not present.
    - ffmpeg, which can be downloaded from https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-latest-win32-static.zip. 
      This utility needs to reside in the same folder as the script, or you need to specify its location using -FFMPEG. 
      The utility is used to bind the seperate video and audio streams of Azure Media Services files 
      in single files.

    When you are interested in retrieving session information only, you can use the InfoOnly switch.

    Note: MEC sessions are not published through the usual API, so I worked around it by digesting its playlist as
    if it were a catalog. Consequence is that filtering might be limited, eg. no Category or Product etc.

    .PARAMETER DownloadFolder
    Specifies location to download sessions to. When omitted, will use 'systemdrive'\'Event'.

    .PARAMETER Format
    The Format specified depends on the media hosting the source videos:
    - Direct Downloads
    - Azure Media Services
    - YouTube

    Azure Media Services
    ====================
    For Azure Media Services, default option is worstvideo+bestaudio/best. Alternatively, you can 
    select other formats (when present), e.g. bestvideo+bestaudio. Note that the format requested 
    needs to be present in the stream package. Storage required for bestvideo is significantly 
    more than worstvideo. Note that you can also provide complex filter and preference, e.g. 
    bestvideo[height=540][filesize<384MB]+bestaudio,bestvideo[height=720][filesize<512MB]+bestaudio,bestvideo[height=360]+bestaudio,bestvideo+bestaudio
    1) This would first attempt to download the video of 540p if it is less than 384MB, and best audio.
    2) When not present, then attempt to downlod video of 720p less than 512MB.
    3) Thirdly, attempt to download video of 360p with best audio.
    4) If none of the previous filters found matches, just pick the best video and best audio streams.

    For Azure Media Services, you could also use format tags, such as 1_V_video_1 or 1_V_video_3.
    Note that these formats might not be consistent for different streams, e.g. 1_V_video_1
    might represent 1280x720 in one stream, while corresponding to 960x540 in another. To 
    prevent this, usage of filters is recommended.

    YouTube
    =======
    For YouTube videos, you can use the following formats:
    160          mp4        256x144    DASH video  108k , avc1.4d400b, 30fps, video only
    133          mp4        426x240    DASH video  242k , avc1.4d400c, 30fps, video only
    134          mp4        640x360    DASH video  305k , avc1.4d401e, 30fps, video only
    135          mp4        854x480    DASH video 1155k , avc1.4d4014, 30fps, video only
    136          mp4        1280x720   DASH video 2310k , avc1.4d4016, 30fps, video only
    137          mp4        1920x1080  DASH video 2495k , avc1.640028, 30fps, video only
    18           mp4        640x360    medium , avc1.42001E,  mp4a.40.2@ 96k
    22           mp4        1280x720   hd720 , avc1.64001F,  mp4a.40.2@192k (best, default)

    You can use filters or priority when selecting the media:
    - Filters allow you to put criteria on the media you select to download, e.g. 
      "bestvideo[height<=540]+bestaudio" will download the video stream where video is 540p at 
      most plus the audio stream (and ffmpeg will combine the two to a single MP4 file). It 
      allows you also to do cool things like "bestvideo[filesize<200M]+bestaudio". 
    - Priority allows you to provide additional criteria if the previous one fails, such as 
      when a desired quality is not available, e.g. "bestvideo+bestaudio/worstvideo+bestaudio" 
      will download worst video and best audio stream when the best video and audio streams 
      are not present.

    Format selection filter courtesey of Youtube-DL; for more examples, see 
    https://github.com/ytdl-org/youtube-dl/blob/master/README.md#format-selection-examples

    Direct Downloads
    ================
    Direct Downloads are downloaded directly from the provided downloadVideoLink source.

    .PARAMETER Captions
    When specified, for Azure Media Services contents, downloads caption files where available. 
    Files are usually in VTT format, and playable by VLC Player a.o. Note that captions might not always 
    be accurate due to machine translation, but at least will help in following the story :)

    .PARAMETER Subs
    When specified, for YouTube contents, downloads subtitles in provided languages by specifying one 
    or more 2-letter language codes seperated by a comma, e.g. en,fr,de,nl. Downloaded subtitles may be
    in VTT or SRT format. Again, the subtitles might not always be accurate due to machine translation.

    .PARAMETER Language
    When specified, for Azure Media hosted contents, downloads videos with specified audio stream where
    available. Note that if you mix this with specifying your own Format parameter, you need to
    add the language in the filter yourself, e.g. bestaudio[format_id*=German]. Default value is English, 
    as otherwise YouTube will download the last audio stream from the manifest (which often is Spanish).

    .PARAMETER Keyword
    Only retrieve sessions with this keyword in their session description.

    .PARAMETER Title
    Only retrieve sessions with this keyword in their session title.

    .PARAMETER Speaker
    Only retrieve sessions with this speaker.

    .PARAMETER Product
    Only retrieve sessions for this product. You need to specify the full product, subproducts seperated
    by '/', e.g. 'Microsoft 365/Office 365/Office 365 Management'. Wildcards are allowed.

    .PARAMETER Category
    Only retrieve sessions for this category. You need to specify the full category, subcategories seperated
    by '/', e.g. 'M365/Admin, Identity & Mgmt'. Wildcards are allowed.

    .PARAMETER SolutionArea
    Only retrieve sessions for this solution area. You need to specify the full 
    name, e.g. 'Modern Workplace'. Wildcards are allowed.

    .PARAMETER LearningPath
    Only retrieve sessions part of this this learningPath. You need to specify 
    the full name, e.g. 'Data Analyst'. Wildcards are allowed.

    .PARAMETER Topic
    Only retrieve sessions for this topic area. Wildcards are allowed.

    .PARAMETER ScheduleCode
    Only retrieve sessions with this session code. You can use one or more codes.

    .PARAMETER NoVideos
    Switch to indicate you don't want to download videos.

    .PARAMETER NoSlidedecks
    Switch to indicate you don't want to download slidedecks.

    .PARAMETER NoGuessing
    Switch to indicate you don't want the script to try to guess the URLs to retrieve media from MS Studios.

    .PARAMETER NoRepeats
    Switch to indicate you don't want the script to download repeated sessions.

    .PARAMETER FFMPEG
    Specifies full location of ffmpeg.exe utility. When omitted, it is searched for and
    when required extracted to the current folder.

    .PARAMETER MaxDownloadJobs
    Specifies the maximum number of concurrent downloads.

    .PARAMETER Proxy
    Specify the URI of the proxy to use, e.g. http://proxy:8080. When omitted, the current 
    system settings will be used.

    .PARAMETER Start
    Item number to start crawling with - useful for restarts.

    .PARAMETER Event
    Specify what event to download sessions for. 
    Options are:
    - Ignite                                                                     : Ignite events (current)
    - Ignite2022,Ignite2021H2, Ignite2021H1, Ignite2020, Ignite2019, Ignite2018  : Ignite contents from that year/time
    - Inspire                                                                    : Inspire contents (current)
    - Inspire2022,Inspire2021, Inspire2020                                       : Inspire contents from that year
    - Build                                                                      : Build contents (current)
    - Build2022,Build2021,Build2020                                              : Build contents from that year
    - MEC                                                                        : MEC contents

    .PARAMETER OGVPicker
    Specify that you want to pick sessions to download using Out-GridView.

    .PARAMETER InfoOnly
    Tells the script to return session information only.
    Note that by default, only session code and title will be displayed.

    .PARAMETER Overwrite
    Skips detecting existing files, overwriting them if they exist.

    .PARAMETER PreferDirect
    Instructs script to prefer direct video downloads over Azure Media Services, when both are 
    available. Note that direct downloads may be faster, but offer only single quality downloads, 
    where AMS may offer multiple video qualities.

    .PARAMETER Timestamp
    Tells script to change the timestamp of the downloaded media files to match the original 
    session timestamp, when available.

    .PARAMETER Locale
    When supported by the event, filters sessions on localization. 
    Currently supported: de-DE, zh-CN, en-US, ja-JP, es-CO, fr-FR. 
    When omitted, defaults to en-US.

    .NOTES
    The youtube-dl.exe utility requires Visual C++ 2010 redist package
    https://www.microsoft.com/en-US/download/details.aspx?id=5555

    Changelog
    =========
    2.0   Initial (Mattias Fors)
    2.1   Added video downloading, reformatting code (Michel de Rooij)
    2.11  Fixed titles with apostrophes
          Added Keyword and Title parameter
    2.12  Replaced pptx download Invoke-WebRequest with .NET webclient request (=faster)
          Fixed titles with backslashes (who does that?)
    2.13  Adjusts pptx timestamp to publishing timestamp
    2.14  Made filtering case-insensitive
          Added NoVideos to download slidedecks only
    2.15  Fixed downloading of differently embedded youtube videos
          Added timestamping of downloaded pptx files
          Minor output changes
    2.16  More illegal character fixups
    2.17  Bumped max post to check to 1750
    2.18  Added option to download for sessions listed in a schedule shared from MyIgnite
          Added lookup of video YouTube URl from MyIgnite if not found in TechCommunity
          Added check to make sure conversation titles begin with session code
          Added check to make sure we skip conversations we've already checked since some RSS IDs are duplicates
    2.19  Added trimming of filenames
    2.20  Incorporated Tim Pringle's code to use JSON to acess MyIgnite catalog
          Added option to select speaker
          Added caching of session information (expires in 1 day, or remove .cache file)
          Removed Start parameter (we're now pre-reading the catalog)
    2.21  Added proxy support, using system configured setting
          Fixed downloading of slidedecks
    2.22  Added URL parameter
          Renamed script to IgniteDownloader.ps1
    2.5   Added InfoOnly switch
          Added Product parameter
          Renamed script to Get-IgniteSession.ps1
    2.6   Fixed slide deck downloading
          Added Overwrite switch
    2.61  Added placeholder slide deck removal
    2.62  Fixed Overwrite logic bug
          Renamed to singular Get-IgniteSession to keep in line with PoSH standards
    2.63  Fixed bug reporting failed pptx download
          Added reporting of placeholder decks and videos
    2.64  Added processing of direct download links for videos
    2.65  Added option to specify multiple sessionCode codes
          Added note in source that format only works for YouTube video downloads.
          Added youtube-dl returncode check in case it won't run (e.g. missing VC library).
    2.66  Added proper downloading of session info using UTF-8 (no more '???')
          Additional trimming of spaces and CRLF's in property values
    2.7   Added Event parameter to switch between Ignite and Inspire catalog
          Renamed script to Get-EventSession
          Changed cached session info name to include event
          Removed obsolete URL parameter
          Added code to download slidedecks in PDF (Inspire)
          Cleanup of script synopsis/description/etc.
    2.8   Added downloading of Azure Media Services hosted streaming media
          Added simultaneous downloading of AMS hosted OnDemand streams
          Added NoSlidedecks switch
    2.9   Added Category parameter
          Fixed searching on Product
          Increased itemsPerPage when retrieving catalog
    2.91  Update to video downloading routine due to changes in published session info
    2.92  Fix 'Could not create SSL/TLS secure channel' issues with Invoke-WebRequest
    2.93  Update to slidedeck downloading routine due to changes in published session info
    2.94  Fixed cleanup of finished jobs
    2.95  Fixed encoding of filenames
    2.96  Fixed terminating cleanup when no slidedecks are being downloaded
          Added testing for contents to show contents is not available rather than generic 'problem'
    2.97  Update to change in video downloading location (YouTube)
          Changed default Format due to switch in video hosting - see YouTube format table
    2.971 Changed regex for YouTube matching to skip 'Coming Soon'
          Made verbose mode less noisy
    2.98  Converted background downloads to single background job queue
          Cosmetics
    2.981 Added cleanup of occasional leftovers (eg *.mp4.f5_A_aac_UND_2_192_1.ytdl, *.f1_V_video_3.mp4)
    2.982 Minor tweaks
    2.983 Added OGVPicker switch
    2.984 Changed keyword search to description, not abstract
          Fixed searching for Products and Category
          Added searching for SolutionArea
          Added searching for LearningPath
    2.985 Added Proxy support
    2.986 Minor update to accomodate publishing of slideDecks links
    3.0   Added Build support
    3.01  Added CTRL-Break notice to 'waiting for downloads' message
          Fixed 'No video located for' message
    3.1   Updated to work with the Inspire 2019 catalog
          Cosmetics
    3.11  Some more Cosmetics
    3.12  Updated to work with current Ignite & Build catalogs
          Bumped the download retry limits for YouTube-dl a bit
    3.13  Updated Ignite catalog endpoints
    3.14  Removed superfluous testing loading of main event page
          Fixed LearningPath option verbose output
          Some code cosmetics
    3.15  Added Topic parameter
    3.16  Corrected prefixes for Ignite 2019
    3.17  Added NoGuess switch
          Added NoRepeats switch
    3.18  Added Ignite2018 event
    3.19  Fixed video downloading
    3.20  Fixed background job cleanup
    3.21  Added Timestamp switch
          Updated file naming to strip embbeded name of format, e.g. f1_V_video_3
          Added stopping of Youtube-DL helper app spawned processes
    3.22  Added skipping of processing future sessions
    3.23  Added Captions switch and Subs parameter
          Added skipping of additional repeats (schedule code ending in R2/R3)
          Fixed filename construction containing '%'
          Added filtering options to description of Format parameter
          Decreased probing/retrieving video URLs from Azure Media Services (speed benefit)
    3.24  Added PreferDirect switch
          Enhanced Format parameter description
    3.25  Updated Youtube-DL download URL
    3.26  Updated mutual exclusion for PreferDirect & other parameters/switches
          Added workaround for long file names (NT Style name syntax)
          Added PowerShell ISE detection
          Added Garbage Collection
    3.27  Reworked jobs for downloading videos
          Added status bars for downloading of videos
          Failed video downloads will show last line of error output
          Added replacement of square brackets in file names
          Removed obsolete Clean-VideoLeftOvers call
    3.28  Uncommented line to cleanup output files after downloading video
          Changed 'Error' lines to single line outputs or throws (where appropriate)
    3.29  Added 'Stopped downloading ..' messages when terminating
    3.30  Increased wait cycle during progress refresh
          Added schedule code to progress status
          Revised detection successful video downloads
    3.31  Corrected video cleanup logic
    3.32  Do not assume Slidedeck exists when size is 0
    3.33  Fixed typo when specifying format for direct YouTube downloads
    3.34  Updated for Build 2020
          Added NoRepeat filtering for Build 2020
          Made Event parameter mandatory, and not defaulting to Ignite
          Added filtering example to Format parameter spec
    3.35  Updated for Inspire 2020
    3.36  Small fix for Inspire repeat session naming
    3.37  Added ExcludecommunityTopic parameter (so you can skip 'Fun and Wellness' Animal Cam contents)
          Modified Keyword and Title parameters (can be multiple values now)
    3.38  Added detection of filetype for presentations (PPTX/PDF)
    3.39  Added code to deal with specifying <Event><Year>
    3.40  Modified API endpoint for Ignite 2020
          Changed yearless Event specification to add year suffix, eg Ignite->Ignite2020, etc.
          Fixed Azure Media Services video scraping for Ignite2020
    3.41  Fixed: Error message for timeless sessions after downloading caption file
          Fixed: Downloading of caption files when video file is already downloaded
    3.42  Changed source location of ffmpeg. Download will now fetch current static x64 release.
    3.43  Fixed Ignite 2020 slidedeck 'trial & error' URL
    3.44  Fixed downloading of non-PDF slidedecks
    3.45  Help updated for -Event
    3.46  Changed downloading of caption files in background jobs as well
          Optimized caption downloading preventing unnecessary page downloads
    3.47  Added Captions to PreferDirect command set
    3.50  Updated for Ignite 2021
          Small cleanup
    3.51  Updated for Build 2021
    3.52  Updated NoRepeats maximum repeat check
          Added Language parameter to support Azure Media Services hosted videos containing multiple audio tracks
    3.53  Updated for Inspire 2021 
    3.54  Fixed adding Language filter when complex Format is specified
    3.55  Fixed audio stream selection when requested language is not available or only single audio stream is present
    3.60  Added support for Ignite 2021; specify individual event using Ignite2021H1 (Spring) or Ignite2021H2 (Fall)
    3.61  Added support for (direct) downloading of Ignite Fall 2021 videos
    3.62  Added Cleanup video leftover files if video file exists (to remove clutter)
          Changed lifetime of cached session information to 8 hours
          Fixed post-download counts
    3.63  Fixed keyword filtering
    3.64  Changed filter so that default language is picked when specified language is not available
    3.65  Updated for Build 2022
          Added Locale parameter to filter local content
          Fixed applying timestamp due to DateTime formatting changes
    3.66  Fixed filtering on langLocale
          Default Locale set to en-US
    3.67  Added removal of placeholder deck/video/vtt files
    3.68  Fixed caching when specifying Event without year tag, eg. Build vs Build2022
          Removed default Locale as that would mess things up for Events where data does not contain that information (yet).
    3.69  Updated for Inspire 2022
    3.70  Added MEC support
    3.71  Fixed MEC description & speaker parsing
    3.72  Fixed usage of format & subs arguments for direct YouTube downloads
    3.73  Added MEC slide deck support
          Fixed MEC parsing of description
    3.74  Fixed MEC processing of multi-line descriptions
    3.75  Added Ignite 2022 support
    3.76  Removed session code uniqueness when storing session data, as session data now can contain multiple entries per language using the same code
    3.77  Corrected API endpoints for some of the older events
    3.78  Fixed content-based help
    3.79  Fixed issue with placeholder detection
          Fixed path handling, fixes file detection and timestamping a.o.
          Added PowerShell 5.1 requirement (tested with)
    3.80  Fixed redundant passing of Format to YouTube-dl

    .EXAMPLE
    Download all available contents of Ignite sessions containing the word 'Teams' in the title to D:\Ignite, and skip sessions from the CommunityTopic 'Fun and Wellness'
    .\Get-EventSession.ps1 -DownloadFolder D:\Ignite-Format 22 -Keyword 'Teams' -Event Ignite -ExcludecommunityTopic 'Fun and Wellness'

    .EXAMPLE
    Get information of all sessions, and output only location and time information for sessions (co-)presented by Tony Redmond:
    .\Get-EventSession.ps1 -InfoOnly | Where {$_.Speakers -contains 'Tony Redmond'} | Select Title, location, startDateTime

    .EXAMPLE
    Download all available contents of sessions BRK3248 and BRK3186 to D:\Ignite
    .\Get-EventSession.ps1 -DownloadFolder D:\Ignite -ScheduleCode BRK3248,BRK3186

    .EXAMPLE
    View all Exchange Server related sessions as Ignite including speakers(s), and sort them by date/time
    Get-EventSession.ps1 -Event Ignite -InfoOnly -Product '*Exchange Server*' | Sort-Object startDateTime | Select-Object @{n='Session'; e={$_.sessionCode}}, @{n='When';e={([datetime]$_.startDateTime).ToString('g')}}, title, @{n='Speakers'; e={$_.speakerNames -join ','}}

    .EXAMPLE
    Get all available sessions, display them in a GridView to select multiple at once, and download them to D:\Ignite
    .\Get-EventSession.ps1 -ScheduleCode (.\Get-EventSession.ps1 -InfoOnly | Out-GridView -Title 'Select Videos to Download, or Cancel for all Videos' -PassThru).SessionCode -MaxDownloadJobs 10 -DownloadFolder 'D:\Ignite'
#>
[cmdletbinding( DefaultParameterSetName = 'Default' )]
param(
    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$DownloadFolder,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [string]$Format,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string[]]$Keyword,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string[]]$Title,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$Speaker,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$Product,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$Category = '',

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$SolutionArea,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$LearningPath,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$Topic,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string[]]$ScheduleCode,

    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string[]]$ExcludecommunityTopic,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [switch]$NoVideos,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [switch]$NoSlidedecks,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [string]$FFMPEG,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [ValidateRange(1,128)] 
    [int]$MaxDownloadJobs=4,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [uri]$Proxy=$null,

    [parameter( Mandatory = $true, ParameterSetName = 'Download')]
    [parameter( Mandatory = $true, ParameterSetName = 'Default')]
    [parameter( Mandatory = $true, ParameterSetName = 'Info')]
    [parameter( Mandatory = $true, ParameterSetName = 'DownloadDirect')]
    [ValidateSet('MEC','MEC2022','Ignite', 'Ignite2022', 'Ignite2021H1', 'Ignite2021H2', 'Ignite2020', 'Ignite2019', 'Ignite2018', 'Inspire', 'Inspire2022', 'Inspire2021', 'Inspire2020', 'Build', 'Build2022', 'Build2021', 'Build2020')]
    [string]$Event='',

    [parameter( Mandatory = $true, ParameterSetName = 'Info')]
    [switch]$InfoOnly,

    [parameter( Mandatory = $true, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [switch]$OGVPicker,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [switch]$Overwrite,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [switch]$NoRepeats,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [switch]$NoGuessing,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [switch]$Timestamp,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [string[]]$Subs,

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [string]$Language='English',

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'Info')]
    [ValidateSet('de-DE','zh-CN','en-US','ja-JP','es-CO','fr-FR')]
    [string[]]$Locale='en-US',

    [parameter( Mandatory = $false, ParameterSetName = 'Download')]
    [parameter( Mandatory = $false, ParameterSetName = 'Default')]
    [parameter( Mandatory = $false, ParameterSetName = 'DownloadDirect')]
    [switch]$Captions,

    [parameter( Mandatory = $true, ParameterSetName = 'DownloadDirect')]
    [switch]$PreferDirect
)

    # Max age for cache, older than this # hours will force info refresh
    $MaxCacheAge = 24
    $ProgressPreference = 'SilentlyContinue'

    $YouTubeDL = Join-Path $PSScriptRoot 'youtube-dl.exe'
    $FFMPEG= Join-Path $PSScriptRoot 'ffmpeg.exe'

    $YTlink = 'https://github.com/ytdl-org/youtube-dl/releases/download/2019.11.05/youtube-dl.exe'
    $FFMPEGlink = 'https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip'

    # Fix 'Could not create SSL/TLS secure channel' issues with Invoke-WebRequest
    [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls" 

    $script:BackgroundDownloadJobs= @()

    Function Iif($Cond, $IfTrue, $IfFalse) {
        If( $Cond) { $IfTrue } Else { $IfFalse }
    }

    Function Fix-FileName ($title) {
        return (((((((($title -replace '\]', ')') -replace '\[', '(') -replace [char]0x202f, ' ') -replace '["\\/\?\*]', ' ') -replace ':', '-') -replace '  ', ' ') -replace '\?\?\?', '') -replace '\<|\>|:|"|/|\\|\||\?|\*', '').Trim()
    }

    Function Get-IEProxy {
        If ( (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings').ProxyEnable -ne 0) {
            $proxies = (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings').proxyServer
            if ($proxies) {
                if ($proxies -ilike "*=*") {
                    return $proxies -replace "=", "://" -split (';') | Select-Object -First 1
                }
                Else {
                    return ('http://{0}' -f $proxies)
                }
            }
            Else {
                return $null
            }
        }
        Else {
            return $null
        }
    }

    Function Test-ResolvedPath( $Path) {
        $null -ne (Get-ChildItem -LiteralPath $Path -ErrorAction SilentlyContinue)
    }

    Function Clean-VideoLeftovers ( $videofile) {
        $masks= '.*.mp4.part', '.*.mp4.ytdl'
    ForEach( $mask in $masks) {
            $FileMask= $videofile -replace '.mp4', $mask
            Get-Item -LiteralPath $FileMask -ErrorAction SilentlyContinue | ForEach-Object {
                Write-Verbose ('Removing leftover file {0}' -f $_.fullname)
            Remove-Item -LiteralPath $_.fullname -Force -ErrorAction SilentlyContinue
            }
        }
    }

    Function Get-BackgroundDownloadJobs {
        $Temp= @()
        ForEach( $job in $script:BackgroundDownloadJobs) {

            switch( $job.Type) {
                1 {
                    $isJobRunning= $job.job.State -eq 'Running'
                }
                2 {
                    $isJobRunning= -not $job.job.hasExited
                }
                3 {
                    $isJobRunning= $job.job.State -eq 'Running'
                }
                default {
                    $isJobRunning= $false
                }
            }
            if( $isJobRunning) {
                $Temp+= $job
            }
            Else {
                # Job finished, process result
                switch( $job.Type) {
                    1 {
                        $isJobSuccess= $job.job.State -eq 'Completed'
                        $DeckInfo[ $InfoDownload]++
                    }
                    2 {
                        $isJobSuccess= Test-ResolvedPath -Path $job.file
                        $VideoInfo[ $InfoDownload]++
                        Write-Progress -Id $job.job.Id -Activity ('Video {0} {1}' -f $Job.scheduleCode, $Job.title) -Completed
                    }
                    3 {
                        $isJobSuccess= $job.job.State -eq 'Completed'
                    }
                    default {
                        $isJobSuccess= $false
                    }
                }

                # Test if file is placeholder
                $isPlaceholder= $false
        If( Test-ResolvedPath -Path $job.file) {
                    $FileObj= Get-ChildItem -LiteralPath $job.file
                    If( $FileObj.Length -eq 42) {

                        If( (Get-Content -LiteralPath $job.File) -eq 'No resource file is available for download') {
                            Write-Warning ('Removing {0} placeholder file {1}' -f $job.scheduleCode, $job.file)
                            Remove-Item -LiteralPath $job.file -Force
                            $isPlaceholder= $true

                            Switch( $job.Type) {
                                1 {
                                    # Placeholder Deck file downloaded
                                    $DeckInfo[ $InfoDownload]--
                                    $DeckInfo[ $InfoPlaceholder]++
                                }
                                2 {
                                    # Placeholder Video file downloaded
                                    $VideoInfo[ $InfoDownload]--
                                    $VideoInfo[ $InfoPlaceholder]++
                                }
                                3 {
                                    # Placeholder VTT file downloaded
                                }
                            }
                        }
                        Else {
                            # Placeholder different text?
                        }
                    }
                }

        If( $isJobSuccess -and -not $isPlaceholder) {

                    Write-Host ('Downloaded {0}' -f $job.file) -ForegroundColor Green

                    # Do we need to adjust timestamp
                    If( $job.Timestamp) {
                        #Set timestamp
                        Write-Verbose ('Applying timestamp {0} to {1}' -f $job.Timestamp, $job.file)
                        $FileObj= Get-ChildItem -LiteralPath $job.file
                        $FileObj.CreationTime= Get-Date -Date $job.Timestamp
                        $FileObj.LastWriteTime= Get-Date -Date $job.Timestamp
                    }

                    If( $job.Type -eq 2) {
                        # Clean video leftovers
                        Clean-VideoLeftovers $job.file
                    }
                }
                Else {
                    switch( $job.Type) {
                        1 {
                            Write-Host ('Problem downloading slidedeck {0} {1}' -f $job.scheduleCode, $job.title) -ForegroundColor Red
                            $job.job.ChildJobs | Stop-Job | Out-Null
                            $job.job | Stop-Job -PassThru | Remove-Job -Force | Out-Null
                        }
                        2 {
                            $LastLine= (Get-Content -LiteralPath $job.stdErrTempFile -ErrorAction SilentlyContinue) | Select-Object -Last 1
                            Write-Host ('Problem downloading video {0} {1}: {2}' -f $job.scheduleCode, $job.title, $LastLine) -ForegroundColor Red
                            Remove-Item -LiteralPath $job.stdOutTempFile, $job.stdErrTempFile -Force -ErrorAction Ignore
                        }
                        3 {
                            Write-Host ('Problem downloading captions {0} {1}' -f $job.scheduleCode, $job.title) -ForegroundColor Red
                            $job.job.ChildJobs | Stop-Job | Out-Null
                            $job.job | Stop-Job -PassThru | Remove-Job -Force | Out-Null
                        }
                        default {
                        }
                    }
                }
            }
        }
        $Num= ($Temp| Measure-Object).Count
        $script:BackgroundDownloadJobs= $Temp
        Show-BackgroundDownloadJobs 
        return $Num
    }

    Function Show-BackgroundDownloadJobs {
        $Num=0
        $NumDeck= 0
        $NumVid= 0
        $NumVtt= 0
        ForEach( $BGJob in $script:BackgroundDownloadJobs) {
            $Num++
            Switch( $BGJob.Type) {
                1 {
                     $NumDeck++
                }
                2 {
                     $NumVid++
                }
                3 {
                     $NumVtt++
                }
            }
        }
        Write-Progress -Id 2 -Activity 'Background Download Jobs' -Status ('Total {0} in progress ({1} slidedeck, {2} video and {3} caption files)' -f $Num, $NumDeck, $NumVid, $NumVtt)

        ForEach( $job in $script:BackgroundDownloadJobs) {
            If( $Job.Type -eq 2) {

                # Get last line of YT log to display for video downloads
                $LastLine= (Get-Content -LiteralPath $job.stdOutTempFile -ErrorAction SilentlyContinue) | Select-Object -Last 1
                If(!( $LastLine)) {
                    $LastLine= 'Evaluating..'
                }
                Write-Progress -Id $job.job.id -Activity ('Video {0} {1}' -f $job.scheduleCode, $Job.title) -Status $LastLine -ParentId 2
                $progressId++
            }
        }
    }

    Function Stop-BackgroundDownloadJobs {
        # Trigger update jobs running data
        $null= Get-BackgroundDownloadJobs
        # Stop all slidedeck background jobs
        ForEach( $BGJob in $script:BackgroundDownloadJobs ) { 
            Switch( $BGJob.Type) {
                1 {
                    $BGJob.Job.ChildJobs | Stop-Job -PassThru 
                $BGJob.Job | Stop-Job -PassThru | Remove-Job -Force -ErrorAction SilentlyContinue
                }
                2 {
                    Stop-Process -Id $BGJob.job.id -Force -ErrorAction SilentlyContinue
                    Start-Sleep -Seconds 5
                    Remove-Item -LiteralPath $BGJob.stdOutTempFile, $BGJob.stdErrTempFile -Force -ErrorAction Ignore
                }
                3 {
                    $BGJob.Job.ChildJobs | Stop-Job -PassThru 
                $BGJob.Job | Stop-Job -PassThru | Remove-Job -Force -ErrorAction SilentlyContinue
                }
            }
            Write-Warning ('Stopped downloading {0} {1}' -f $BGJob.scheduleCode, $BGJob.title) 
        }
    }

    Function Add-BackgroundDownloadJob {
        param(
            $Type,
            $FilePath,
            $DownloadUrl,
            $ArgumentList,
            $File,
            $Timestamp= $null,
            $Title='',
            $ScheduleCode=''
        )
        $JobsRunning= Get-BackgroundDownloadJobs
        If ( $JobsRunning -ge $MaxDownloadJobs) {
            Write-Host ('Maximum background download jobs reached ({0}), waiting for free slot - press Ctrl-C once to abort..' -f $JobsRunning)
            While ( $JobsRunning -ge $MaxDownloadJobs) {
                if ([system.console]::KeyAvailable) { 
                    Start-Sleep 1
                    $key = [system.console]::readkey($true)
                    if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
                        Write-Host "TERMINATING" -ForegroundColor Red
                        Stop-BackgroundDownloadJobs
                        Exit -1
                    }
                }
                $JobsRunning= Get-BackgroundDownloadJobs
            }
        }
        Switch( $Type) {
            1 {
                # Slidedeck
                $job= Start-Job -ScriptBlock { 
                    param( $url, $file) 
                    $wc = New-Object System.Net.WebClient
                    $wc.Encoding = [System.Text.Encoding]::UTF8
                    $wc.DownloadFile( $url, $file) 
                } -ArgumentList $DownloadUrl, $FilePath
                $stdOutTempFile = $null
                $stdErrTempFile = $null
            }
            2 {
                # Video
                $TempFile= Join-Path ($env:TEMP) (New-Guid).Guid
                $stdOutTempFile = '{0}-Out.log' -f $TempFile
                $stdErrTempFile = '{0}-Err.log' -f $TempFile
                $ProcessParam= @{
                    FilePath= $FilePath
                    ArgumentList= $ArgumentList
                    RedirectStandardError= $stdErrTempFile 
                    RedirectStandardOutput= $stdOutTempFile 
                    Wait= $false
                    Passthru= $true
                    NoNewWindow= $true
                    #WindowStyle= [System.Diagnostics.ProcessWindowStyle]::Normal
                }
                $job= Start-Process @ProcessParam
            }
            3 {
                # Caption
                $job= Start-Job -ScriptBlock { 
                    param( $url, $file) 
                    $wc = New-Object System.Net.WebClient
                    $wc.Encoding = [System.Text.Encoding]::UTF8
                    $wc.DownloadFile( $url, $file) 
                } -ArgumentList $DownloadUrl, $FilePath
                $stdOutTempFile = $null
                $stdErrTempFile = $null
            }
        }
        $object= New-Object -TypeName PSObject -Property @{
            Type= $Type
            job= $job
            file= $file
            title= $Title
            url= $DownloadUrl
            scheduleCode= $ScheduleCode
            timestamp= $timestamp
            stdOutTempFile= $stdOutTempFile
            stdErrTempFile= $stdErrTempFile
        }

        $script:BackgroundDownloadJobs+= $object
        Show-BackgroundDownloadJobs
    }

##########
# MAIN
##########
#Requires -Version 5.1

    If( $psISE) {
        Throw( 'Running from PowerShell ISE is not supported due to requirement to capture console input for proper termination of the script. Please run from a regular PowerShell session.')
    }

    If( $Proxy) {
        $ProxyURL= $Proxy
    }
    Else {
        $ProxyURL = Get-IEProxy
    }
    If ( $ProxyURL) {
        Write-Host "Using proxy address $ProxyURL"
    }
    Else {
        Write-Host "No proxy setting detected, using direct connection"
    }

    # Determine what event URLs to use. 
    # Use {0} for session code (eg BRK123), {1} for session id (guid)
    Switch( $Event) {
        {'MEC','MEC2022' -contains $_} {
            $EventName= 'MEC2022'
            $EventType='YT'
            $EventYTUrl= 'https://www.youtube.com/playlist?list=PLxdTT6-7g--2POisC5XcDQxUXHhWsoZc9'
            $EventLocale= 'en-us'
        }
        {'Ignite','Ignite2022' -contains $_} {
            $EventName= 'Ignite2022'
            $EventType='API'
            $EventAPIUrl= 'https://api.ignite.microsoft.com'
            $EventSearchURI= 'api/session/all'
            $SessionUrl= 'https://medius.studios.ms/Embed/video-nc/IG22-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/IG22-{0}'
            $SlidedeckUrl= 'https://medius.microsoft.com/video/asset/PPT/{0}'
            $Method= 'Post'
            # Note: to have literal accolades and not string formatter evaluate interior, use a pair:
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2022-10-12T08:00:00-05:00","endDateTime":"2022-10-14T19:00:00-05:00"}}]}}'
        }
        {'Ignite2021H2' -contains $_} {
            $EventName= 'Ignite2021H2'
            $EventType='API'
            $EventAPIUrl= 'https://api.ignite.microsoft.com'
            $EventSearchURI= 'api/session/all'
            $SessionUrl= 'https://medius.studios.ms/Embed/video-nc/IG21-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/IG21-{0}'
            $SlidedeckUrl= 'https://medius.microsoft.com/video/asset/PPT/{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2021-11-01T08:00:00-05:00","endDateTime":"2021-11-30T19:00:00-05:00"}}]}}'
        }
        {'Ignite2021H1' -contains $_} {
            $EventName= 'Ignite2021H1'
            $EventType='API'
            $EventAPIUrl= 'https://api.ignite.microsoft.com'
            $EventSearchURI= 'api/session/all'
            $SessionUrl= 'https://medius.studios.ms/Embed/video-nc/IG21-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/IG21-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/IG21-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2021-03-01T08:00:00-05:00","endDateTime":"2021-03-31T19:00:00-05:00"}}]}}'
        }
        {'Ignite2020' -contains $_} {
            $EventName= 'Ignite2020'
            $EventType='API'
            $EventAPIUrl= 'https://api.ignite.microsoft.com'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/Embed/video-nc/IG20-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/IG20-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/IG20-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2020-01-01T08:00:00-05:00","endDateTime":"2020-12-31T19:00:00-05:00"}}]}}'
        }
        {'Ignite2019' -contains $_} {
            $EventAPIUrl= 'https://api.ignite.microsoft.com'
            $EventType='API'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/Embed/Video/IG19-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/IG19-{0}'
            $SlidedeckUrl= 'https://mediusprodstatic.studios.ms/presentations/Ignite2019/{0}.pptx'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2019-01-01T08:00:00-05:00","endDateTime":"2019-12-31T19:00:00-05:00"}}]}}'
        }
        'Ignite2018' {
            $EventAPIUrl= 'https://api.ignite.microsoft.com'
            $EventType='API'
            $EventSearchURI= 'api/videos/search'
            $SessionUrl= 'https://medius.studios.ms/Embed/Video/IG18-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/IG18-{0}'
            $SlidedeckUrl= 'https://mediusprodstatic.studios.ms/presentations/Ignite2018/{0}.pptx'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2018-01-01T08:00:00-05:00","endDateTime":"2018-12-31T19:00:00-05:00"}}]}}'
        }
        {'Inspire', 'Inspire2022' -contains $_} {
            $EventName= 'Inspire2022'
            $EventType='API'
            $EventAPIUrl= 'https://api.inspire.microsoft.com'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/INSP22-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/INSP22-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/INSP22-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2022-07-19T08:00:00-05:00","endDateTime":"2022-07-20T19:00:00-05:00"}}]}}'
        }
        {'Inspire2021' -contains $_} {
            $EventName= 'Inspire2021'
            $EventType='API'
            $EventAPIUrl= 'https://api.inspire.microsoft.com'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/INSP21-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/INSP21-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/INSP21-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2021-01-01T08:00:00-05:00","endDateTime":"2021-12-31T19:00:00-05:00"}}]}}'
        }
        {'Inspire2020' -contains $_} {
            $EventName= 'Inspire2020'
            $EventType='API'
            $EventAPIUrl= 'https://api.inspire.microsoft.com'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/INSP20-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/INSP20-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/INSP20-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2020-01-01T08:00:00-05:00","endDateTime":"2020-12-31T19:00:00-05:00"}}]}}'
        }
        {'Inspire2019' -contains $_} {
            $EventAPIUrl= 'https://api.inspire.microsoft.com'
            $EventType='API'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/INSP19-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/INSP19-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/INSP19-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2019-01-01T08:00:00-05:00","endDateTime":"2019-12-31T19:00:00-05:00"}}]}}'
        }
        {'Build', 'Build2022' -contains $_} {
            $EventName= 'Build2022'
            $EventType='API'
            $EventAPIUrl= 'https://api.mybuild.microsoft.com'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/B21-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/B21-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/B21-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[]}}}}'
        }
        {'Build2021' -contains $_} {
            $EventName= 'Build2021'
            $EventType='API'
            $EventAPIUrl= 'https://api.mybuild.microsoft.com'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/B21-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/B21-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/B21-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[]}}}}'
        }
        {'Build2020' -contains $_} {
            $EventName= 'Build2020'
            $EventType='API'
            $EventAPIUrl= 'https://api.mybuild.microsoft.com'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/B20-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/B20-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/B20-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2020-01-01T08:00:00-05:00","endDateTime":"2020-12-31T19:00:00-05:00"}}]}}'
        }
        {'Build2019' -contains $_} {
            $EventName= 'Build2019'
            $EventAPIUrl= 'https://api.mybuild.microsoft.com'
            $EventType='API'
            $EventSearchURI= 'api/session/search'
            $SessionUrl= 'https://medius.studios.ms/video/asset/HIGHMP4/B19-{0}'
            $CaptionURL= 'https://medius.studios.ms/video/asset/CAPTION/B19-{0}'
            $SlidedeckUrl= 'https://medius.studios.ms/video/asset/PPT/B19-{0}'
            $Method= 'Post'
            $EventSearchBody = '{{"itemsPerPage":{0},"searchText":"*","searchPage":{1},"sortOption":"None","searchFacets":{{"facets":[],"personalizationFacets":[],"dateFacet":[{{"startDateTime":"2019-01-01T08:00:00-05:00","endDateTime":"2019-12-31T19:00:00-05:00"}}]}}'
        }
        default {
            Write-Host ('Unknown event: {0}' -f $Event) -ForegroundColor Red
            Exit -1
        }
    }

    If (-not ($InfoOnly)) {

        # If no download folder set, use system drive with event subfolder
        If( -not( $DownloadFolder)) {
            $DownloadFolder= '{0}\{1}' -f $ENV:SystemDrive, $EventName
        }

        Add-Type -AssemblyName System.Web
        Write-Host "Using download path: $DownloadFolder"
        # Create the local content path if not exists
        if ( (Test-Path $DownloadFolder) -eq $false ) {
            New-Item -LiteralPath $DownloadFolder -ItemType Directory | Out-Null
        }

        If ( $NoVideos) {
            Write-Host 'Will skip downloading videos'
            $DownloadVideos = $false
        }
        Else {
            If (-not( Test-Path $YouTubeDL)) {
                Write-Host ('youtube-dl.exe not found, will try to download from {0}' -f $YTLink)
                Invoke-WebRequest -Uri $YTLink -OutFile $YouTubeDL -Proxy $ProxyURL
            }
            If ( Test-Path $YouTubeDL) {
                Write-Host ('Running self-update of youtube-dl.exe')

                $Arg = @('-U')
                If ( $ProxyURL) { $Arg += "--proxy $ProxyURL" }

                $pinfo = New-Object System.Diagnostics.ProcessStartInfo
                $pinfo.FileName = $YouTubeDL
                $pinfo.RedirectStandardError = $true
                $pinfo.RedirectStandardOutput = $true
                $pinfo.UseShellExecute = $false
                $pinfo.Arguments = $Arg
                Write-Verbose ('Running {0} using {1}' -f $pinfo.FileName, ($pinfo.Arguments -join ' '))
                $p = New-Object System.Diagnostics.Process
                $p.StartInfo = $pinfo
                $p.Start() | Out-Null
                $stdout = $p.StandardOutput.ReadToEnd()
                $stderr = $p.StandardError.ReadToEnd()
                $p.WaitForExit()

                If ($p.ExitCode -ne 0) {
                    If ( $stderr -contains 'Error launching') {
                        Throw 'Problem running youtube-dl.exe. Make sure this is an x86 system, and the required Visual C++ 2010 redistribution package is installed (available from https://www.microsoft.com/en-US/download/details.aspx?id=5555).'
                    }
                    Else {
                        Write-Host $stderr
                    }
                }
                Else {
                    Write-Host $stdout
                }
                $DownloadVideos = $true
            }
            Else {
                Write-Warning 'Unable to locate or download youtube-dl.exe, will skip downloading YouTube videos'
                $DownloadVideos = $false
            }

            If (-not( Test-Path $FFMPEG)) {

                Write-Host ('ffmpeg.exe not found, will try to download from {0}' -f $FFMPEGlink)
                $TempFile= Join-Path $PSScriptRoot 'ffmpeg-latest-win32-static.zip'
                Invoke-WebRequest -Uri $FFMPEGlink -OutFile $TempFile -Proxy $ProxyURL

                If( Test-Path $TempFile) {
                    Add-Type -AssemblyName System.IO.Compression.FileSystem
                    Write-Host ('{0} downloaded, trying to extract ffmpeg.exe' -f $TempFile)
                    $FFMPEGZip= [System.IO.Compression.ZipFile]::OpenRead( $TempFile)
                    $FFMPEGEntry= $FFMPEGZip.Entries | Where-Object {$_.FullName -like '*/ffmpeg.exe'}
                    If( $FFMPEGEntry) {
                        Try {
                            [System.IO.Compression.ZipFileExtensions]::ExtractToFile( $FFMPEGEntry, $FFMPEG)
                            $FFMPEGZip.Dispose()
                            Remove-Item -LiteralPath $TempFile -Force
                        }
                        Catch {
                            Throw ('Problem extracting ffmpeg.exe from {0}' -f $FFMPEGZip)
                        }
                    }
                    Else {
                        Throw 'ffmpeg.exe missing in downloaded archive'
                    }
                }
            }
            If ( Test-Path $FFMPEG) {
                Write-Host ('ffmpeg.exe located at {0}' -f $FFMPEG)
                $DownloadAMSVideos= $true
            }
            Else {
                Write-Warning 'Unable to locate or download ffmpeg.exe, will skip downloading Azure Media Services videos'
                $DownloadAMSVideos = $false
            }
        }
    }

    $SessionCache = Join-Path $PSScriptRoot ('{0}-Sessions.cache' -f $EventName)
    $SessionCacheValid = $false
    If ( Test-Path $SessionCache) {
        Try {
            If ( (Get-childItem -LiteralPath $SessionCache).LastWriteTime -ge (Get-Date).AddHours( - $MaxCacheAge)) {
                Write-Host 'Session cache file found, reading session information'
                $data = Import-CliXml -LiteralPath $SessionCache -ErrorAction SilentlyContinue
                $SessionCacheValid = $true
            }
            Else {
                Write-Warning 'Cache information expired, will re-read information from catalog'
            }
        }
        Catch {
            Write-Host 'Error reading cache file or cache file invalid - will read from online catalog' -ForegroundColor Red
        }
    }

    If ( -not( $SessionCacheValid)) {

      Switch($EventType) {
        'API' {

            Write-Host ('Reading {0} session catalog' -f $EventName)
            $web = @{
                contentType = 'application/json; charset=utf-8'
                userAgent   = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36'
                requestUri  = [uri]('{0}/{1}' -f $EventAPIUrl, $EventSearchURI)
                itemsPerPage= 100
            }

            Try {
                $SearchBody= $EventSearchBody -f '12', '1'
                Write-Verbose ('Using URI {0}' -f $web.requestUri)
                $searchResultsResponse = Invoke-WebRequest -Uri $web.requestUri -Body $searchbody -Method $Method -ContentType $web.contentType -UserAgent $web.userAgent -WebSession $session -Proxy $ProxyURL
                $searchResults = [system.Text.Encoding]::UTF8.GetString($searchResultsResponse.RawContentStream.ToArray());
            }
            Catch {
                Throw ('Problem retrieving session catalog: {0}' -f $error[0])
            }
            $sessiondata = ConvertFrom-Json -InputObject $searchResults
            [int32] $sessionCount = $sessiondata.total
            [int32] $remainder = 0
 
            $PageCount = [System.Math]::DivRem($sessionCount, $web.itemsPerPage, [ref]$remainder)
            If ($remainder -gt 0) {
                $PageCount++
            }

            Write-Host ('Reading information for {0} sessions' -f $sessionCount)
            $data = [System.Collections.ArrayList]@()
            $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]('sessionCode', 'title'))
            $PSStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
            For ($page = 1; $page -le $PageCount; $page++) {
                Write-Progress -Id 1 -Activity "Retrieving Session Catalog" -Status "Processing page $page of $PageCount" -PercentComplete ($page / $PageCount * 100)
                $SearchBody= $EventSearchBody -f $web.itemsPerPage, $page
                $searchResultsResponse = Invoke-WebRequest -Uri $web.requestUri -Body $searchbody -Method $Method -ContentType $web.contentType -UserAgent $web.userAgent -WebSession $session  -Proxy $ProxyURL
                $searchResults = [system.Text.Encoding]::UTF8.GetString($searchResultsResponse.RawContentStream.ToArray());
                $sessiondata = ConvertFrom-Json -InputObject $searchResults
                ForEach ( $Item in $sessiondata.data) {
                    $object = $Item -as [PSCustomObject]
                    $object.PSObject.Properties | ForEach-Object {
                        if ($_.Name -eq 'speakerNames') { $object.($_.Name) = @($_.Value) }
                        if ($_.Name -eq 'products') { $object.($_.Name) = @($_.Value -replace [char]9, '/') }
                        if ($_.Name -eq 'contentCategory') { $object.($_.Name) = @(($_.Value -replace [char]9, '/') -replace ' / ', '/') }
                    }
                    Write-Verbose ('Adding info for session {0}' -f $Object.sessionCode)
                    $object.PSObject.TypeNames.Insert(0, 'Session.Information')
                    $object | Add-Member MemberSet PSStandardMembers $PSStandardMembers
                    $data.Add( $object) | Out-Null
                }
            }

            Write-Progress -Id 1 -Completed -Activity "Finished retrieval of catalog" 
        }

        'YT' {
            # YouTube published - Use youtube-dl to download the playlist as JSON so we can parse it to 'expected format'
            Write-Host ('Reading {0} playlist information (might take a while) ..' -f $EventName)
            $data = [System.Collections.ArrayList]@()
            $Arg= [System.Collections.ArrayList]@()
            If ( $ProxyURL) { 
                $Arg.Add( '--proxy "{0}"' -f $ProxyURL) | Out-Null
            }
            $Arg.Add( '--socket-timeout 90') | Out-Null
            $Arg.Add( '--retries 15') | Out-Null
            $Arg.Add( '--dump-json') | Out-Null
            $Arg.Add( ('"{0}"' -f $EventYTUrl)) | Out-Null

            $pinfo = New-Object System.Diagnostics.ProcessStartInfo
            $pinfo.FileName = $YouTubeDL
            $pinfo.RedirectStandardError = $true
            $pinfo.RedirectStandardOutput = $true
            $pinfo.UseShellExecute = $false
            $pinfo.Arguments = $Arg
            Write-Verbose ('Running {0} using {1}' -f $pinfo.FileName, ($pinfo.Arguments -join ' '))

            $p = New-Object System.Diagnostics.Process
            $p.StartInfo = $pinfo
            $p.Start() | Out-Null
            $stdout = $p.StandardOutput.ReadToEnd()
            $stderr = $p.StandardError.ReadToEnd()
            $p.WaitForExit()

            If ($p.ExitCode -ne 0) {
                Throw ('Problem running youtube-dl.exe: {0}' -f $stderr)
            }

            Try {
                Write-Verbose ('Converting from Json ..')
                # Trim any trailing empty lines, convert single string with line-breaks to array for JSON conversion
                $JsonData= ($stdout.Trim([System.Environment]::Newline) -Split "`n") | ConvertFrom-Json
            }
            Catch {
                Throw( 'Output does not seem to be proper JSON format, see {0}' -f $TempJsonFile)
            }

            ForEach( $Item in $JsonData) {

                $SpeakerNames= [System.Collections.ArrayList]@()

                # Description match pattern? Set Desc+Speakers, otherwise Desc=Description, assume no Speakers defined

                If($Item.Description -match '^(?<Description>[\s\S]*?)(\s)*(Download the slide deck from (?<Slidedeck>https:\/\/.*?)[\.]?)?(\s)*(Speaker(s)?:(\s)?(?<Speakers>.*))?(\s)*$') {
                    $Description= $Matches.Description
                    $Matches.Speakers -Split ';' | ForEach-Object { $SpeakerNames.Add( $_.Trim() ) |Out-Null }
                    $SlidedeckUrl= $Matches.Slidedeck
                }
                Else {
                    $Description= $Item.Description
                    $SlidedeckUrl= $null
                }

                # Slidedeck url, construct real link:
                If( $SlidedeckUrl) {
                    # https://www.microsoft.com/en-us/download/details.aspx?id=104608 -> https://www.microsoft.com/en-us/download/confirmation.aspx?id=104608

                    If( $SlidedeckUrl -match '^(?<host>https:\/\/www\.microsoft\.com).*id=(?<id>[\d]+)$') {
                        $SlideDeck= '{0}/en-us/download/confirmation.aspx?id={1}' -f $Matches.host, $Matches.id
                    }
                    Else {
                        Write-Warning ('Unexpected slide deck URL format: {0}' -f $SlidedeckUrl)
                        $Slidedeck= $null
                    }
                }
                Else {
                    $SlideDeck= $null
                }

                $object = [PSCustomObject]@{
                    sessionCode= [string]('{0:d2}' -f $Item.playlist_index)
                    SessionType= 'On-Demand'
                    Title= $Item.Title
                    Description= $Description
                    onDemand= $Item.webpage_url
                    Views= $Item.view_count
                    Likes= $Item.like_count
                    Duration= [timespan]::FromSeconds( $Item.duration).ToString()
                    langLocale= $EventLocale
                    SolutionArea= $Item.Tags
                    contentCategory= $Item.categories
                    SpeakerNames= $SpeakerNames
                    Slidedeck= $Slidedeck
                    startDateTime= [Datetime]::ParseExact( $Item.upload_date, 'yyyyMMdd', $null)
                    onDemandThumbnail= ($Item.thumbnails | Sort-Object -Property Id | Select-Object -First 1).Url
                }
                Write-Verbose ('Adding info for session {0}' -f $Item.Title)
                $data.Add( $object) | Out-Null
            }
        }

        default {
          Throw( 'Unknown event catalog type {0}' -f $EventType)
        }
      }

      Write-Host 'Storing session information'
      $data | Export-CliXml -Encoding Unicode -Force -LiteralPath $SessionCache

    }

    $SessionsToGet = $data
    $TotalNumberOfSessions= ($SessionsToGet | Measure-Object).Count

    If ($scheduleCode) {
        Write-Verbose ('Session code(s) specified: {0}' -f ($ScheduleCode -join ','))
        $SessionsToGet = $SessionsToGet | Where-Object { $scheduleCode -contains $_.sessioncode }
    }

    If ($ExcludeCommunityTopic) {
        Write-Verbose ('Excluding community topic: {0}' -f $ExcludeCommunityTopic)
        $SessionsToGet = $SessionsToGet | Where-Object { $ExcludeCommunityTopic -inotcontains $_.CommunityTopic  }
    }

    If ($Speaker) {
        Write-Verbose ('Speaker keyword specified: {0}' -f $Speaker)
        $SessionsToGet = $SessionsToGet | Where-Object { $_.speakerNames | Where-Object {$_ -ilike $Speaker} }
    }

    If ($Product) {
        Write-Verbose ('Product specified: {0}' -f $Product)
        $SessionsToGet = $SessionsToGet | Where-Object { $_.products | Where-Object {$_ -ilike $Product }}
    }

    If ($Category) {
        Write-Verbose ('Category specified: {0}' -f $Category)
        $SessionsToGet = $SessionsToGet | Where-Object { $_.contentCategory | Where-Object {$_ -ilike $Category }}
    }

    If ($SolutionArea) {
        Write-Verbose ('SolutionArea specified: {0}' -f $SolutionArea)
        $SessionsToGet = $SessionsToGet | Where-Object { $_.solutionArea | Where-Object {$_ -ilike $SolutionArea }}
    }

    If ($LearningPath) {
        Write-Verbose ('LearningPath specified: {0}' -f $LearningPath)
        $SessionsToGet = $SessionsToGet | Where-Object { $_.learningPath | Where-Object {$_ -ilike $LearningPath }}
    }

    If ($Topic) {
        Write-Verbose ('Topic specified: {0}' -f $Topic)
        $SessionsToGet = $SessionsToGet | Where-Object { $_.topic | Where-Object {$_ -ilike $Topic }}
    }

    If ($Locale) {
        Write-Verbose ('Locale(s) specified: {0}' -f ($Locale -join ','))
        Write-Verbose ('Sessions Pre: {0}'  -f ($SessionsToGet.Count))
        $SessionsToGetTemp= [System.Collections.ArrayList]@()
        ForEach( $item in $Locale) {
            $SessionsToGet | Where-Object {$item -ieq $_.langLocale} | ForEach-Object { $null= $SessionsToGetTemp.Add(  $_ ) }
        }
        $SessionsToGet= $SessionsToGetTemp | Sort-Object -Unique -Property sessionCode
        Write-Verbose ('Sessions Post: {0}'  -f ($SessionsToGet.Count))
    }

    If ($Title) {
        Write-Verbose ('Title keyword(s) specified: {0}' -f ( $Title -join ','))
        $SessionsToGetTemp= [System.Collections.ArrayList]@()
        ForEach( $item in $Title) {
            $SessionsToGet | Where-Object {$_.title -ilike ('*{0}*' -f $item) } | ForEach-Object { $null= $SessionsToGetTemp.Add(  $_ ) }
        }
        $SessionsToGet= $SessionsToGetTemp | Sort-Object -Unique -Property sessionCode
    }

    If ($Keyword) {
        Write-Verbose ('Description keyword(s) specified: {0}' -f ( $Keyword -join ','))
        $SessionsToGetTemp= [System.Collections.ArrayList]@()
        ForEach( $item in $Keyword) {
            $SessionsToGet | Where-Object {$_.description -ilike ('*{0}*' -f $item) } | ForEach-Object { $null= $SessionsToGetTemp.Add(  $_ ) }
        }
        $SessionsToGet= $SessionsToGetTemp | Sort-Object -Unique -Property sessionCode
    }

    If ($NoRepeats) {
        Write-Verbose ('Skipping repeated sessions')
        $SessionsToGet = $SessionsToGet | Where-Object {$_.sessionCode -inotmatch '^*R[1-9]?$' -and $_.sessionCode -inotmatch '^[A-Z]+[0-9]+[B-C]+$'}
    }

    If ( $InfoOnly) {
        Write-Verbose ('There are {0} sessions matching your criteria.' -f (($SessionsToGet | Measure-Object).Count))
        Write-Output $SessionsToGet
    }
    Else {

        If( $OGVPicker) {
            $SessionsToGet= $SessionsToGet | Out-GridView -Title 'Select Videos to Download, or Cancel for all Videos' -PassThru
        }

        $i = 0
        $DeckInfo = @(0, 0, 0)
        $VideoInfo = @(0, 0, 0)
        $InfoDownload = 0
        $InfoPlaceholder = 1
        $InfoExist = 2

        $myTimeZone = [System.TimeZoneInfo]::FindSystemTimeZoneById( 'US Eastern Standard Time')

        [console]::TreatControlCAsInput = $true

        $SessionsSelected = ($SessionsToGet | Measure-Object).Count
        Write-Host ('There are {0} sessions matching your criteria.' -f $SessionsSelected)
        Foreach ($SessionToGet in $SessionsToGet) {

            $i++
            Write-Progress -Id 1 -Activity 'Inspecting session information' -Status "Processing session $i of $SessionsSelected" -PercentComplete ($i / $SessionsSelected * 100)
            If( $SessionToGet.sessionCode) {
                $FileName = Fix-FileName ('{0}-{1}' -f $SessionToGet.sessionCode.Trim(), $SessionToGet.title.Trim())
            }
            Else {
                $FileName = Fix-FileName ('{0}' -f $SessionToGet.title.Trim())
            }
            If(! ([string]::IsNullOrEmpty( $SessionToGet.startDateTime))) {
                # Get session localized timestamp, undoing TZ adjustments
                $SessionTime= [System.TimeZoneInfo]::ConvertTime((Get-Date -Date $SessionToGet.startDateTime).ToUniversalTime(), $myTimeZone ).toString('g')
            }
            Else {
                $SessionTime= $null
            }
            Write-Host ('Processing info session {0} from {1} [{2}]' -f $FileName, (Iif -Cond $SessionTime -IfTrue $SessionTime -IfFalse 'No Timestamp'), $SessionToGet.langLocale)
            If(!([string]::IsNullOrEmpty( $SessionToGet.startDateTime)) -and (Get-Date -Date $SessionToGet.startDateTime) -ge (Get-Date)) {
                Write-Verbose ('Skipping session {0}: Didn''t take place yet' -f $SessionToGet.sessioncode)
            }
            Else {

              If( ! $NoVideos) {
                If ( $DownloadVideos -or $DownloadAMSVideos) {

                $topic = $SessionToGet.contentArea | Select-Object -first 1

                #Create directory.sessionCode.
                $folder = Join-Path -Path $DownloadFolder -ChildPath $topic;
                if (-not (Test-Path $folder)) {
                    Write-Host "Folder ($folder) doesn't exist. Creating it..."  ;
                    New-Item $folder -type directory | Out-Null;
                }

                    $vidfileName = ("$FileName.mp4")
                    $vidFullFile = "$DownloadFolder\$topic\$vidfileName"

                    if ((Test-ResolvedPath -Path $vidFullFile) -and -not $Overwrite) {
                        Write-Host ('Video exists {0}' -f $vidfileName) -ForegroundColor Gray
                        $VideoInfo[ $InfoExist]++
                        # Clean video leftovers
                        Clean-VideoLeftovers $vidFullFile
                    }
                    else {
                        $downloadLink= $null
                        If ( !( [string]::IsNullOrEmpty( $SessionToGet.onDemand)) ) {
                            If( $PreferDirect -and (!( [string]::IsNullOrEmpty( $SessionToGet.downloadVideoLink)))) {
                                $downloadLink = $SessionToGet.downloadVideoLink
                            }
                            Else {
                                $downloadLink = $SessionToGet.onDemand
                            }
                        }
                        Else {
                            If (!( [string]::IsNullOrEmpty( $SessionToGet.downloadVideoLink)) ) {
                                $downloadLink = $SessionToGet.downloadVideoLink
                            }
                            Else {
                                If($NoGuessing) {
                                    $downloadLink= $null
                                }
                                Else { 
                                    # Try session page, eg https://medius.studios.ms/Embed/Video/IG18-BRK2094
                                    $downloadLink = $SessionUrl -f $SessionToGet.SessionCode
                                }
                            }
                        }

                        If( $downloadLink -match '(medius\.studios\.ms\/Embed\/Video|medius\.microsoft\.com|mediastream\.microsoft\.com)' ) {
                            Write-Verbose ('Checking hosted video link {0}' -f $downloadLink)
                            Try {
                                $DownloadedPage= Start-BitsTransfer -Source $downloadLink -Destination $vidFullFile;
            #Invoke-WebRequest -Uri $downloadLink -Proxy $ProxyURL -DisableKeepAlive -ErrorAction SilentlyContinue
                            }
                            Catch {
                                $DownloadedPage= $null
                            }
                            If($DownloadedPage) {                        
                                $OnDemandPage= $DownloadedPage.RawContent

                                # Check for embedded AMS 
                                If( $OnDemandPage -match '<video (playsinline)? id="azuremediaplayer" class=".*?" data-id="(?<AzureStreamURL>.*?)"><\/video>') {
                                    Write-Verbose ('Using Azure Media Services URL {0}' -f $matches.AzureStreamURL)
                                    $Endpoint= '{0}(format=mpd-time-csf)' -f $matches.AzureStreamURL
                                    $Arg = @( ('-o "{0}"' -f ($vidFullFile -replace '%', '%%')), $Endpoint)

                                    # Construct Format for this specific video, language and audio languages available
                                    If ( $Format) {
                                        $ThisFormat= $Format
                                    } 
                                    Else { 
                                        $ThisFormat= 'bestvideo+bestaudio'
                                    }

                                    If( $SessionToGet.audioLanguage) {

                                        If( $SessionToGet.audioLanguage.Count -gt 1) {
                                            # Session has multiple audio tracks
                                            If( $SessionToGet.audioLanguage -icontains $Language) {
                                                Write-Warning ('Multiple audio languages available; will try downloading {0} audio stream' -f $Language)
                                                $ThisLanguage= $Language
                                            }
                                            Else {
                                                $ThisLanguage= $SessionToGet.audioLanguage | Select -First 1
                                                Write-Warning ('Requested language {0} not available; will use default stream ({1})' -f $Language, $ThisLanguage)
                                            }

                                            # Take specified Format apart so we can insert the language filter per specification
                                            $ThisFormatElem= $ThisFormat -Split ','
                                            $NewFormat= [System.Collections.ArrayList]@()
                                            ForEach( $Elem in $ThisFormatElem) {
                                                If( $Elem -match '^(?<pre>.*audio)(\[(?<audioparam>.*)\])?(?<post>(.*)?)$' ) {
                                                    If( $matches.audioparam) {
                                                        $NewFormatElem= '{0}[format_id*={1},{2}]{3}' -f $matches.Pre, $ThisLanguage, $matches.audioparam, $matches.post
                                                    }
                                                    Else {
                                                        $NewFormatElem= '{0}[format_id*={1}]{2}' -f $matches.Pre, $ThisLanguage, $matches.post
                                                    }
                                                }
                                                Else {
                                                    $NewFormatElem= $Elem
                                                    Write-Warning ('Problem determining where to add language criteria in {0}, leaving criteria as-is' -f $NewFormat)
                                                }
                                                $null= $NewFormat.Add( $NewFormatElem)
                                            }

                                            # With language filters determined, recreate filter and add whole non-language specific qualifiers as next best 
                                            $ThisFormat= ($NewFormat -Join ','), $ThisFormat -Join ','

                                        }
                                        Else {
                                            # Only 1 Language available, so use default audio stream
                                            Write-Warning ('Only single audio stream available, will use default audio stream')
                                        }
                                    }
                                    Else {
                                        # No multiple audio languages, use default audio stream
                                        Write-Warning ('Multiple audio streams not available, will use default audio stream')
                                    }
                                    $Arg += ('--format {0}' -f $ThisFormat)
                                }
                                Else {
                                    # Check for embedded YouTube 
                                    If( $OnDemandPage -match '"https:\/\/www\.youtube-nocookie\.com\/embed\/(?<YouTubeID>.+?)\?enablejsapi=1&"') {
                                        $Endpoint= 'https://www.youtube.com/watch?v={0}' -f $matches.YouTubeID
                                        Write-Verbose ('Using YouTube URL {0}' -f $Endpoint)
                                        $Arg = @( ('-o "{0}"' -f ($vidFullFile -replace '%', '%%')), $Endpoint)
                                        If ( $Format) { $Arg += ('--format {0}' -f $Format) } Else { $Arg += ('--format 22') }
                                        If ( $Subs) { $Arg += ('--sub-lang {0}' -f ($Subs -Join ',')), ('--write-sub'), ('--write-auto-sub'), ('--convert-subs srt') }
                                    }
                                    Else {
                                        Write-Warning "Skipping: Embedded AMS or YouTube URL not found"
                                        $EndPoint= $null
                                    }
                                }

                            }
                            Else {
                                 Write-Warning ('Skipping: {0} unavailable' -f $downloadLink)
                            }
                        }
                        Else {
                            # Direct
                            Write-Verbose ('Using direct video link {0}' -f $downloadLink)
                            If( $downloadLink) {
                                $Endpoint= $downloadLink
                                $Arg = @( ('-o "{0}"' -f $vidFullFile), $downloadLink)
                            }
                            Else {
                                Write-Warning ('No video link for {0}' -f ($SessionToGet.Title))
                                $Endpoint= $null
                            }
                        }
                        If( $Endpoint) {
                            # Direct, AMS or YT video found, attempt download but first define common parameters

                            If ( $ProxyURL) { 
                                $Arg += ('--proxy "{0}"' -f $ProxyURL)
                            }
                            $Arg+= '--socket-timeout 90'
                            $Arg+= '--no-check-certificate'                            
                            $Arg+= '--retries 15'

                            If ( $Subs) { $Arg += ('--sub-lang {0}' -f ($Subs -Join ',')), ('--write-sub'), ('--write-auto-sub'), ('--convert-subs srt') }

                            Write-Verbose ('Running: youtube-dl.exe {0}' -f ($Arg -join ' '))
                            Add-BackgroundDownloadJob -Type 2 -FilePath $YouTubeDL -ArgumentList $Arg -File $vidFullFile -Timestamp $SessionTime -scheduleCode ($SessionToGet.sessioncode) -Title ($SessionToGet.Title)
                        }
                        Else {
                            # Video not available or no link found
                            $VideoInfo[ $InfoPlaceholder]++
                        }
                    }

                    If( $Captions) {
                        $captionVTTFile= $vidFullFile -replace '.mp4', '.vtt'

                        If ((Test-ResolvedPath -Path $captionVTTFile) -and -not $Overwrite) {
                            Write-Host ('Caption file exists {0}' -f $captionVTTFile) -ForegroundColor Gray
                        }
                        Else {
                            # Caption file in AMS needs seperate download
                            $captionFileLink= $SessionToGet.captionFileLink
                            If( ! $captionFileLink) {

                                If(! $OnDemandPage) {
                                    # Try if there is caption file reference on page
                                    Try {
                                        $DownloadedPage = Invoke-WebRequest -Uri $downloadLink -Proxy $ProxyURL -DisableKeepAlive -ErrorAction SilentlyContinue
                                        $OnDemandPage= $DownloadedPage.RawContent
                                    }
                                    Catch {
                                        $DownloadedPage= $null
                                        $onDemandPage= $null
                                    } 
                                }
                                Else {
                                    # Reuse one from video download
                                }

                                If( $OnDemandPage -match '"(?<AzureCaptionURL>https:\/\/mediusprodstatic\.studios\.ms\/asset-[a-z0-9\-]+\/transcript\.vtt\?.*?)"') {
                                    $captionFileLink= $matches.AzureCaptionURL
                                }
                                If( ! $captionFileLink) {
                                    $captionFileLink= $captionURL -f $SessionToGet.SessionCode
                                }
                            }
                            If( $captionFileLink) {
                                Write-Verbose ('Retrieving caption file from URL {0}' -f $captionFileLink)

                                 $captionFullFile= $captionVTTFile
                                 Write-Verbose ('Downloading {0} to {1}' -f $captionFileLink,  $captionFullFile)
                                 Add-BackgroundDownloadJob -Type 3 -FilePath $captionVTTFile -DownloadUrl $captionFileLink -File $captionFullFile -Timestamp $SessionTime -scheduleCode ($SessionToGet.sessioncode) -Title ($SessionToGet.Title)

                             }
                             Else {
                                 Write-Warning "Subtitles requested, but no Caption URL found"
                             }
                        }
                    }
                    $OnDemandPage= $null
                }
              }

              If(! $NoSlidedecks) {
                If ( !( [string]::IsNullOrEmpty( $SessionToGet.slideDeck)) ) {
                    $downloadLink = $SessionToGet.slideDeck
                }
                Else {
                    If( $NoGuessing) {
                        $downloadLink= $null
                    }
                    Else {
                        # Try alternative construction
                        $downloadLink = $SlidedeckUrl -f $SessionToGet.SessionCode
                    }
                }

                If ($downloadLink -match "view.officeapps.live.com.*PPTX" -or $downloadLink -match 'downloaddocument' -or $downloadLink -match 'medius' -or $downloadLink -match 'confirmation\.aspx') {

                $topic = $SessionToGet.contentArea | Select-Object -first 1

                #Create directory.sessionCode.
                $folder = Join-Path -Path $DownloadFolder -ChildPath $topic;
                if (-not (Test-Path $folder)) {
                    Write-Host "Folder ($folder) doesn't exist. Creating it...";
                    New-Item $folder -type directory | Out-Null;
                }

                    $slidedeckFullFile = "$DownloadFolder\$topic\$slidedeckFile"
                    $DownloadURL = [System.Web.HttpUtility]::UrlDecode( $downloadLink )

                    Try {
                       If( $downloadLink -notmatch 'confirmation\.aspx') {
                           $ValidUrl = Invoke-WebRequest -Uri $DownloadURL -Method HEAD -UseBasicParsing -DisableKeepAlive -MaximumRedirection 10 -ErrorAction SilentlyContinue
                       }
                       Else {
                           $ValidUrl = Invoke-WebRequest -Uri $DownloadURL -Method HEAD -UseBasicParsing -DisableKeepAlive -MaximumRedirection 10 -ErrorAction SilentlyContinue
                       }
                    }
                    Catch {
                        $ValidUrl= $false
                    }

                    If( $downloadLink -match 'confirmation\.aspx' -and $ValidURL.Headers.'Content-Type' -ilike 'text/html') {
                        # Extra parsing for MS downloads
                        If( $ValidUrl.RawContent -match 'href="(?<Url>https:\/\/download\.microsoft\.com\/download[\/0-9\-]*\/.*(pdf|pptx))".*click here to download manually') {
                            $DownloadURL= [System.Web.HttpUtility]::UrlDecode( $Matches.Url)
                            $ValidUrl = Invoke-WebRequest -Uri $DownloadURL -Method HEAD -UseBasicParsing -DisableKeepAlive -MaximumRedirection 10 -ErrorAction SilentlyContinue
                        }
                    }

                    If( $ValidUrl ) {
                        If( $DownloadURL -like '*.pdf' -or $ValidURL.Headers.'Content-Type' -ieq 'application/pdf') {
                            # Slidedeck offered is PDF format
                            $slidedeckFile = '{0}.pdf' -f $FileName
                        }
                        Else {
                            $slidedeckFile = '{0}.pptx' -f $FileName
                        }

                $topic = $SessionToGet.contentArea | Select-Object -first 1

                #Create directory.sessionCode.
                $folder = Join-Path -Path $DownloadFolder -ChildPath $topic;
                if (-not (Test-Path $folder)) {
                    Write-Host "Folder ($folder) doesn't exist. Creating it..."  ;
                    New-Item $folder -type directory | Out-Null;
                }
                        $slidedeckFullFile = "$DownloadFolder\$topic\$slidedeckFile"

                        if ((Test-ResolvedPath -Path $slidedeckFullFile) -and ((Get-ChildItem -LiteralPath $slidedeckFullFile -ErrorAction SilentlyContinue).Length -gt 0) -and -not $Overwrite) {
                            Write-Host ('Slidedeck exists {0}' -f $slidedeckFile) -ForegroundColor Gray 
                            $DeckInfo[ $InfoExist]++
                        }
                        Else {
                            Write-Verbose ('Downloading {0} to {1}' -f $DownloadURL,  $slidedeckFullFile)
                            Add-BackgroundDownloadJob -Type 1 -FilePath $slidedeckFullFile -DownloadUrl $DownloadURL -File $slidedeckFullFile -Timestamp $SessionTime -scheduleCode ($SessionToGet.sessioncode) -Title ($SessionToGet.Title)
                        }
                    }
                    Else {
                        Write-Warning ('Skipping: Slidedeck unavailable {0}' -f $DownloadURL)
                        $DeckInfo[ $InfoPlaceholder]++
                    }
                }
                Else {
                    Write-Warning ('No slidedeck link for {0}' -f ($SessionToGet.Title))
                }
              }
            }

            $JobsRunning= Get-BackgroundDownloadJobs

            if ([system.console]::KeyAvailable) { 
                $key = [system.console]::readkey($true)
                if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
                    Write-Host "TERMINATING" -ForegroundColor Red
                    Stop-BackgroundDownloadJobs
                    Exit -1
                }
            }

        }

        $ProcessedSessions= $i

        Write-Progress -Id 1 -Completed -Activity "Finished processing session information"

        $JobsRunning= Get-BackgroundDownloadJobs
        If ( $JobsRunning -gt 0) {
            Write-Host ('Waiting for download jobs to finish - press Ctrl-C once to abort)' -f $JobsRunning)
            While  ( $JobsRunning -gt 0) {
                if ([system.console]::KeyAvailable) { 
                    Start-Sleep 1
                    $key = [system.console]::readkey($true)
                    if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
                        Write-Host "TERMINATING" -ForegroundColor Red
                        Stop-BackgroundDownloadJobs
                        Exit -1
                    }
                }
                Start-Sleep 5
                $JobsRunning= Get-BackgroundDownloadJobs
            }
        }
        Else {
            Write-Host ('Background download jobs have finished' -f $JobsRunning)
        }

        Write-Progress -Id 2 -Completed -Activity "Download jobs finished"  

        Write-Host ('Selected {0} sessions out of a total of {1}' -f $ProcessedSessions, $TotalNumberOfSessions)
        Write-Host ('Downloaded {0} slide decks and {1} videos.' -f $DeckInfo[ $InfoDownload], $VideoInfo[ $InfoDownload])
        Write-Host ('Not (yet) available: {0} slide decks and {1} videos' -f $DeckInfo[ $InfoPlaceholder], $VideoInfo[ $InfoPlaceholder])
        Write-Host ('Skipped {0} slide decks and {1} videos as they were already downloaded.' -f $DeckInfo[ $InfoExist], $VideoInfo[ $InfoExist])
    }
`
michelderooij commented 1 year ago

This usually happens when video content is moved across storage, and links stop to work/start to appear. It's an ongoing struggle, trying to keep up :)