2022-02-16 03:15:41 +01:00
using Newtonsoft.Json.Linq ;
using System ;
using System.Collections.Generic ;
2022-03-03 00:34:52 +01:00
using System.Diagnostics ;
2022-03-09 23:43:51 +01:00
using System.IO ;
2022-02-16 03:15:41 +01:00
using System.Linq ;
using System.Net ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
using VDownload.Core.Enums ;
2022-03-09 23:43:51 +01:00
using VDownload.Core.Exceptions ;
2022-02-16 03:15:41 +01:00
using VDownload.Core.Interfaces ;
2022-03-07 14:59:11 +01:00
using VDownload.Core.Services.Sources.Twitch.Helpers ;
using VDownload.Core.Structs ;
2022-02-16 03:15:41 +01:00
using Windows.Storage ;
namespace VDownload.Core.Services.Sources.Twitch
{
public class Vod : IVideoService
{
#region CONSTRUCTORS
public Vod ( string id )
{
ID = id ;
}
#endregion
2022-02-19 01:38:37 +01:00
#region PROPERTIES
2022-02-16 03:15:41 +01:00
public string ID { get ; private set ; }
2022-02-26 14:32:34 +01:00
public Uri VideoUrl { get ; private set ; }
2022-03-07 14:59:11 +01:00
public Metadata Metadata { get ; private set ; }
public BaseStream [ ] BaseStreams { get ; private set ; }
2022-02-16 03:15:41 +01:00
#endregion
#region STANDARD METHODS
// GET VOD METADATA
2022-02-26 14:32:34 +01:00
public async Task GetMetadataAsync ( CancellationToken cancellationToken = default )
2022-02-16 03:15:41 +01:00
{
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2022-02-16 03:15:41 +01:00
// Get response
2022-03-07 14:59:11 +01:00
JToken response = null ;
using ( WebClient client = await Client . Helix ( ) )
{
client . QueryString . Add ( "id" , ID ) ;
cancellationToken . ThrowIfCancellationRequested ( ) ;
2022-03-09 23:43:51 +01:00
try
{
response = JObject . Parse ( await client . DownloadStringTaskAsync ( "https://api.twitch.tv/helix/videos" ) ) . GetValue ( "data" ) [ 0 ] ;
}
catch ( WebException ex )
{
if ( ex . Response ! = null & & new StreamReader ( ex . Response . GetResponseStream ( ) ) . ReadToEnd ( ) . Contains ( "Not Found" ) ) throw new MediaNotFoundException ( $"Twitch VOD (ID: {ID}) was not found" ) ;
else if ( ex . Response ! = null & & new StreamReader ( ex . Response . GetResponseStream ( ) ) . ReadToEnd ( ) = = string . Empty & & ex . Message . Contains ( "400" ) ) throw new MediaNotFoundException ( $"Twitch VOD (ID: {ID}) was not found" ) ;
else throw ;
}
2022-03-07 14:59:11 +01:00
}
2022-02-16 03:15:41 +01:00
// Set parameters
2022-02-19 01:38:37 +01:00
GetMetadataAsync ( response ) ;
2022-02-16 03:15:41 +01:00
}
2022-02-19 01:38:37 +01:00
internal void GetMetadataAsync ( JToken response )
2022-02-16 03:15:41 +01:00
{
2022-02-26 14:32:34 +01:00
// Create unified video url
VideoUrl = new Uri ( $"https://www.twitch.tv/videos/{ID}" ) ;
2022-03-07 14:59:11 +01:00
// Set metadata
Metadata = new Metadata ( )
{
Title = ( ( string ) response [ "title" ] ) . Replace ( "\n" , "" ) ,
Author = ( string ) response [ "user_name" ] ,
Date = Convert . ToDateTime ( response [ "created_at" ] ) ,
Duration = ParseDuration ( ( string ) response [ "duration" ] ) ,
Views = ( long ) response [ "view_count" ] ,
Thumbnail = ( string ) response [ "thumbnail_url" ] = = string . Empty ? null : new Uri ( ( ( string ) response [ "thumbnail_url" ] ) . Replace ( "%{width}" , "1920" ) . Replace ( "%{height}" , "1080" ) ) ,
} ;
2022-02-16 03:15:41 +01:00
}
// GET VOD STREAMS
2022-02-26 14:32:34 +01:00
public async Task GetStreamsAsync ( CancellationToken cancellationToken = default )
2022-02-16 03:15:41 +01:00
{
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2022-02-16 03:15:41 +01:00
2022-03-07 14:59:11 +01:00
// Get response
string [ ] response = null ;
using ( WebClient client = Client . GQL ( ) )
{
// Get video GQL access token
cancellationToken . ThrowIfCancellationRequested ( ) ;
JToken videoAccessToken = JObject . Parse ( await client . UploadStringTaskAsync ( "https://gql.twitch.tv/gql" , "{\"operationName\":\"PlaybackAccessToken_Template\",\"query\":\"query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: \\\"web\\\", playerBackend: \\\"mediaplayer\\\", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}\",\"variables\":{\"isLive\":false,\"login\":\"\",\"isVod\":true,\"vodID\":\"" + ID + "\",\"playerType\":\"embed\"}}" ) ) [ "data" ] [ "videoPlaybackAccessToken" ] ;
// Get video streams
cancellationToken . ThrowIfCancellationRequested ( ) ;
response = ( await client . DownloadStringTaskAsync ( $"http://usher.twitch.tv/vod/{ID}?nauth={videoAccessToken[" value "]}&nauthsig={videoAccessToken[" signature "]}&allow_source=true&player=twitchweb" ) ) . Split ( "\n" ) ;
}
2022-02-16 03:15:41 +01:00
// Init streams list
2022-03-07 14:59:11 +01:00
List < BaseStream > streams = new List < BaseStream > ( ) ;
// Stream data line2 regular expression
Regex streamDataL2Regex = new Regex ( @"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""\S+,\S+"",RESOLUTION=\d+x(?<height>\d+),VIDEO=""\w+""(,FRAME-RATE=(?<frame_rate>\d+.\d+))?" ) ;
2022-02-16 03:15:41 +01:00
// Parse response
for ( int i = 2 ; i < response . Length ; i + = 3 )
{
// Parse line 2
2022-03-07 14:59:11 +01:00
Match line2 = streamDataL2Regex . Match ( response [ i + 1 ] ) ;
2022-02-16 03:15:41 +01:00
// Create stream
2022-03-07 14:59:11 +01:00
BaseStream stream = new BaseStream ( )
2022-02-16 03:15:41 +01:00
{
2022-03-07 14:59:11 +01:00
Url = new Uri ( response [ i + 2 ] ) ,
Height = int . Parse ( line2 . Groups [ "height" ] . Value ) ,
FrameRate = line2 . Groups [ "frame_rate" ] . Value ! = string . Empty ? ( int ) Math . Round ( double . Parse ( line2 . Groups [ "frame_rate" ] . Value ) ) : 0 ,
2022-02-16 03:15:41 +01:00
} ;
// Add stream
streams . Add ( stream ) ;
}
2022-03-07 14:59:11 +01:00
// Set streams
2022-03-02 22:13:28 +01:00
BaseStreams = streams . ToArray ( ) ;
2022-02-16 03:15:41 +01:00
}
// DOWNLOAD AND TRANSCODE VOD
2022-03-07 14:59:11 +01:00
public async Task < StorageFile > DownloadAndTranscodeAsync ( StorageFolder downloadingFolder , BaseStream baseStream , MediaFileExtension extension , MediaType mediaType , TimeSpan trimStart , TimeSpan trimEnd , CancellationToken cancellationToken = default )
2022-02-16 03:15:41 +01:00
{
// Invoke DownloadingStarted event
2022-03-07 14:59:11 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
DownloadingProgressChanged ( this , new EventArgs . ProgressChangedEventArgs ( 0 ) ) ;
2022-02-16 03:15:41 +01:00
// Get video chunks
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
List < ( Uri ChunkUrl , TimeSpan ChunkDuration ) > chunksList = await ExtractChunksFromM3U8Async ( baseStream . Url , cancellationToken ) ;
2022-02-16 03:15:41 +01:00
2022-03-07 14:59:11 +01:00
// Changeable duration
TimeSpan duration = Metadata . Duration ;
2022-02-16 03:15:41 +01:00
// Passive trim
2022-03-07 14:59:11 +01:00
if ( ( bool ) Config . GetValue ( "twitch_vod_passive_trim" ) & & trimStart ! = TimeSpan . Zero & & trimEnd ! = duration ) ( trimStart , trimEnd , duration ) = PassiveVideoTrim ( chunksList , trimStart , trimEnd , Metadata . Duration ) ;
2022-02-16 03:15:41 +01:00
// Download
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
StorageFile rawFile = await downloadingFolder . CreateFileAsync ( "raw.ts" ) ;
2022-02-16 03:15:41 +01:00
2022-03-07 14:59:11 +01:00
double chunksDownloaded = 0 ;
2022-02-16 03:15:41 +01:00
2022-03-02 22:13:28 +01:00
Task < byte [ ] > downloadTask ;
Task writeTask ;
2022-02-28 13:31:58 +01:00
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
downloadTask = DownloadChunkAsync ( chunksList [ 0 ] . ChunkUrl ) ;
await downloadTask ;
for ( int i = 1 ; i < chunksList . Count ; i + + )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
writeTask = WriteChunkToFileAsync ( rawFile , downloadTask . Result ) ;
downloadTask = DownloadChunkAsync ( chunksList [ i ] . ChunkUrl ) ;
await Task . WhenAll ( writeTask , downloadTask ) ;
2022-03-07 14:59:11 +01:00
DownloadingProgressChanged ( this , new EventArgs . ProgressChangedEventArgs ( + + chunksDownloaded * 100 / chunksList . Count ) ) ;
2022-02-16 03:15:41 +01:00
}
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
await WriteChunkToFileAsync ( rawFile , downloadTask . Result ) ;
2022-03-07 14:59:11 +01:00
DownloadingProgressChanged ( this , new EventArgs . ProgressChangedEventArgs ( 100 , true ) ) ;
2022-02-16 03:15:41 +01:00
// Processing
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
StorageFile outputFile = await downloadingFolder . CreateFileAsync ( $"transcoded.{extension.ToString().ToLower()}" ) ;
2022-02-16 03:15:41 +01:00
2022-03-07 14:59:11 +01:00
MediaProcessor mediaProcessor = new MediaProcessor ( ) ;
mediaProcessor . ProgressChanged + = ProcessingProgressChanged ;
Task mediaProcessorTask ;
if ( trimStart = = TimeSpan . Zero & & trimEnd = = duration ) mediaProcessorTask = mediaProcessor . Run ( rawFile , extension , mediaType , outputFile , cancellationToken : cancellationToken ) ;
else if ( trimStart = = TimeSpan . Zero ) mediaProcessorTask = mediaProcessor . Run ( rawFile , extension , mediaType , outputFile , trimStart : trimStart , cancellationToken : cancellationToken ) ;
else if ( trimEnd = = duration ) mediaProcessorTask = mediaProcessor . Run ( rawFile , extension , mediaType , outputFile , trimEnd : trimEnd , cancellationToken : cancellationToken ) ;
else mediaProcessorTask = mediaProcessor . Run ( rawFile , extension , mediaType , outputFile , trimStart , trimEnd , cancellationToken ) ;
await mediaProcessorTask ;
2022-02-16 03:15:41 +01:00
// Return output file
return outputFile ;
}
#endregion
#region LOCAL METHODS
// GET CHUNKS DATA FROM M3U8 PLAYLIST
2022-03-02 22:13:28 +01:00
private static async Task < List < ( Uri ChunkUrl , TimeSpan ChunkDuration ) > > ExtractChunksFromM3U8Async ( Uri streamUrl , CancellationToken cancellationToken = default )
2022-02-16 03:15:41 +01:00
{
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2022-03-07 14:59:11 +01:00
// Get response
string response = null ;
using ( WebClient client = Client . GQL ( ) )
{
response = await client . DownloadStringTaskAsync ( streamUrl ) ;
}
2022-03-02 22:13:28 +01:00
2022-02-16 03:15:41 +01:00
// Create dictionary
List < ( Uri ChunkUrl , TimeSpan ChunkDuration ) > chunks = new List < ( Uri ChunkUrl , TimeSpan ChunkDuration ) > ( ) ;
2022-03-07 14:59:11 +01:00
// Chunk data regular expression
Regex chunkDataRegex = new Regex ( @"#EXTINF:(?<duration>\d+.\d+),\n(?<filename>\S+.ts)" ) ;
// Chunks location
string chunkLocationPath = streamUrl . AbsoluteUri . Replace ( System . IO . Path . GetFileName ( streamUrl . AbsoluteUri ) , "" ) ;
2022-02-16 03:15:41 +01:00
// Pack data into dictionary
2022-03-07 14:59:11 +01:00
foreach ( Match chunk in chunkDataRegex . Matches ( response ) )
2022-02-16 03:15:41 +01:00
{
2022-03-07 14:59:11 +01:00
Uri chunkUrl = new Uri ( $"{chunkLocationPath}{chunk.Groups[" filename "].Value}" ) ;
2022-02-16 03:15:41 +01:00
TimeSpan chunkDuration = TimeSpan . FromSeconds ( double . Parse ( chunk . Groups [ "duration" ] . Value ) ) ;
chunks . Add ( ( chunkUrl , chunkDuration ) ) ;
}
// Return chunks data
return chunks ;
}
// PASSIVE TRIM
2022-03-07 14:59:11 +01:00
private static ( TimeSpan NewTrimStart , TimeSpan NewTrimEnd , TimeSpan NewDuration ) PassiveVideoTrim ( List < ( Uri ChunkUrl , TimeSpan ChunkDuration ) > chunksList , TimeSpan trimStart , TimeSpan trimEnd , TimeSpan duration )
2022-02-16 03:15:41 +01:00
{
// Copy duration
TimeSpan newDuration = duration ;
// Trim at start
while ( chunksList [ 0 ] . ChunkDuration < = trimStart )
{
trimStart = trimStart . Subtract ( chunksList [ 0 ] . ChunkDuration ) ;
trimEnd = trimEnd . Subtract ( chunksList [ 0 ] . ChunkDuration ) ;
newDuration = newDuration . Subtract ( chunksList [ 0 ] . ChunkDuration ) ;
chunksList . RemoveAt ( 0 ) ;
}
// Trim at end
while ( chunksList . Last ( ) . ChunkDuration < = newDuration . Subtract ( trimEnd ) )
{
newDuration = newDuration . Subtract ( chunksList . Last ( ) . ChunkDuration ) ;
chunksList . RemoveAt ( chunksList . Count - 1 ) ;
}
// Return data
2022-03-07 14:59:11 +01:00
return ( trimStart , trimEnd , newDuration ) ;
2022-02-16 03:15:41 +01:00
}
// DOWNLOAD CHUNK
2022-03-02 22:13:28 +01:00
private static async Task < byte [ ] > DownloadChunkAsync ( Uri chunkUrl , CancellationToken cancellationToken = default )
2022-02-16 03:15:41 +01:00
{
int retriesCount = 0 ;
2022-03-03 00:34:52 +01:00
while ( true )
2022-02-16 03:15:41 +01:00
{
2022-03-02 22:13:28 +01:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2022-02-16 03:15:41 +01:00
try
{
using ( WebClient client = new WebClient ( ) )
{
return await client . DownloadDataTaskAsync ( chunkUrl ) ;
}
}
2022-03-03 00:34:52 +01:00
catch ( WebException wex )
2022-02-16 03:15:41 +01:00
{
2022-03-03 00:34:52 +01:00
if ( ( bool ) Config . GetValue ( "twitch_vod_downloading_chunk_retry_after_error" ) & & retriesCount < ( int ) Config . GetValue ( "twitch_vod_downloading_chunk_max_retries" ) )
{
retriesCount + + ;
await Task . Delay ( ( int ) Config . GetValue ( "twitch_vod_downloading_chunk_retries_delay" ) ) ;
}
else throw wex ;
2022-02-16 03:15:41 +01:00
}
}
}
// WRITE CHUNK TO FILE
private static Task WriteChunkToFileAsync ( StorageFile file , byte [ ] chunk )
{
return Task . Factory . StartNew ( ( ) = >
{
using ( var stream = new System . IO . FileStream ( file . Path , System . IO . FileMode . Append ) )
{
stream . Write ( chunk , 0 , chunk . Length ) ;
stream . Close ( ) ;
}
} ) ;
}
2022-02-28 13:31:58 +01:00
// PARSE DURATION
2022-02-26 14:32:34 +01:00
private static TimeSpan ParseDuration ( string duration )
{
char [ ] separators = { 'h' , 'm' , 's' } ;
string [ ] durationParts = duration . Split ( separators , StringSplitOptions . RemoveEmptyEntries ) . Reverse ( ) . ToArray ( ) ;
TimeSpan timeSpan = new TimeSpan (
durationParts . Count ( ) > 2 ? int . Parse ( durationParts [ 2 ] ) : 0 ,
durationParts . Count ( ) > 1 ? int . Parse ( durationParts [ 1 ] ) : 0 ,
int . Parse ( durationParts [ 0 ] ) ) ;
return timeSpan ;
}
2022-02-16 03:15:41 +01:00
#endregion
#region EVENT HANDLERS
2022-03-07 14:59:11 +01:00
public event EventHandler < EventArgs . ProgressChangedEventArgs > DownloadingProgressChanged ;
public event EventHandler < EventArgs . ProgressChangedEventArgs > ProcessingProgressChanged ;
2022-02-16 03:15:41 +01:00
#endregion
}
}