1.0-dev10 (Code cleaning)

This commit is contained in:
2022-03-02 22:13:28 +01:00
Unverified
parent 25041f9d43
commit d8eb103dcc
36 changed files with 338 additions and 336 deletions

View File

@@ -1,6 +1,6 @@
namespace VDownload.Core.Enums
{
public enum VideoStatus
public enum TaskStatus
{
Idle,
Waiting,

View File

@@ -0,0 +1,8 @@
namespace VDownload.Core.EventArgs
{
public class PlaylistSearchEventArgs : System.EventArgs
{
public string Phrase { get; set; }
public int Count { get; set; }
}
}

View File

@@ -4,13 +4,13 @@ using VDownload.Core.Interfaces;
using VDownload.Core.Objects;
using Windows.Storage;
namespace VDownload.Core.EventArgsObjects
namespace VDownload.Core.EventArgs
{
public class VideoAddEventArgs : EventArgs
public class VideoAddEventArgs : System.EventArgs
{
public IVideoService VideoService { get; set; }
public MediaType MediaType { get; set; }
public Stream Stream { get; set; }
public IBaseStream Stream { get; set; }
public TimeSpan TrimStart { get; set; }
public TimeSpan TrimEnd { get; set; }
public string Filename { get; set; }

View File

@@ -0,0 +1,7 @@
namespace VDownload.Core.EventArgs
{
public class VideoSearchEventArgs : System.EventArgs
{
public string Phrase { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
using System;
namespace VDownload.Core.EventArgsObjects
{
public class PlaylistSearchEventArgs : EventArgs
{
public string Phrase { get; set; }
public int Count { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
using System;
namespace VDownload.Core.EventArgsObjects
{
public class VideoSearchEventArgs : EventArgs
{
public string Phrase { get; set; }
}
}

View File

@@ -1,18 +0,0 @@
using System;
using VDownload.Core.Enums;
namespace VDownload.Core.Interfaces
{
public interface IAStream
{
#region PROPERTIES
Uri Url { get; }
bool IsChunked { get; }
StreamType StreamType { get; }
int AudioBitrate { get; }
string AudioCodec { get; }
#endregion
}
}

View File

@@ -1,19 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Enums;
namespace VDownload.Core.Interfaces
{
public interface IVStream
public interface IBaseStream
{
#region PROPERTIES
Uri Url { get; }
bool IsChunked { get; }
StreamType StreamType { get; }
int Width { get; }
int Height { get; }
int FrameRate { get; }
string VideoCodec { get; }
#endregion
}

View File

@@ -7,6 +7,7 @@ namespace VDownload.Core.Interfaces
{
#region PROPERTIES
// PLAYLIST PROPERTIES
string ID { get; }
string Name { get; }
@@ -16,8 +17,10 @@ namespace VDownload.Core.Interfaces
#region METHODS
// GET PLAYLIST METADATA
Task GetMetadataAsync(CancellationToken cancellationToken = default);
// GET VIDEOS FROM PLAYLIST
Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default);
#endregion

View File

@@ -3,7 +3,6 @@ using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Objects;
using Windows.Storage;
namespace VDownload.Core.Interfaces
@@ -21,7 +20,7 @@ namespace VDownload.Core.Interfaces
TimeSpan Duration { get; }
long Views { get; }
Uri Thumbnail { get; }
Stream[] Streams { get; }
IBaseStream[] BaseStreams { get; }
#endregion
@@ -36,14 +35,7 @@ namespace VDownload.Core.Interfaces
Task GetStreamsAsync(CancellationToken cancellationToken = default);
// DOWNLOAD VIDEO
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default);
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
#endregion

View File

@@ -4,7 +4,7 @@ using VDownload.Core.Interfaces;
namespace VDownload.Core.Objects
{
public class Stream : IVStream, IAStream
public class Stream : IBaseStream
{
#region CONSTRUCTORS

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using VDownload.Core.Enums;
using Windows.Media.Editing;
using Windows.Storage;

View File

@@ -15,7 +15,7 @@ namespace VDownload.Core.Services
{
public class MediaProcessor
{
#region CONSTRUCTOR
#region CONSTRUCTORS
public MediaProcessor(StorageFile outputFile, TimeSpan trimStart, TimeSpan trimEnd)
{
@@ -40,10 +40,11 @@ namespace VDownload.Core.Services
#region STANDARD METHODS
// SINGLE AUDIO & VIDEO FILE PROCESSING
public async Task Run(StorageFile audioVideoInputFile, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default)
{
// Invoke ProcessingStarted event
ProcessingStarted?.Invoke(this, EventArgs.Empty);
ProcessingStarted?.Invoke(this, System.EventArgs.Empty);
// Init transcoder
MediaTranscoder mediaTranscoder = new MediaTranscoder
@@ -55,31 +56,31 @@ namespace VDownload.Core.Services
};
// Start transcoding operation
cancellationToken.ThrowIfCancellationRequested();
using (IRandomAccessStream outputFileOpened = await OutputFile.OpenAsync(FileAccessMode.ReadWrite))
{
PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await audioVideoInputFile.OpenAsync(FileAccessMode.Read), outputFileOpened, await GetMediaEncodingProfile(audioVideoInputFile, extension, mediaType));
IAsyncActionWithProgress<double> transcodingTask = transcodingPreparated.TranscodeAsync();
try
{
await transcodingTask.AsTask(cancellationToken, new Progress<double>((percent) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(percent), null)); }));
await outputFileOpened.FlushAsync();
}
catch (TaskCanceledException) { }
await transcodingTask.AsTask(cancellationToken, new Progress<double>((percent) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(percent), null)); }));
await outputFileOpened.FlushAsync();
transcodingTask.Close();
}
// Invoke ProcessingCompleted event
ProcessingCompleted?.Invoke(this, EventArgs.Empty);
ProcessingCompleted?.Invoke(this, System.EventArgs.Empty);
}
// SEPARATE AUDIO & VIDEO FILES PROCESSING
public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default)
{
// Invoke ProcessingStarted event
ProcessingStarted?.Invoke(this, EventArgs.Empty);
ProcessingStarted?.Invoke(this, System.EventArgs.Empty);
// Init editor
MediaComposition mediaEditor = new MediaComposition();
// Add media files
cancellationToken.ThrowIfCancellationRequested();
Task<MediaClip> getVideoFileTask = MediaClip.CreateFromFileAsync(videoFile).AsTask();
Task<BackgroundAudioTrack> getAudioFileTask = BackgroundAudioTrack.CreateFromFileAsync(audioFile).AsTask();
await Task.WhenAll(getVideoFileTask, getAudioFileTask);
@@ -97,11 +98,14 @@ namespace VDownload.Core.Services
// Start rendering operation
var renderOperation = mediaEditor.RenderToFileAsync(OutputFile, (MediaTrimmingPreference)Config.GetValue("media_editing_algorithm"), await GetMediaEncodingProfile(videoFile, audioFile, (MediaFileExtension)extension, MediaType.AudioVideo));
renderOperation.Progress += (info, progress) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(progress), null)); };
cancellationToken.ThrowIfCancellationRequested();
await renderOperation.AsTask(cancellationToken);
// Invoke ProcessingCompleted event
ProcessingCompleted?.Invoke(this, EventArgs.Empty);
ProcessingCompleted?.Invoke(this, System.EventArgs.Empty);
}
// SINGLE AUDIO OR VIDEO FILES PROCESSING
public async Task Run(StorageFile audioFile, AudioFileExtension extension, CancellationToken cancellationToken = default) { await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, cancellationToken); }
public async Task Run(StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default) { await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, cancellationToken); }

View File

@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using VDownload.Core.Enums;
namespace VDownload.Core.Services
@@ -12,6 +7,7 @@ namespace VDownload.Core.Services
{
#region CONSTANTS
// VIDEO SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, VideoSource Type)[] VideoSources = new (Regex Regex, VideoSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/videos/(?<id>\d+)"), VideoSource.TwitchVod),
@@ -19,6 +15,7 @@ namespace VDownload.Core.Services
(new Regex(@"^https://clips.twitch.tv/(?<id>[^?]+)"), VideoSource.TwitchClip),
};
// PLAYLIST SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, PlaylistSource Type)[] PlaylistSources = new (Regex Regex, PlaylistSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/(?<id>[^?]+)"), PlaylistSource.TwitchChannel),
@@ -30,6 +27,7 @@ namespace VDownload.Core.Services
#region METHODS
// GET VIDEO SOURCE
public static (VideoSource Type, string ID) GetVideoSource(string url)
{
foreach ((Regex Regex, VideoSource Type) Source in VideoSources)
@@ -40,6 +38,7 @@ namespace VDownload.Core.Services
return (VideoSource.Null, null);
}
// GET PLAYLIST SOURCE
public static (PlaylistSource Type, string ID) GetPlaylistSource(string url)
{
foreach ((Regex Regex, PlaylistSource Type) Source in PlaylistSources)

View File

@@ -65,7 +65,7 @@ namespace VDownload.Core.Services.Sources.Twitch
StorageFile authDataFile = await authDataFolder.CreateFileAsync("Twitch.auth", CreationCollisionOption.ReplaceExisting);
// Save data
FileIO.WriteTextAsync(authDataFile, accessToken);
await FileIO.WriteTextAsync(authDataFile, accessToken);
}
// DELETE ACCESS TOKEN

View File

@@ -40,14 +40,13 @@ namespace VDownload.Core.Services.Sources.Twitch
// GET CHANNEL METADATA
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
cancellationToken.ThrowIfCancellationRequested();
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
// Check access token
cancellationToken.ThrowIfCancellationRequested();
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
@@ -58,6 +57,7 @@ namespace VDownload.Core.Services.Sources.Twitch
// Get response
client.QueryString.Add("login", ID);
cancellationToken.ThrowIfCancellationRequested();
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"][0];
// Set parameters
@@ -68,10 +68,8 @@ namespace VDownload.Core.Services.Sources.Twitch
// GET CHANNEL VIDEOS
public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
cancellationToken.ThrowIfCancellationRequested();
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
@@ -88,6 +86,7 @@ namespace VDownload.Core.Services.Sources.Twitch
do
{
// Check access token
cancellationToken.ThrowIfCancellationRequested();
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
@@ -104,6 +103,7 @@ namespace VDownload.Core.Services.Sources.Twitch
client.QueryString.Add("first", count.ToString());
client.QueryString.Add("after", pagination);
cancellationToken.ThrowIfCancellationRequested();
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos"));
pagination = (string)response["pagination"]["cursor"];

View File

@@ -48,7 +48,7 @@ namespace VDownload.Core.Services.Sources.Twitch
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public Stream[] Streams { get; private set; }
public IBaseStream[] BaseStreams { get; private set; }
#endregion
@@ -59,14 +59,13 @@ namespace VDownload.Core.Services.Sources.Twitch
// GET CLIP METADATA
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
cancellationToken.ThrowIfCancellationRequested();
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
// Check access token
cancellationToken.ThrowIfCancellationRequested();
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
@@ -93,14 +92,12 @@ namespace VDownload.Core.Services.Sources.Twitch
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Create client
WebClient client = new WebClient { Encoding = Encoding.UTF8 };
client.Headers.Add("Client-ID", Auth.GQLApiClientID);
// Get video streams
cancellationToken.ThrowIfCancellationRequested();
JToken[] 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();
// Init streams list
@@ -126,60 +123,55 @@ namespace VDownload.Core.Services.Sources.Twitch
}
// Set Streams parameter
Streams = streams.ToArray();
BaseStreams = streams.ToArray();
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Invoke DownloadingStarted event
DownloadingStarted?.Invoke(this, EventArgs.Empty);
DownloadingStarted?.Invoke(this, System.EventArgs.Empty);
// Create client
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
// Get video GQL access token
cancellationToken.ThrowIfCancellationRequested();
JToken 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 (client = new WebClient())
{
client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new ProgressChangedEventArgs(a.ProgressPercentage, null)); };
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(audioVideoStream.Url, rawFile.Path);
await client.DownloadFileTaskAsync(baseStream.Url, rawFile.Path);
}
}
DownloadingCompleted?.Invoke(this, EventArgs.Empty);
DownloadingCompleted?.Invoke(this, System.EventArgs.Empty);
// Processing
StorageFile outputFile = rawFile;
if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart > new TimeSpan(0) || trimEnd < Duration)
{
cancellationToken.ThrowIfCancellationRequested();
outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd);
mediaProcessor.ProcessingStarted += ProcessingStarted;
mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged;
mediaProcessor.ProcessingCompleted += ProcessingCompleted;
cancellationToken.ThrowIfCancellationRequested();
await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken);
}
// Return output file
return outputFile;
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioVideoStream, extension, mediaType, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch Clip download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
#endregion

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
@@ -51,7 +50,7 @@ namespace VDownload.Core.Services.Sources.Twitch
public TimeSpan Duration { get; private set; }
public long Views { get; private set; }
public Uri Thumbnail { get; private set; }
public Stream[] Streams { get; private set; }
public IBaseStream[] BaseStreams { get; private set; }
#endregion
@@ -62,14 +61,13 @@ namespace VDownload.Core.Services.Sources.Twitch
// GET VOD METADATA
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
cancellationToken.ThrowIfCancellationRequested();
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
// Check access token
cancellationToken.ThrowIfCancellationRequested();
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
@@ -80,6 +78,7 @@ namespace VDownload.Core.Services.Sources.Twitch
// Get response
client.QueryString.Add("id", ID);
cancellationToken.ThrowIfCancellationRequested();
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0];
// Set parameters
@@ -102,17 +101,16 @@ namespace VDownload.Core.Services.Sources.Twitch
// GET VOD STREAMS
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Create client
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
// 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();
string[] response = (await client.DownloadStringTaskAsync($"http://usher.twitch.tv/vod/{ID}?nauth={videoAccessToken["value"]}&nauthsig={videoAccessToken["signature"]}&allow_source=true&player=twitchweb")).Split("\n");
// Init streams list
@@ -147,74 +145,64 @@ namespace VDownload.Core.Services.Sources.Twitch
}
// Set Streams parameter
Streams = streams.ToArray();
BaseStreams = streams.ToArray();
}
// DOWNLOAD AND TRANSCODE VOD
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
{
// Invoke DownloadingStarted event
if (!cancellationToken.IsCancellationRequested) DownloadingStarted?.Invoke(this, EventArgs.Empty);
DownloadingStarted?.Invoke(this, System.EventArgs.Empty);
// Get video chunks
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = null;
if (!cancellationToken.IsCancellationRequested) chunksList = await ExtractChunksFromM3U8Async(audioVideoStream.Url);
cancellationToken.ThrowIfCancellationRequested();
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(baseStream.Url, cancellationToken);
// Passive trim
if ((bool)Config.GetValue("twitch_vod_passive_trim")) (trimStart, trimEnd) = PassiveVideoTrim(chunksList, trimStart, trimEnd, Duration);
// Download
StorageFile rawFile = null;
if (!cancellationToken.IsCancellationRequested)
cancellationToken.ThrowIfCancellationRequested();
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
float chunksDownloaded = 0;
Task<byte[]> downloadTask;
Task writeTask;
cancellationToken.ThrowIfCancellationRequested();
downloadTask = DownloadChunkAsync(chunksList[0].ChunkUrl);
await downloadTask;
for (int i = 1; i < chunksList.Count; i++)
{
rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
float chunksDownloaded = 0;
Task<byte[]> downloadTask;
Task writeTask;
downloadTask = DownloadChunkAsync(chunksList[0].ChunkUrl);
await downloadTask;
for (int i = 1; i < chunksList.Count && !cancellationToken.IsCancellationRequested; i++)
{
writeTask = WriteChunkToFileAsync(rawFile, downloadTask.Result);
downloadTask = DownloadChunkAsync(chunksList[i].ChunkUrl);
await Task.WhenAll(writeTask, downloadTask);
DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null));
}
if (!cancellationToken.IsCancellationRequested)
{
await WriteChunkToFileAsync(rawFile, downloadTask.Result);
DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null));
DownloadingCompleted?.Invoke(this, EventArgs.Empty);
}
cancellationToken.ThrowIfCancellationRequested();
writeTask = WriteChunkToFileAsync(rawFile, downloadTask.Result);
downloadTask = DownloadChunkAsync(chunksList[i].ChunkUrl);
await Task.WhenAll(writeTask, downloadTask);
DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null));
}
cancellationToken.ThrowIfCancellationRequested();
await WriteChunkToFileAsync(rawFile, downloadTask.Result);
DownloadingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(++chunksDownloaded * 100 / chunksList.Count), null));
DownloadingCompleted?.Invoke(this, System.EventArgs.Empty);
// Processing
StorageFile outputFile = null;
if (!cancellationToken.IsCancellationRequested)
{
outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
cancellationToken.ThrowIfCancellationRequested();
StorageFile outputFile = await downloadingFolder.CreateFileAsync($"transcoded.{extension.ToString().ToLower()}");
MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd);
mediaProcessor.ProcessingStarted += ProcessingStarted;
mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged;
mediaProcessor.ProcessingCompleted += ProcessingCompleted;
await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken);
}
MediaProcessor mediaProcessor = new MediaProcessor(outputFile, trimStart, trimEnd);
mediaProcessor.ProcessingStarted += ProcessingStarted;
mediaProcessor.ProcessingProgressChanged += ProcessingProgressChanged;
mediaProcessor.ProcessingCompleted += ProcessingCompleted;
cancellationToken.ThrowIfCancellationRequested();
await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken);
// Return output file
return outputFile;
}
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, Stream audioVideoStream, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioVideoStream, extension, mediaType, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IAStream audioStream, AudioFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, audioStream, extension, new TimeSpan(0), Duration, cancellationToken); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default) { throw new NotImplementedException("Twitch VOD download service doesn't support separate video and audio streams"); }
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IVStream videoStream, VideoFileExtension extension, CancellationToken cancellationToken = default) { return await DownloadAndTranscodeAsync(downloadingFolder, videoStream, extension, new TimeSpan(0), Duration, cancellationToken); }
#endregion
@@ -223,14 +211,16 @@ namespace VDownload.Core.Services.Sources.Twitch
#region LOCAL METHODS
// GET CHUNKS DATA FROM M3U8 PLAYLIST
private static async Task<List<(Uri ChunkUrl, TimeSpan ChunkDuration)>> ExtractChunksFromM3U8Async(Uri streamUrl)
private static async Task<List<(Uri ChunkUrl, TimeSpan ChunkDuration)>> ExtractChunksFromM3U8Async(Uri streamUrl, CancellationToken cancellationToken = default)
{
// Create client
WebClient client = new WebClient();
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
// Get playlist
cancellationToken.ThrowIfCancellationRequested();
string response = await client.DownloadStringTaskAsync(streamUrl);
// Create dictionary
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>();
@@ -273,11 +263,12 @@ namespace VDownload.Core.Services.Sources.Twitch
}
// DOWNLOAD CHUNK
private static async Task<byte[]> DownloadChunkAsync(Uri chunkUrl)
private static async Task<byte[]> DownloadChunkAsync(Uri chunkUrl, CancellationToken cancellationToken = default)
{
int retriesCount = 0;
while ((bool)Config.GetValue("twitch_vod_downloading_chunk_retry_after_error") && retriesCount < (int)Config.GetValue("twitch_vod_downloading_chunk_max_retries"))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (WebClient client = new WebClient())

View File

@@ -1,38 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Services
{
public class TaskId
{
// VARIABLES
#region CONSTANTS
// RANDOM
private static readonly Random Random = new Random();
private static readonly char[] CharsID = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
private static readonly int LengthID = 10;
// ID SETTINGS
private static readonly char[] IDChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
private static readonly int IDLength = 10;
#endregion
#region PROPERTIES
// USED IDS LIST
private static readonly List<string> UsedIDs = new List<string>();
// METHOD
#endregion
#region METHODS
// GET TASK ID
public static string Get()
{
string id;
do
{
id = "";
while (id.Length < LengthID)
while (id.Length < IDLength)
{
id += CharsID[Random.Next(0, CharsID.Length)];
id += IDChars[Random.Next(0, IDChars.Length)];
}
} while (UsedIDs.Contains(id));
UsedIDs.Add(id);
return id;
}
// DISPOSE TASK ID
public static void Dispose(string id)
{
UsedIDs.Remove(id);
}
#endregion
}
}

View File

@@ -127,16 +127,15 @@
<Compile Include="Enums\StreamType.cs" />
<Compile Include="Enums\VideoFileExtension.cs" />
<Compile Include="Enums\VideoSource.cs" />
<Compile Include="Enums\VideoStatus.cs" />
<Compile Include="EventArgsObjects\VideoAddEventArgs.cs" />
<Compile Include="EventArgsObjects\VideoSearchEventArgs.cs" />
<Compile Include="EventArgsObjects\PlaylistSearchEventArgs.cs" />
<Compile Include="Enums\TaskStatus.cs" />
<Compile Include="EventArgs\VideoAddEventArgs.cs" />
<Compile Include="EventArgs\VideoSearchEventArgs.cs" />
<Compile Include="EventArgs\PlaylistSearchEventArgs.cs" />
<Compile Include="Exceptions\TwitchAccessTokenNotFoundException.cs" />
<Compile Include="Exceptions\TwitchAccessTokenNotValidException.cs" />
<Compile Include="Interfaces\IAStream.cs" />
<Compile Include="Interfaces\IBaseStream.cs" />
<Compile Include="Interfaces\IPlaylistService.cs" />
<Compile Include="Interfaces\IVideoService.cs" />
<Compile Include="Interfaces\IVStream.cs" />
<Compile Include="Objects\Stream.cs" />
<Compile Include="Services\Config.cs" />
<Compile Include="Services\MediaProcessor.cs" />