Video adding and subscriptions finished
This commit is contained in:
113
VDownload.Core/Sources/Twitch/Channel.cs
Normal file
113
VDownload.Core/Sources/Twitch/Channel.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Exceptions;
|
||||
using VDownload.Core.Interfaces;
|
||||
using VDownload.Core.Services.Sources.Twitch.Helpers;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch
|
||||
{
|
||||
[Serializable]
|
||||
public class Channel : IPlaylist
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public Channel(string id)
|
||||
{
|
||||
Source = PlaylistSource.TwitchChannel;
|
||||
ID = id;
|
||||
Url = new Uri($"https://twitch.tv/{ID}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public PlaylistSource Source { get; private set; }
|
||||
public string ID { get; private set; }
|
||||
private string UniqueID { get; set; }
|
||||
public Uri Url { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public IVideo[] Videos { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
JToken response = null;
|
||||
using (WebClient client = await Client.Helix())
|
||||
{
|
||||
client.QueryString.Add("login", ID);
|
||||
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"];
|
||||
if (((JArray)response).Count > 0) response = response[0];
|
||||
else throw new MediaNotFoundException($"Twitch Channel (ID: {ID}) was not found");
|
||||
}
|
||||
|
||||
UniqueID = (string)response["id"];
|
||||
Name = (string)response["display_name"];
|
||||
}
|
||||
|
||||
public async Task GetVideosAsync(CancellationToken cancellationToken = default) => await GetVideosAsync(0, cancellationToken);
|
||||
public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string pagination = "";
|
||||
|
||||
List<Vod> videos = new List<Vod>();
|
||||
|
||||
bool getAll = numberOfVideos == 0;
|
||||
int count;
|
||||
JToken[] videosData;
|
||||
List<Task> getStreamsTasks = new List<Task>();
|
||||
do
|
||||
{
|
||||
count = numberOfVideos < 100 && !getAll ? numberOfVideos : 100;
|
||||
|
||||
JToken response = null;
|
||||
using (WebClient client = await Client.Helix())
|
||||
{
|
||||
client.QueryString.Add("user_id", UniqueID);
|
||||
client.QueryString.Add("first", count.ToString());
|
||||
client.QueryString.Add("after", pagination);
|
||||
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos"));
|
||||
}
|
||||
|
||||
pagination = (string)response["pagination"]["cursor"];
|
||||
|
||||
videosData = response["data"].ToArray();
|
||||
foreach (JToken videoData in videosData)
|
||||
{
|
||||
Vod video = new Vod((string)videoData["id"]);
|
||||
video.GetMetadataAsync(videoData);
|
||||
getStreamsTasks.Add(video.GetStreamsAsync());
|
||||
videos.Add(video);
|
||||
|
||||
numberOfVideos--;
|
||||
}
|
||||
}
|
||||
while ((getAll || numberOfVideos > 0) && count == videosData.Length);
|
||||
|
||||
await Task.WhenAll(getStreamsTasks);
|
||||
|
||||
Videos = videos.ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
158
VDownload.Core/Sources/Twitch/Clip.cs
Normal file
158
VDownload.Core/Sources/Twitch/Clip.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Exceptions;
|
||||
using VDownload.Core.Interfaces;
|
||||
using VDownload.Core.Services.Sources.Twitch.Helpers;
|
||||
using VDownload.Core.Structs;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch
|
||||
{
|
||||
[Serializable]
|
||||
public class Clip : IVideo
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public Clip(string id)
|
||||
{
|
||||
Source = VideoSource.TwitchClip;
|
||||
ID = id;
|
||||
Url = new Uri($"https://clips.twitch.tv/{ID}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public VideoSource Source { get; private set; }
|
||||
public string ID { get; private set; }
|
||||
public Uri Url { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Author { get; private set; }
|
||||
public DateTime Date { get; private set; }
|
||||
public TimeSpan Duration { get; private set; }
|
||||
public long Views { get; private set; }
|
||||
public Uri Thumbnail { get; private set; }
|
||||
public BaseStream[] BaseStreams { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
JToken response = null;
|
||||
using (WebClient client = await Client.Helix())
|
||||
{
|
||||
client.QueryString.Add("id", ID);
|
||||
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/clips")).GetValue("data");
|
||||
if (((JArray)response).Count > 0) response = response[0];
|
||||
else throw new MediaNotFoundException($"Twitch Clip (ID: {ID}) was not found");
|
||||
}
|
||||
|
||||
Title = (string)response["title"];
|
||||
Author = (string)response["broadcaster_name"];
|
||||
Date = Convert.ToDateTime(response["created_at"]);
|
||||
Duration = TimeSpan.FromSeconds((double)response["duration"]);
|
||||
Views = (long)response["view_count"];
|
||||
Thumbnail = new Uri((string)response["thumbnail_url"]);
|
||||
}
|
||||
|
||||
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
JToken[] response;
|
||||
using (WebClient client = Client.GQL())
|
||||
{
|
||||
response = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["videoQualities"].ToArray();
|
||||
}
|
||||
|
||||
List<BaseStream> streams = new List<BaseStream>();
|
||||
|
||||
foreach (JToken streamData in response)
|
||||
{
|
||||
BaseStream stream = new BaseStream()
|
||||
{
|
||||
Url = new Uri((string)streamData["sourceURL"]),
|
||||
Height = int.Parse((string)streamData["quality"]),
|
||||
FrameRate = (int)streamData["frameRate"],
|
||||
};
|
||||
|
||||
streams.Add(stream);
|
||||
}
|
||||
|
||||
BaseStreams = streams.ToArray();
|
||||
}
|
||||
|
||||
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
|
||||
|
||||
JToken videoAccessToken = null;
|
||||
using (WebClient client = Client.GQL())
|
||||
{
|
||||
videoAccessToken = JArray.Parse(await client.UploadStringTaskAsync("https://gql.twitch.tv/gql", "[{\"operationName\":\"VideoAccessToken_Clip\",\"variables\":{\"slug\":\"" + ID + "\"},\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11\"}}}]"))[0]["data"]["clip"]["playbackAccessToken"];
|
||||
}
|
||||
|
||||
// Download
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.mp4");
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(a.ProgressPercentage)); };
|
||||
client.QueryString.Add("sig", (string)videoAccessToken["signature"]);
|
||||
client.QueryString.Add("token", HttpUtility.UrlEncode((string)videoAccessToken["value"]));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (cancellationToken.Register(client.CancelAsync))
|
||||
{
|
||||
await client.DownloadFileTaskAsync(baseStream.Url, rawFile.Path);
|
||||
}
|
||||
}
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
|
||||
|
||||
// Processing
|
||||
StorageFile outputFile = rawFile;
|
||||
if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trim.Start != null || trim.End != null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
|
||||
|
||||
MediaProcessor mediaProcessor = new MediaProcessor();
|
||||
mediaProcessor.ProgressChanged += ProcessingProgressChanged;
|
||||
|
||||
await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken);
|
||||
}
|
||||
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region EVENTS
|
||||
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
107
VDownload.Core/Sources/Twitch/Helpers/Authorization.cs
Normal file
107
VDownload.Core/Sources/Twitch/Helpers/Authorization.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Core.Sources;
|
||||
using VDownload.Core.Structs;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch.Helpers
|
||||
{
|
||||
public static class Authorization
|
||||
{
|
||||
#region CONSTANTS
|
||||
|
||||
public readonly static string ClientID = "yukkqkwp61wsv3u1pya17crpyaa98y";
|
||||
public readonly static string GQLApiClientID = "kimne78kx3ncx6brgo4mv6wki5h1ko";
|
||||
public readonly static Uri RedirectUrl = new Uri("https://www.vd.com");
|
||||
|
||||
private readonly static string ResponseType = "token";
|
||||
private readonly static string[] Scopes = new[]
|
||||
{
|
||||
"user:read:subscriptions",
|
||||
};
|
||||
public readonly static Uri AuthorizationUrl = new Uri($"https://id.twitch.tv/oauth2/authorize?client_id={ClientID}&redirect_uri={RedirectUrl.OriginalString}&response_type={ResponseType}&scope={string.Join(" ", Scopes)}");
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region METHODS
|
||||
|
||||
public static async Task<string> ReadAccessTokenAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName);
|
||||
StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}");
|
||||
|
||||
return await FileIO.ReadTextAsync(authDataFile);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SaveAccessTokenAsync(string accessToken)
|
||||
{
|
||||
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.CreateFolderAsync(AuthorizationData.FolderName, CreationCollisionOption.OpenIfExists);
|
||||
StorageFile authDataFile = await authDataFolder.CreateFileAsync($"Twitch.{AuthorizationData.FilesExtension}", CreationCollisionOption.ReplaceExisting);
|
||||
|
||||
await FileIO.WriteTextAsync(authDataFile, accessToken);
|
||||
}
|
||||
|
||||
public static async Task DeleteAccessTokenAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StorageFolder authDataFolder = await AuthorizationData.FolderLocation.GetFolderAsync(AuthorizationData.FolderName);
|
||||
StorageFile authDataFile = await authDataFolder.GetFileAsync($"Twitch.{AuthorizationData.FilesExtension}");
|
||||
|
||||
await authDataFile.DeleteAsync();
|
||||
}
|
||||
catch (FileNotFoundException) { }
|
||||
}
|
||||
|
||||
public static async Task<TwitchAccessTokenValidationData> ValidateAccessTokenAsync(string accessToken)
|
||||
{
|
||||
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
|
||||
client.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
|
||||
try
|
||||
{
|
||||
JObject response = JObject.Parse(await client.DownloadStringTaskAsync("https://id.twitch.tv/oauth2/validate"));
|
||||
|
||||
string login = response["login"].ToString();
|
||||
DateTime? expirationDate = DateTime.Now.AddSeconds(long.Parse(response["expires_in"].ToString()));
|
||||
|
||||
return new TwitchAccessTokenValidationData(accessToken, true, login, expirationDate);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
if (ex.Response is null)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
else
|
||||
{
|
||||
JObject exInfo = JObject.Parse(new StreamReader(ex.Response.GetResponseStream()).ReadToEnd());
|
||||
if ((int)exInfo["status"] == 401) return new TwitchAccessTokenValidationData(accessToken, false, null, null);
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task RevokeAccessTokenAsync(string accessToken)
|
||||
{
|
||||
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
|
||||
|
||||
await client.UploadStringTaskAsync(new Uri("https://id.twitch.tv/oauth2/revoke"), $"client_id={ClientID}&token={accessToken}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
32
VDownload.Core/Sources/Twitch/Helpers/Client.cs
Normal file
32
VDownload.Core/Sources/Twitch/Helpers/Client.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Core.Exceptions;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch.Helpers
|
||||
{
|
||||
internal static class Client
|
||||
{
|
||||
internal static async Task<WebClient> Helix()
|
||||
{
|
||||
string accessToken = await Authorization.ReadAccessTokenAsync();
|
||||
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
|
||||
|
||||
var twitchAccessTokenValidation = await Authorization.ValidateAccessTokenAsync(accessToken);
|
||||
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
|
||||
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
client.Headers.Add("Client-Id", Authorization.ClientID);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
internal static WebClient GQL()
|
||||
{
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Client-Id", Authorization.GQLApiClientID);
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
285
VDownload.Core/Sources/Twitch/Vod.cs
Normal file
285
VDownload.Core/Sources/Twitch/Vod.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Exceptions;
|
||||
using VDownload.Core.Interfaces;
|
||||
using VDownload.Core.Services.Sources.Twitch.Helpers;
|
||||
using VDownload.Core.Structs;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch
|
||||
{
|
||||
[Serializable]
|
||||
public class Vod : IVideo
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public Vod(string id)
|
||||
{
|
||||
Source = VideoSource.TwitchVod;
|
||||
ID = id;
|
||||
Url = new Uri($"https://www.twitch.tv/videos/{ID}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public VideoSource Source { get; private set; }
|
||||
public string ID { get; private set; }
|
||||
public Uri Url { get; private set; }
|
||||
public string Title { get; private set; }
|
||||
public string Author { get; private set; }
|
||||
public DateTime Date { get; private set; }
|
||||
public TimeSpan Duration { get; private set; }
|
||||
public long Views { get; private set; }
|
||||
public Uri Thumbnail { get; private set; }
|
||||
public BaseStream[] BaseStreams { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
JToken response = null;
|
||||
using (WebClient client = await Client.Helix())
|
||||
{
|
||||
client.QueryString.Add("id", ID);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
GetMetadataAsync(response);
|
||||
}
|
||||
internal void GetMetadataAsync(JToken response)
|
||||
{
|
||||
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"));
|
||||
}
|
||||
|
||||
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string[] response = null;
|
||||
using (WebClient client = Client.GQL())
|
||||
{
|
||||
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"];
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
List<BaseStream> streams = new List<BaseStream>();
|
||||
|
||||
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+))?");
|
||||
|
||||
for (int i = 2; i < response.Length; i += 3)
|
||||
{
|
||||
Match line2 = streamDataL2Regex.Match(response[i + 1]);
|
||||
|
||||
BaseStream stream = new BaseStream()
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
streams.Add(stream);
|
||||
}
|
||||
|
||||
BaseStreams = streams.ToArray();
|
||||
}
|
||||
|
||||
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TrimData trim, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
DownloadingProgressChanged.Invoke(this, new EventArgs.ProgressChangedEventArgs(0));
|
||||
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(baseStream.Url, cancellationToken);
|
||||
|
||||
TimeSpan duration = Duration;
|
||||
|
||||
// Passive trim
|
||||
if ((bool)Config.GetValue("twitch_vod_passive_trim") && trim.Start != TimeSpan.Zero && trim.End != duration) (trim, duration) = PassiveVideoTrim(chunksList, trim, Duration);
|
||||
|
||||
// Download
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
|
||||
|
||||
double chunksDownloaded = 0;
|
||||
|
||||
Task<byte[]> downloadTask;
|
||||
Task writeTask;
|
||||
|
||||
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);
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(++chunksDownloaded * 100 / chunksList.Count));
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await WriteChunkToFileAsync(rawFile, downloadTask.Result);
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
|
||||
|
||||
// Processing
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
StorageFile outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
|
||||
|
||||
MediaProcessor mediaProcessor = new MediaProcessor();
|
||||
mediaProcessor.ProgressChanged += ProcessingProgressChanged;
|
||||
|
||||
await mediaProcessor.Run(rawFile, extension, mediaType, outputFile, trim, cancellationToken);
|
||||
|
||||
// Return output file
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private static async Task<List<(Uri ChunkUrl, TimeSpan ChunkDuration)>> ExtractChunksFromM3U8Async(Uri streamUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
string response = null;
|
||||
using (WebClient client = Client.GQL())
|
||||
{
|
||||
response = await client.DownloadStringTaskAsync(streamUrl);
|
||||
}
|
||||
|
||||
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>();
|
||||
|
||||
Regex chunkDataRegex = new Regex(@"#EXTINF:(?<duration>\d+.\d+),\n(?<filename>\S+.ts)");
|
||||
|
||||
string chunkLocationPath = streamUrl.AbsoluteUri.Replace(Path.GetFileName(streamUrl.AbsoluteUri), "");
|
||||
|
||||
foreach (Match chunk in chunkDataRegex.Matches(response))
|
||||
{
|
||||
Uri chunkUrl = new Uri($"{chunkLocationPath}{chunk.Groups["filename"].Value}");
|
||||
TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(chunk.Groups["duration"].Value));
|
||||
chunks.Add((chunkUrl, chunkDuration));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static (TrimData Trim, TimeSpan NewDuration) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TrimData trim, TimeSpan duration)
|
||||
{
|
||||
TimeSpan newDuration = duration;
|
||||
|
||||
while (chunksList[0].ChunkDuration <= trim.Start)
|
||||
{
|
||||
trim.Start = trim.Start.Subtract(chunksList[0].ChunkDuration);
|
||||
trim.End = trim.End.Subtract(chunksList[0].ChunkDuration);
|
||||
newDuration = newDuration.Subtract(chunksList[0].ChunkDuration);
|
||||
chunksList.RemoveAt(0);
|
||||
}
|
||||
|
||||
while (chunksList.Last().ChunkDuration <= newDuration.Subtract(trim.End))
|
||||
{
|
||||
newDuration = newDuration.Subtract(chunksList.Last().ChunkDuration);
|
||||
chunksList.RemoveAt(chunksList.Count - 1);
|
||||
}
|
||||
|
||||
return (trim, newDuration);
|
||||
}
|
||||
|
||||
private static async Task<byte[]> DownloadChunkAsync(Uri chunkUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int retriesCount = 0;
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
return await client.DownloadDataTaskAsync(chunkUrl);
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Task WriteChunkToFileAsync(StorageFile file, byte[] chunk)
|
||||
{
|
||||
return Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var stream = new FileStream(file.Path, FileMode.Append))
|
||||
{
|
||||
stream.Write(chunk, 0, chunk.Length);
|
||||
stream.Close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region EVENTS
|
||||
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user