Skip to content

Instantly share code, notes, and snippets.

@tillig
Last active December 7, 2018 00:38
Show Gist options
  • Save tillig/20f8656b9622f9dcd7ace41f3f55b9ed to your computer and use it in GitHub Desktop.
Save tillig/20f8656b9622f9dcd7ace41f3f55b9ed to your computer and use it in GitHub Desktop.
Download .m3u8 contents for ffmpeg concatenation
<#
.Synopsis
Downloads an M3U8 playlist and the subsequent TS files, ready for combining.
.DESCRIPTION
Using the headers from an authenticated session with a video provider, download the
contents of a playlist and prepare the TS files in the playlist for merging.
Assuming you have your headers exported as JSON like this...
{
"Host": "video.somesite.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Accept": "*/*",
"Referer": "https://course.somesite.com/play/1234",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Cookie": "..."
}
...you can get your headers in hashtable format like:
$headers = @{}
(Get-Content .\request_headers.json | ConvertFrom-Json).PSObject.Properties | %{ $headers[$_.Name] = $_.Value }
You can use ffmpeg to merge the ts files later (https://trac.ffmpeg.org/wiki/Concatenate) with the
ffmpeg-input.txt that will also be generated as the download runs.
.EXAMPLE
.\Get-M3u8Content.ps1 -Headers $headers -PlaylistUrl https://video.somesite.com/1234/playlist.m3u8 -DestinationFolder .\destination
.EXAMPLE
ffmpeg -f concat -safe 0 -i ffmpeg-input.txt -c copy fullvideo.mp4
#>
[CmdletBinding()]
Param(
[Parameter(
Mandatory=$True,
HelpMessage="The set of headers Chrome is using to request the M3U8 - get these out of dev tools.")]
[ValidateNotNull()]
[Hashtable]
$Headers,
[Parameter(
Mandatory=$True,
HelpMessage="Full URL to the .m3u8 playlist.")]
[ValidateNotNull()]
[System.Uri]
$PlaylistUrl,
[Parameter(
Mandatory=$True,
HelpMessage="The folder where the .ts files will end up.")]
[ValidateNotNullOrEmpty()]
[string]
$DestinationFolder
)
Begin
{
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$m3u8Filename = $PlaylistUrl.Segments[$PlaylistUrl.Segments.Length - 1]
If (-Not (Test-Path $DestinationFolder -PathType Container))
{
Write-Verbose "Creating destination folder $DestinationFolder"
New-Item $DestinationFolder -ItemType Directory
}
$m3u8Filename = Join-Path -Path $DestinationFolder -ChildPath $PlaylistUrl.Segments[$PlaylistUrl.Segments.Length - 1]
Write-Verbose "M3U8 will be saved at $m3u8Filename"
$ffmpegPlaylist = Join-Path -Path $DestinationFolder -ChildPath "ffmpeg-input.txt"
# Gets the expected length of the file and double-checks that the result was correct.
# For auto-resume, double-checks the existing file is the expected length and re-downloads
# if not.
function DownloadFile([string]$uri, [string]$destination, [switch]$skipSizeCheck = $False)
{
$OriginalProgressPreference = $ProgressPreference
If (-Not $skipSizeCheck)
{
$expectedLength = 0
Try
{
# Disabling progress on Invoke-WebRequest makes it WAY faster.
$ProgressPreference = 'SilentlyContinue'
$head = Invoke-WebRequest -Headers $Headers -Method HEAD -Uri $uri
$expectedLength = [System.Int32]($head.Headers["Content-Length"])
}
Finally
{
$ProgressPreference = $OriginalProgressPreference
}
Write-Verbose "Expectd size of $destination is $expectedLength"
}
If (Test-Path $destination)
{
If ($skipSizeCheck -Or ((Get-Item $destination).Length -Eq $expectedLength))
{
# We're not checking sizes and just trusting the downloaded file is good OR
# We ensure the expected size is the actual size.
# Either way, we call it good.
Write-Verbose "Bypassing re-download of $uri."
Return $False
}
# The file exists but we're re-downloading because it's not right.
Remove-Item $destination -Force
}
Write-Verbose "Downloading $uri to $destination"
Try
{
# Disabling progress on Invoke-WebRequest makes it WAY faster.
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Headers $Headers -Method GET -Uri $uri -OutFile $destination
}
Finally
{
$ProgressPreference = $OriginalProgressPreference
}
If ((-Not $skipSizeCheck) -And ((Get-Item $destination).Length -Ne $expectedLength))
{
throw "Failed to download $uri"
}
Return $True
}
}
Process
{
DownloadFile $PlaylistUrl $m3u8Filename -skipSizeCheck | Out-Null
$tsFiles = Get-Content $m3u8Filename | %{ $_.Trim() } | Where-Object { -Not $_.StartsWith('#') -And $_.Length -Gt 0 }
If ($tsFiles.Length -Eq 0)
{
throw "No TS files found in the M3U8 file."
}
$downloadCount = 0;
$tsFiles | %{
$baseTsFilename = $_
#Ensure the local path written to the input file is absolute.
$localTsFilename = Join-Path -Path $DestinationFolder -ChildPath $baseTsFilename
$localTsFilename = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($localTsFilename)
$tsUrl = $Null
$downloadCount++
If (-Not [System.Uri]::TryCreate($PlaylistUrl, [System.Uri]$baseTsFilename, [ref]$tsUrl))
{
throw "Unable to determine location of $baseTsFilename in relation to $PlaylistUrl"
}
Write-Progress -Activity "Downloading M3U8 contents" -Id 1 -CurrentOperation "Downloading $tsUrl" -PercentComplete (($downloadCount / $tsFiles.Length) * 100)
$downloaded = DownloadFile $tsUrl $localTsFilename
If ($downloaded)
{
Add-Content -Value "file '$localTsFilename'" -Path $ffmpegPlaylist
}
}
}
End
{
Write-Progress -Activity "Downloading M3U8 contents" -Id 1 -Completed
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment