1.0-dev15 (Core code cleaning)
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
namespace VDownload.Core.Enums
|
||||
{
|
||||
public enum StreamType
|
||||
{
|
||||
AudioVideo,
|
||||
OnlyAudio,
|
||||
OnlyVideo,
|
||||
}
|
||||
}
|
||||
14
VDownload.Core/Enums/TaskAddingRequestSource.cs
Normal file
14
VDownload.Core/Enums/TaskAddingRequestSource.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Core.Enums
|
||||
{
|
||||
public enum TaskAddingRequestSource
|
||||
{
|
||||
Video,
|
||||
Playlist
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Interfaces;
|
||||
using VDownload.Core.Objects;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.EventArgs
|
||||
{
|
||||
public class PlaylistAddEventArgs : System.EventArgs
|
||||
{
|
||||
public (
|
||||
IVideoService VideoService,
|
||||
MediaType MediaType,
|
||||
IBaseStream Stream,
|
||||
TimeSpan TrimStart,
|
||||
TimeSpan TrimEnd,
|
||||
string Filename,
|
||||
MediaFileExtension Extension,
|
||||
StorageFolder Location,
|
||||
double Schedule
|
||||
)[] Videos { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
public class PlaylistSearchEventArgs : System.EventArgs
|
||||
{
|
||||
public string Phrase { get; set; }
|
||||
public int Count { get; set; }
|
||||
public string Url { get; set; }
|
||||
public int VideosCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
24
VDownload.Core/EventArgs/ProgressChangedEventArgs.cs
Normal file
24
VDownload.Core/EventArgs/ProgressChangedEventArgs.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace VDownload.Core.EventArgs
|
||||
{
|
||||
public class ProgressChangedEventArgs : System.EventArgs
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public ProgressChangedEventArgs(double progress, bool isCompleted = false)
|
||||
{
|
||||
Progress = progress;
|
||||
IsCompleted = isCompleted;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public double Progress { get; set; }
|
||||
public bool IsCompleted { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
11
VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs
Normal file
11
VDownload.Core/EventArgs/TasksAddingRequestedEventArgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Structs;
|
||||
|
||||
namespace VDownload.Core.EventArgs
|
||||
{
|
||||
public class TasksAddingRequestedEventArgs : System.EventArgs
|
||||
{
|
||||
public TaskData[] TaskData { get; set; }
|
||||
public TaskAddingRequestSource RequestSource { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
{
|
||||
public class VideoSearchEventArgs : System.EventArgs
|
||||
{
|
||||
public string Phrase { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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 IBaseStream
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
Uri Url { get; }
|
||||
bool IsChunked { get; }
|
||||
StreamType StreamType { get; }
|
||||
int Height { get; }
|
||||
int FrameRate { get; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Structs;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.Interfaces
|
||||
@@ -14,13 +15,8 @@ namespace VDownload.Core.Interfaces
|
||||
// VIDEO PROPERTIES
|
||||
string ID { get; }
|
||||
Uri VideoUrl { get; }
|
||||
string Title { get; }
|
||||
string Author { get; }
|
||||
DateTime Date { get; }
|
||||
TimeSpan Duration { get; }
|
||||
long Views { get; }
|
||||
Uri Thumbnail { get; }
|
||||
IBaseStream[] BaseStreams { get; }
|
||||
Metadata Metadata { get; }
|
||||
BaseStream[] BaseStreams { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -35,7 +31,7 @@ namespace VDownload.Core.Interfaces
|
||||
Task GetStreamsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
// DOWNLOAD VIDEO
|
||||
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
|
||||
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -43,12 +39,8 @@ namespace VDownload.Core.Interfaces
|
||||
|
||||
#region EVENT HANDLERS
|
||||
|
||||
event EventHandler DownloadingStarted;
|
||||
event EventHandler<ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
event EventHandler DownloadingCompleted;
|
||||
event EventHandler ProcessingStarted;
|
||||
event EventHandler<ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
event EventHandler ProcessingCompleted;
|
||||
event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Interfaces;
|
||||
|
||||
namespace VDownload.Core.Objects
|
||||
{
|
||||
public class Stream : IBaseStream
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public Stream(Uri url, bool isChunked, StreamType streamType)
|
||||
{
|
||||
Url = url;
|
||||
IsChunked = isChunked;
|
||||
StreamType = streamType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public Uri Url { get; private set; }
|
||||
public bool IsChunked { get; private set; }
|
||||
public StreamType StreamType { get; private set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public int FrameRate { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
public int AudioBitrate { get; set; }
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using VDownload.Core.Enums;
|
||||
using Windows.Media.Editing;
|
||||
using Windows.Media.Transcoding;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.Services
|
||||
@@ -21,7 +22,7 @@ namespace VDownload.Core.Services
|
||||
{ "twitch_vod_downloading_chunk_max_retries", 10 },
|
||||
{ "twitch_vod_downloading_chunk_retries_delay", 5000 },
|
||||
{ "media_transcoding_use_hardware_acceleration", true },
|
||||
{ "media_transcoding_use_mrfcrf444_algorithm", true },
|
||||
{ "media_transcoding_algorithm", (int)MediaVideoProcessingAlgorithm.MrfCrf444 },
|
||||
{ "media_editing_algorithm", (int)MediaTrimmingPreference.Fast },
|
||||
{ "default_max_playlist_videos", 0 },
|
||||
{ "default_media_type", (int)MediaType.AudioVideo },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,66 +14,50 @@ namespace VDownload.Core.Services
|
||||
{
|
||||
public class MediaProcessor
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public MediaProcessor(StorageFile outputFile, TimeSpan trimStart, TimeSpan trimEnd)
|
||||
{
|
||||
OutputFile = outputFile;
|
||||
TrimStart = trimStart;
|
||||
TrimEnd = trimEnd;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public StorageFile OutputFile { get; private set; }
|
||||
public TimeSpan TrimStart { get; private set; }
|
||||
public TimeSpan TrimEnd { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region STANDARD METHODS
|
||||
|
||||
// SINGLE AUDIO & VIDEO FILE PROCESSING
|
||||
public async Task Run(StorageFile audioVideoInputFile, MediaFileExtension extension, MediaType mediaType, CancellationToken cancellationToken = default)
|
||||
public async Task Run(StorageFile mediaFile, MediaFileExtension extension, MediaType mediaType, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Invoke ProcessingStarted event
|
||||
ProcessingStarted?.Invoke(this, System.EventArgs.Empty);
|
||||
// Invoke event at start
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
|
||||
|
||||
// Init transcoder
|
||||
MediaTranscoder mediaTranscoder = new MediaTranscoder
|
||||
{
|
||||
HardwareAccelerationEnabled = (bool)Config.GetValue("media_transcoding_use_hardware_acceleration"),
|
||||
VideoProcessingAlgorithm = (bool)Config.GetValue("media_transcoding_use_mrfcrf444_algorithm") ? MediaVideoProcessingAlgorithm.MrfCrf444 : MediaVideoProcessingAlgorithm.Default,
|
||||
TrimStartTime = TrimStart,
|
||||
TrimStopTime = TrimEnd,
|
||||
VideoProcessingAlgorithm = (MediaVideoProcessingAlgorithm)Config.GetValue("media_transcoding_algorithm"),
|
||||
};
|
||||
if (trimStart != null) mediaTranscoder.TrimStartTime = (TimeSpan)trimStart;
|
||||
if (trimEnd != null) mediaTranscoder.TrimStopTime = (TimeSpan)trimEnd;
|
||||
|
||||
// Start transcoding operation
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (IRandomAccessStream outputFileOpened = await OutputFile.OpenAsync(FileAccessMode.ReadWrite))
|
||||
using (IRandomAccessStream openedOutputFile = await outputFile.OpenAsync(FileAccessMode.ReadWrite))
|
||||
{
|
||||
PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await audioVideoInputFile.OpenAsync(FileAccessMode.Read), outputFileOpened, await GetMediaEncodingProfile(audioVideoInputFile, extension, mediaType));
|
||||
// Prepare transcode task
|
||||
PrepareTranscodeResult transcodingPreparated = await mediaTranscoder.PrepareStreamTranscodeAsync(await mediaFile.OpenAsync(FileAccessMode.Read), openedOutputFile, await GetMediaEncodingProfile(mediaFile, extension, mediaType));
|
||||
|
||||
// Start transcoding
|
||||
IAsyncActionWithProgress<double> transcodingTask = transcodingPreparated.TranscodeAsync();
|
||||
await transcodingTask.AsTask(cancellationToken, new Progress<double>((percent) => { ProcessingProgressChanged(this, new ProgressChangedEventArgs((int)Math.Round(percent), null)); }));
|
||||
await outputFileOpened.FlushAsync();
|
||||
await transcodingTask.AsTask(cancellationToken, new Progress<double>((percent) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(percent)); }));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Finalizing
|
||||
await openedOutputFile.FlushAsync();
|
||||
transcodingTask.Close();
|
||||
}
|
||||
|
||||
// Invoke ProcessingCompleted event
|
||||
ProcessingCompleted?.Invoke(this, System.EventArgs.Empty);
|
||||
// Invoke event at end
|
||||
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
|
||||
}
|
||||
|
||||
// SEPARATE AUDIO & VIDEO FILES PROCESSING
|
||||
public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, CancellationToken cancellationToken = default)
|
||||
public async Task Run(StorageFile audioFile, StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Invoke ProcessingStarted event
|
||||
ProcessingStarted?.Invoke(this, System.EventArgs.Empty);
|
||||
// Invoke event at start
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
|
||||
|
||||
// Init editor
|
||||
MediaComposition mediaEditor = new MediaComposition();
|
||||
@@ -86,28 +69,36 @@ namespace VDownload.Core.Services
|
||||
await Task.WhenAll(getVideoFileTask, getAudioFileTask);
|
||||
|
||||
MediaClip videoElement = getVideoFileTask.Result;
|
||||
videoElement.TrimTimeFromStart = TrimStart;
|
||||
videoElement.TrimTimeFromEnd = TrimEnd;
|
||||
if (trimStart != null) videoElement.TrimTimeFromStart = (TimeSpan)trimStart;
|
||||
if (trimEnd != null) videoElement.TrimTimeFromEnd = (TimeSpan)trimEnd;
|
||||
BackgroundAudioTrack audioElement = getAudioFileTask.Result;
|
||||
audioElement.TrimTimeFromStart = TrimStart;
|
||||
audioElement.TrimTimeFromEnd = TrimEnd;
|
||||
if (trimStart != null) audioElement.TrimTimeFromStart = (TimeSpan)trimStart;
|
||||
if (trimEnd != null) audioElement.TrimTimeFromEnd = (TimeSpan)trimEnd;
|
||||
|
||||
mediaEditor.Clips.Add(getVideoFileTask.Result);
|
||||
mediaEditor.BackgroundAudioTracks.Add(getAudioFileTask.Result);
|
||||
mediaEditor.Clips.Add(videoElement);
|
||||
mediaEditor.BackgroundAudioTracks.Add(audioElement);
|
||||
|
||||
// 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)); };
|
||||
var renderOperation = mediaEditor.RenderToFileAsync(outputFile, (MediaTrimmingPreference)Config.GetValue("media_editing_algorithm"), await GetMediaEncodingProfile(videoFile, audioFile, (MediaFileExtension)extension, MediaType.AudioVideo));
|
||||
renderOperation.Progress += (info, progress) => { ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(progress)); };
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await renderOperation.AsTask(cancellationToken);
|
||||
|
||||
// Invoke ProcessingCompleted event
|
||||
ProcessingCompleted?.Invoke(this, System.EventArgs.Empty);
|
||||
// Invoke event at end
|
||||
ProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
|
||||
}
|
||||
|
||||
// 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); }
|
||||
// AUDIO FILE PROCESSING
|
||||
public async Task Run(StorageFile audioFile, AudioFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Run(audioFile, (MediaFileExtension)extension, MediaType.OnlyAudio, outputFile, trimStart, trimEnd, cancellationToken);
|
||||
}
|
||||
|
||||
// VIDEO FILE PROCESSING
|
||||
public async Task Run(StorageFile videoFile, VideoFileExtension extension, StorageFile outputFile, TimeSpan? trimStart = null, TimeSpan? trimEnd = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Run(videoFile, (MediaFileExtension)extension, MediaType.OnlyVideo, outputFile, trimStart, trimEnd, cancellationToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -116,7 +107,7 @@ namespace VDownload.Core.Services
|
||||
#region LOCAL METHODS
|
||||
|
||||
// GET ENCODING PROFILE
|
||||
public static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType)
|
||||
private static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile videoFile, StorageFile audioFile, MediaFileExtension extension, MediaType mediaType)
|
||||
{
|
||||
// Create profile object
|
||||
MediaEncodingProfile profile;
|
||||
@@ -164,7 +155,10 @@ namespace VDownload.Core.Services
|
||||
// Return profile
|
||||
return profile;
|
||||
}
|
||||
public static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType) { return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType); }
|
||||
private static async Task<MediaEncodingProfile> GetMediaEncodingProfile(StorageFile audioVideoFile, MediaFileExtension extension, MediaType mediaType)
|
||||
{
|
||||
return await GetMediaEncodingProfile(audioVideoFile, audioVideoFile, extension, mediaType);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -172,9 +166,7 @@ namespace VDownload.Core.Services
|
||||
|
||||
#region EVENT HANDLERS
|
||||
|
||||
public event EventHandler ProcessingStarted;
|
||||
public event EventHandler<ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
public event EventHandler ProcessingCompleted;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProgressChanged;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Core.Exceptions;
|
||||
using VDownload.Core.Interfaces;
|
||||
using VDownload.Core.Services.Sources.Twitch.Helpers;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch
|
||||
{
|
||||
@@ -41,25 +42,15 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
// GET CHANNEL METADATA
|
||||
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 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();
|
||||
|
||||
// Create client
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
client.Headers.Add("Client-Id", Auth.ClientID);
|
||||
|
||||
// Get response
|
||||
client.QueryString.Add("login", ID);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/users"))["data"][0];
|
||||
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"][0];
|
||||
}
|
||||
|
||||
// Create unified playlist url
|
||||
PlaylistUrl = new Uri($"https://twitch.tv/{ID}");
|
||||
@@ -72,12 +63,9 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
// GET CHANNEL VIDEOS
|
||||
public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get access token
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
string accessToken = await Auth.ReadAccessTokenAsync();
|
||||
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
|
||||
|
||||
// Set pagination
|
||||
// Set page id
|
||||
string pagination = "";
|
||||
|
||||
// Set array of videos
|
||||
@@ -92,30 +80,24 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
List<Task> getStreamsTasks = new List<Task>();
|
||||
do
|
||||
{
|
||||
// Check access token
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
|
||||
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
|
||||
|
||||
// Create client
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
client.Headers.Add("Client-Id", Auth.ClientID);
|
||||
|
||||
// Set number of videos to get in this iteration
|
||||
count = numberOfVideos < 100 && !getAll ? numberOfVideos : 100;
|
||||
|
||||
// Get response
|
||||
client.QueryString.Add("user_id", ID);
|
||||
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"));
|
||||
JToken response = null;
|
||||
using (WebClient client = await Client.Helix())
|
||||
{
|
||||
client.QueryString.Add("user_id", ID);
|
||||
client.QueryString.Add("first", count.ToString());
|
||||
client.QueryString.Add("after", pagination);
|
||||
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos"));
|
||||
}
|
||||
|
||||
// Set page id
|
||||
pagination = (string)response["pagination"]["cursor"];
|
||||
videosData = response["data"].ToArray();
|
||||
|
||||
// Set videos data
|
||||
videosData = response["data"].ToArray();
|
||||
foreach (JToken videoData in videosData)
|
||||
{
|
||||
Vod video = new Vod((string)videoData["id"]);
|
||||
@@ -131,7 +113,7 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
// Wait for all getStreams tasks
|
||||
await Task.WhenAll(getStreamsTasks);
|
||||
|
||||
// Set Videos parameter
|
||||
// Set videos
|
||||
Videos = videos.ToArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,21 +12,14 @@ using System.Web;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Exceptions;
|
||||
using VDownload.Core.Interfaces;
|
||||
using VDownload.Core.Objects;
|
||||
using VDownload.Core.Services.Sources.Twitch.Helpers;
|
||||
using VDownload.Core.Structs;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch
|
||||
{
|
||||
public class Clip : IVideoService
|
||||
{
|
||||
#region CONSTANTS
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public Clip(string id)
|
||||
@@ -42,13 +35,8 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
|
||||
public string ID { get; private set; }
|
||||
public Uri VideoUrl { 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 IBaseStream[] BaseStreams { get; private set; }
|
||||
public Metadata Metadata { get; private set; }
|
||||
public BaseStream[] BaseStreams { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -59,92 +47,83 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
// GET CLIP METADATA
|
||||
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 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();
|
||||
|
||||
// Create client
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
client.Headers.Add("Client-Id", Auth.ClientID);
|
||||
|
||||
// Get response
|
||||
client.QueryString.Add("id", ID);
|
||||
JToken response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/clips")).GetValue("data")[0];
|
||||
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")[0];
|
||||
}
|
||||
|
||||
// Create unified video url
|
||||
VideoUrl = new Uri($"https://clips.twitch.tv/{ID}");
|
||||
|
||||
// Set parameters
|
||||
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"]);
|
||||
// Set metadata
|
||||
Metadata = new Metadata()
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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();
|
||||
|
||||
|
||||
// Get response
|
||||
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();
|
||||
}
|
||||
|
||||
// Init streams list
|
||||
List<Stream> streams = new List<Stream>();
|
||||
List<BaseStream> streams = new List<BaseStream>();
|
||||
|
||||
// Parse response
|
||||
foreach (JToken streamData in response)
|
||||
{
|
||||
// Get info
|
||||
Uri url = new Uri((string)streamData["sourceURL"]);
|
||||
int height = int.Parse((string)streamData["quality"]);
|
||||
int frameRate = (int)streamData["frameRate"];
|
||||
|
||||
// Create stream
|
||||
Stream stream = new Stream(url, false, StreamType.AudioVideo)
|
||||
BaseStream stream = new BaseStream()
|
||||
{
|
||||
Height = height,
|
||||
FrameRate = frameRate
|
||||
Url = new Uri((string)streamData["sourceURL"]),
|
||||
Height = int.Parse((string)streamData["quality"]),
|
||||
FrameRate = (int)streamData["frameRate"],
|
||||
};
|
||||
|
||||
// Add stream
|
||||
streams.Add(stream);
|
||||
}
|
||||
|
||||
// Set Streams parameter
|
||||
// Set streams
|
||||
BaseStreams = streams.ToArray();
|
||||
}
|
||||
|
||||
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
|
||||
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Invoke DownloadingStarted event
|
||||
DownloadingStarted?.Invoke(this, System.EventArgs.Empty);
|
||||
|
||||
// Create client
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
|
||||
|
||||
// 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"];
|
||||
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 (client = new WebClient())
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
client.DownloadProgressChanged += (s, a) => { DownloadingProgressChanged(this, new ProgressChangedEventArgs(a.ProgressPercentage, null)); };
|
||||
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();
|
||||
@@ -153,20 +132,24 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
await client.DownloadFileTaskAsync(baseStream.Url, rawFile.Path);
|
||||
}
|
||||
}
|
||||
DownloadingCompleted?.Invoke(this, System.EventArgs.Empty);
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
|
||||
|
||||
// Processing
|
||||
StorageFile outputFile = rawFile;
|
||||
if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart > new TimeSpan(0) || trimEnd < Duration)
|
||||
if (extension != MediaFileExtension.MP4 || mediaType != MediaType.AudioVideo || trimStart != null || trimEnd != null)
|
||||
{
|
||||
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);
|
||||
|
||||
MediaProcessor mediaProcessor = new MediaProcessor();
|
||||
mediaProcessor.ProgressChanged += ProcessingProgressChanged;
|
||||
|
||||
Task mediaProcessorTask;
|
||||
if (trimStart == TimeSpan.Zero && trimEnd == Metadata.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 == Metadata.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;
|
||||
}
|
||||
|
||||
// Return output file
|
||||
@@ -179,12 +162,8 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
|
||||
#region EVENT HANDLERS
|
||||
|
||||
public event EventHandler DownloadingStarted;
|
||||
public event EventHandler<ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler DownloadingCompleted;
|
||||
public event EventHandler ProcessingStarted;
|
||||
public event EventHandler<ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
public event EventHandler ProcessingCompleted;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
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;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch
|
||||
namespace VDownload.Core.Services.Sources.Twitch.Helpers
|
||||
{
|
||||
public class Auth
|
||||
{
|
||||
38
VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs
Normal file
38
VDownload.Core/Services/Sources/Twitch/Helpers/Client.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Core.Exceptions;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch.Helpers
|
||||
{
|
||||
internal class Client
|
||||
{
|
||||
internal static async Task<WebClient> Helix()
|
||||
{
|
||||
// Get access token
|
||||
string accessToken = await Auth.ReadAccessTokenAsync();
|
||||
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
|
||||
|
||||
// Check access token
|
||||
var twitchAccessTokenValidation = await Auth.ValidateAccessTokenAsync(accessToken);
|
||||
if (!twitchAccessTokenValidation.IsValid) throw new TwitchAccessTokenNotValidException();
|
||||
|
||||
// Create client
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
client.Headers.Add("Client-Id", Auth.ClientID);
|
||||
|
||||
// Return client
|
||||
return client;
|
||||
}
|
||||
|
||||
internal static WebClient GQL()
|
||||
{
|
||||
// Create client
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Client-Id", Auth.GQLApiClientID);
|
||||
|
||||
// Return client
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -9,27 +8,15 @@ 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.Objects;
|
||||
using VDownload.Core.Services.Sources.Twitch.Helpers;
|
||||
using VDownload.Core.Structs;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.Services.Sources.Twitch
|
||||
{
|
||||
public class Vod : IVideoService
|
||||
{
|
||||
#region CONSTANTS
|
||||
|
||||
// STREAMS RESPONSE REGULAR EXPRESSIONS
|
||||
private static readonly Regex L2Regex = new Regex(@"^#EXT-X-STREAM-INF:BANDWIDTH=\d+,CODECS=""(?<video_codec>\S+),(?<audio_codec>\S+)"",RESOLUTION=(?<width>\d+)x(?<height>\d+),VIDEO=""\w+""(,FRAME-RATE=(?<frame_rate>\d+.\d+))?");
|
||||
|
||||
// CHUNK RESPONSE REGULAR EXPRESSION
|
||||
private static readonly Regex ChunkRegex = new Regex(@"#EXTINF:(?<duration>\d+.\d+),\n(?<filename>\S+.ts)");
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public Vod(string id)
|
||||
@@ -45,13 +32,8 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
|
||||
public string ID { get; private set; }
|
||||
public Uri VideoUrl { 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 IBaseStream[] BaseStreams { get; private set; }
|
||||
public Metadata Metadata { get; private set; }
|
||||
public BaseStream[] BaseStreams { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -62,25 +44,16 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
// GET VOD METADATA
|
||||
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 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();
|
||||
|
||||
// Create client
|
||||
WebClient client = new WebClient();
|
||||
client.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||
client.Headers.Add("Client-Id", Auth.ClientID);
|
||||
|
||||
// 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];
|
||||
JToken response = null;
|
||||
using (WebClient client = await Client.Helix())
|
||||
{
|
||||
client.QueryString.Add("id", ID);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
response = JObject.Parse(await client.DownloadStringTaskAsync("https://api.twitch.tv/helix/videos")).GetValue("data")[0];
|
||||
}
|
||||
|
||||
// Set parameters
|
||||
GetMetadataAsync(response);
|
||||
@@ -90,83 +63,86 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
// Create unified video url
|
||||
VideoUrl = new Uri($"https://www.twitch.tv/videos/{ID}");
|
||||
|
||||
// Set parameters
|
||||
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"));
|
||||
// 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")),
|
||||
};
|
||||
}
|
||||
|
||||
// GET VOD STREAMS
|
||||
public async Task GetStreamsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 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");
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Init streams list
|
||||
List<Stream> streams = new List<Stream>();
|
||||
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+))?");
|
||||
|
||||
// Parse response
|
||||
for (int i = 2; i < response.Length; i += 3)
|
||||
{
|
||||
// Parse line 2
|
||||
Match line2 = L2Regex.Match(response[i + 1]);
|
||||
|
||||
// Get info
|
||||
Uri url = new Uri(response[i + 2]);
|
||||
int width = int.Parse(line2.Groups["width"].Value);
|
||||
int height = int.Parse(line2.Groups["height"].Value);
|
||||
int frameRate = line2.Groups["frame_rate"].Value != string.Empty ? (int)Math.Round(double.Parse(line2.Groups["frame_rate"].Value)) : 0;
|
||||
string videoCodec = line2.Groups["video_codec"].Value;
|
||||
string audioCodec = line2.Groups["audio_codec"].Value;
|
||||
Match line2 = streamDataL2Regex.Match(response[i + 1]);
|
||||
|
||||
// Create stream
|
||||
Stream stream = new Stream(url, true, StreamType.AudioVideo)
|
||||
BaseStream stream = new BaseStream()
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
FrameRate = frameRate,
|
||||
VideoCodec = videoCodec,
|
||||
AudioCodec = audioCodec,
|
||||
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,
|
||||
};
|
||||
|
||||
// Add stream
|
||||
streams.Add(stream);
|
||||
}
|
||||
|
||||
// Set Streams parameter
|
||||
// Set streams
|
||||
BaseStreams = streams.ToArray();
|
||||
}
|
||||
|
||||
// DOWNLOAD AND TRANSCODE VOD
|
||||
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, IBaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
|
||||
public async Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Invoke DownloadingStarted event
|
||||
DownloadingStarted?.Invoke(this, System.EventArgs.Empty);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(0));
|
||||
|
||||
// Get video chunks
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(baseStream.Url, cancellationToken);
|
||||
|
||||
// Changeable duration
|
||||
TimeSpan duration = Metadata.Duration;
|
||||
|
||||
// Passive trim
|
||||
if ((bool)Config.GetValue("twitch_vod_passive_trim")) (trimStart, trimEnd) = PassiveVideoTrim(chunksList, trimStart, trimEnd, Duration);
|
||||
if ((bool)Config.GetValue("twitch_vod_passive_trim") && trimStart != TimeSpan.Zero && trimEnd != duration) (trimStart, trimEnd, duration) = PassiveVideoTrim(chunksList, trimStart, trimEnd, Metadata.Duration);
|
||||
|
||||
// Download
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
|
||||
|
||||
float chunksDownloaded = 0;
|
||||
double chunksDownloaded = 0;
|
||||
|
||||
Task<byte[]> downloadTask;
|
||||
Task writeTask;
|
||||
@@ -180,26 +156,25 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
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));
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(++chunksDownloaded * 100 / chunksList.Count));
|
||||
}
|
||||
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);
|
||||
|
||||
DownloadingProgressChanged(this, new EventArgs.ProgressChangedEventArgs(100, true));
|
||||
|
||||
// Processing
|
||||
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;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await mediaProcessor.Run(rawFile, extension, mediaType, cancellationToken);
|
||||
|
||||
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;
|
||||
|
||||
// Return output file
|
||||
return outputFile;
|
||||
@@ -214,21 +189,28 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
// GET CHUNKS DATA FROM M3U8 PLAYLIST
|
||||
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);
|
||||
|
||||
// Get response
|
||||
string response = null;
|
||||
using (WebClient client = Client.GQL())
|
||||
{
|
||||
response = await client.DownloadStringTaskAsync(streamUrl);
|
||||
}
|
||||
|
||||
// Create dictionary
|
||||
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunks = new List<(Uri ChunkUrl, TimeSpan ChunkDuration)>();
|
||||
|
||||
// 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), "");
|
||||
|
||||
// Pack data into dictionary
|
||||
foreach (Match chunk in ChunkRegex.Matches(response))
|
||||
foreach (Match chunk in chunkDataRegex.Matches(response))
|
||||
{
|
||||
Uri chunkUrl = new Uri($"{streamUrl.AbsoluteUri.Replace(System.IO.Path.GetFileName(streamUrl.AbsoluteUri), "")}{chunk.Groups["filename"].Value}");
|
||||
Uri chunkUrl = new Uri($"{chunkLocationPath}{chunk.Groups["filename"].Value}");
|
||||
TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(chunk.Groups["duration"].Value));
|
||||
chunks.Add((chunkUrl, chunkDuration));
|
||||
}
|
||||
@@ -238,7 +220,7 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
}
|
||||
|
||||
// PASSIVE TRIM
|
||||
private static (TimeSpan TrimStart, TimeSpan TrimEnd) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TimeSpan trimStart, TimeSpan trimEnd, TimeSpan duration)
|
||||
private static (TimeSpan NewTrimStart, TimeSpan NewTrimEnd, TimeSpan NewDuration) PassiveVideoTrim(List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList, TimeSpan trimStart, TimeSpan trimEnd, TimeSpan duration)
|
||||
{
|
||||
// Copy duration
|
||||
TimeSpan newDuration = duration;
|
||||
@@ -260,7 +242,7 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
}
|
||||
|
||||
// Return data
|
||||
return (trimStart, trimEnd);
|
||||
return (trimStart, trimEnd, newDuration);
|
||||
}
|
||||
|
||||
// DOWNLOAD CHUNK
|
||||
@@ -322,12 +304,8 @@ namespace VDownload.Core.Services.Sources.Twitch
|
||||
|
||||
#region EVENT HANDLERS
|
||||
|
||||
public event EventHandler DownloadingStarted;
|
||||
public event EventHandler<ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler DownloadingCompleted;
|
||||
public event EventHandler ProcessingStarted;
|
||||
public event EventHandler<ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
public event EventHandler ProcessingCompleted;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
|
||||
public event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -7,21 +7,12 @@ namespace VDownload.Core.Services
|
||||
{
|
||||
#region CONSTANTS
|
||||
|
||||
// RANDOM
|
||||
private static readonly Random Random = new Random();
|
||||
|
||||
// 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>();
|
||||
// IDS LIST
|
||||
private static readonly List<string> IDList = new List<string>();
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -38,17 +29,17 @@ namespace VDownload.Core.Services
|
||||
id = "";
|
||||
while (id.Length < IDLength)
|
||||
{
|
||||
id += IDChars[Random.Next(0, IDChars.Length)];
|
||||
id += IDChars[new Random().Next(0, IDChars.Length)];
|
||||
}
|
||||
} while (UsedIDs.Contains(id));
|
||||
UsedIDs.Add(id);
|
||||
} while (IDList.Contains(id));
|
||||
IDList.Add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
// DISPOSE TASK ID
|
||||
public static void Dispose(string id)
|
||||
{
|
||||
UsedIDs.Remove(id);
|
||||
IDList.Remove(id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
61
VDownload.Core/Services/TimeSpanCustomFormat.cs
Normal file
61
VDownload.Core/Services/TimeSpanCustomFormat.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Core.Services
|
||||
{
|
||||
public class TimeSpanCustomFormat
|
||||
{
|
||||
// (TH:)MM:SS
|
||||
public static string ToOptTHBaseMMSS(TimeSpan timeSpan, params TimeSpan[] formatBase)
|
||||
{
|
||||
string formattedTimeSpan = string.Empty;
|
||||
|
||||
int maxTHLength = 0;
|
||||
if (Math.Floor(timeSpan.TotalHours) > 0)
|
||||
{
|
||||
maxTHLength = Math.Floor(timeSpan.TotalHours).ToString().Length;
|
||||
foreach (TimeSpan format in formatBase)
|
||||
{
|
||||
int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0;
|
||||
if (THLength > maxTHLength) maxTHLength = THLength;
|
||||
}
|
||||
formattedTimeSpan += $"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:";
|
||||
}
|
||||
formattedTimeSpan += maxTHLength == 0 ? $"{timeSpan.Minutes}:" : $"{timeSpan.Minutes:00}:";
|
||||
formattedTimeSpan += $"{timeSpan.Seconds:00}";
|
||||
|
||||
return formattedTimeSpan;
|
||||
}
|
||||
|
||||
// ((TH:)MM:)SS
|
||||
public static string ToOptTHMMBaseSS(TimeSpan timeSpan, params TimeSpan[] formatBase)
|
||||
{
|
||||
string formattedTimeSpan = string.Empty;
|
||||
|
||||
int maxTHLength = 0;
|
||||
if (Math.Floor(timeSpan.TotalHours) > 0)
|
||||
{
|
||||
maxTHLength = Math.Floor(timeSpan.TotalHours).ToString().Length;
|
||||
foreach (TimeSpan format in formatBase)
|
||||
{
|
||||
int THLength = Math.Floor(format.TotalHours) > 0 ? Math.Floor(timeSpan.TotalHours).ToString().Length : 0;
|
||||
if (THLength > maxTHLength) maxTHLength = THLength;
|
||||
}
|
||||
formattedTimeSpan += $"{((int)Math.Floor(timeSpan.TotalHours)).ToString($"D{maxTHLength}")}:";
|
||||
}
|
||||
bool MM = false;
|
||||
if (Math.Floor(timeSpan.TotalMinutes) > 0)
|
||||
{
|
||||
formattedTimeSpan += maxTHLength > 0 ? $"{timeSpan.Minutes:00}:" : $"{timeSpan.Minutes}:";
|
||||
MM = true;
|
||||
}
|
||||
formattedTimeSpan += MM ? $"{timeSpan.Seconds:00}:" : $"{timeSpan.Seconds}:";
|
||||
|
||||
return formattedTimeSpan;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
VDownload.Core/Structs/BaseStream.cs
Normal file
15
VDownload.Core/Structs/BaseStream.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Core.Structs
|
||||
{
|
||||
public struct BaseStream
|
||||
{
|
||||
public Uri Url { get; set; }
|
||||
public int Height { get; set; }
|
||||
public int FrameRate { get; set; }
|
||||
}
|
||||
}
|
||||
18
VDownload.Core/Structs/Metadata.cs
Normal file
18
VDownload.Core/Structs/Metadata.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Core.Structs
|
||||
{
|
||||
public struct Metadata
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Author { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public long Views { get; set; }
|
||||
public Uri Thumbnail { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
using System;
|
||||
using VDownload.Core.Enums;
|
||||
using VDownload.Core.Interfaces;
|
||||
using VDownload.Core.Objects;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace VDownload.Core.EventArgs
|
||||
namespace VDownload.Core.Structs
|
||||
{
|
||||
public class VideoAddEventArgs : System.EventArgs
|
||||
public struct TaskData
|
||||
{
|
||||
public IVideoService VideoService { get; set; }
|
||||
public MediaType MediaType { get; set; }
|
||||
public IBaseStream Stream { get; set; }
|
||||
public BaseStream Stream { get; set; }
|
||||
public TimeSpan TrimStart { get; set; }
|
||||
public TimeSpan TrimEnd { get; set; }
|
||||
public string Filename { get; set; }
|
||||
@@ -124,24 +124,27 @@
|
||||
<Compile Include="Enums\MediaFileExtension.cs" />
|
||||
<Compile Include="Enums\MediaType.cs" />
|
||||
<Compile Include="Enums\PlaylistSource.cs" />
|
||||
<Compile Include="Enums\StreamType.cs" />
|
||||
<Compile Include="Enums\TaskAddingRequestSource.cs" />
|
||||
<Compile Include="Enums\VideoFileExtension.cs" />
|
||||
<Compile Include="Enums\VideoSource.cs" />
|
||||
<Compile Include="Enums\TaskStatus.cs" />
|
||||
<Compile Include="EventArgs\PlaylistAddEventArgs.cs" />
|
||||
<Compile Include="EventArgs\VideoAddEventArgs.cs" />
|
||||
<Compile Include="EventArgs\ProgressChangedEventArgs.cs" />
|
||||
<Compile Include="EventArgs\TasksAddingRequestedEventArgs.cs" />
|
||||
<Compile Include="EventArgs\VideoSearchEventArgs.cs" />
|
||||
<Compile Include="EventArgs\PlaylistSearchEventArgs.cs" />
|
||||
<Compile Include="Exceptions\TwitchAccessTokenNotFoundException.cs" />
|
||||
<Compile Include="Exceptions\TwitchAccessTokenNotValidException.cs" />
|
||||
<Compile Include="Interfaces\IBaseStream.cs" />
|
||||
<Compile Include="Interfaces\IPlaylistService.cs" />
|
||||
<Compile Include="Interfaces\IVideoService.cs" />
|
||||
<Compile Include="Objects\Stream.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Helpers\Client.cs" />
|
||||
<Compile Include="Services\TimeSpanCustomFormat.cs" />
|
||||
<Compile Include="Structs\BaseStream.cs" />
|
||||
<Compile Include="Structs\Metadata.cs" />
|
||||
<Compile Include="Structs\TaskData.cs" />
|
||||
<Compile Include="Services\Config.cs" />
|
||||
<Compile Include="Services\MediaProcessor.cs" />
|
||||
<Compile Include="Services\Source.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Auth.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Helpers\Auth.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Channel.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Clip.cs" />
|
||||
<Compile Include="Services\Sources\Twitch\Vod.cs" />
|
||||
|
||||
Reference in New Issue
Block a user