1.0-dev7 (Video adding panel added)

This commit is contained in:
2022-02-26 14:32:34 +01:00
Unverified
parent 96a7953500
commit f571a42995
95 changed files with 1436 additions and 227 deletions

View File

@@ -2,11 +2,11 @@
{
public enum AudioFileExtension
{
MP3 = 4,
FLAC = 5,
WAV = 6,
M4A = 7,
ALAC = 8,
WMA = 9,
MP3 = 3,
FLAC = 4,
WAV = 5,
M4A = 6,
ALAC = 7,
WMA = 8,
}
}

View File

@@ -0,0 +1,8 @@
namespace VDownload.Core.Enums
{
public enum DefaultLocationType
{
Last,
Selected
}
}

View File

@@ -2,8 +2,8 @@
{
public enum MediaType
{
AudioVideo,
OnlyAudio,
OnlyVideo,
AudioVideo = 0,
OnlyAudio = 1,
OnlyVideo = 2,
}
}

View File

@@ -0,0 +1,8 @@
namespace VDownload.Core.Enums
{
public enum PlaylistSource
{
TwitchChannel,
Null
}
}

View File

@@ -2,8 +2,8 @@
{
public enum VideoFileExtension
{
MP4 = 1,
WMV = 2,
HEVC = 3,
MP4 = 0,
WMV = 1,
HEVC = 2,
}
}

View File

@@ -0,0 +1,9 @@
namespace VDownload.Core.Enums
{
public enum VideoSource
{
TwitchVod,
TwitchClip,
Null
}
}

View File

@@ -0,0 +1,20 @@
using System;
using VDownload.Core.Enums;
using VDownload.Core.Interfaces;
using VDownload.Core.Objects;
using Windows.Storage;
namespace VDownload.Core.EventArgsObjects
{
public class VideoAddEventArgs : EventArgs
{
public IVideoService VideoService { get; set; }
public MediaType MediaType { get; set; }
public Stream Stream { get; set; }
public TimeSpan TrimStart { get; set; }
public TimeSpan TrimEnd { get; set; }
public string Filename { get; set; }
public MediaFileExtension Extension { get; set; }
public StorageFolder Location { get; set; }
}
}

View File

@@ -1,8 +1,9 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
namespace VDownload.Core.Interfaces
{
internal interface IPlaylistService
public interface IPlaylistService
{
#region PROPERTIES
@@ -15,9 +16,9 @@ namespace VDownload.Core.Interfaces
#region METHODS
Task GetMetadataAsync();
Task GetMetadataAsync(CancellationToken cancellationToken = default);
Task GetVideosAsync(int numberOfVideos);
Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default);
#endregion
}

View File

@@ -12,13 +12,16 @@ namespace VDownload.Core.Interfaces
{
#region PROPERTIES
// 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; }
Stream[] Streams { get; }
#endregion
@@ -27,10 +30,10 @@ namespace VDownload.Core.Interfaces
#region METHODS
// GET VIDEO METADATA
Task GetMetadataAsync();
Task GetMetadataAsync(CancellationToken cancellationToken = default);
// GET VIDEO STREAMS
Task GetStreamsAsync();
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);

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using VDownload.Core.Enums;
using Windows.Media.Editing;
using Windows.Storage;
@@ -24,6 +25,11 @@ namespace VDownload.Core.Services
{ "media_transcoding_use_mrfcrf444_algorithm", true },
{ "media_editing_algorithm", (int)MediaTrimmingPreference.Fast },
{ "default_max_playlist_videos", 0 },
{ "default_media_type", (int)MediaType.AudioVideo },
{ "default_filename", "[<date_pub:yyyy.MM.dd>] <title>" },
{ "default_video_extension", (int)VideoFileExtension.MP4 },
{ "default_audio_extension", (int)AudioFileExtension.MP3 },
{ "default_location_type", (int)DefaultLocationType.Last },
};
#endregion

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using VDownload.Core.Enums;
namespace VDownload.Core.Services
{
public class Source
{
#region CONSTANTS
private static readonly (Regex Regex, VideoSource Type)[] VideoSources = new (Regex Regex, VideoSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/videos/(?<id>\d+)"), VideoSource.TwitchVod),
(new Regex(@"^https://www.twitch.tv/\S+/clip/(?<id>[^?]+)"), VideoSource.TwitchClip),
(new Regex(@"^https://clips.twitch.tv/(?<id>[^?]+)"), VideoSource.TwitchClip),
};
private static readonly (Regex Regex, PlaylistSource Type)[] PlaylistSources = new (Regex Regex, PlaylistSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/(?<id>[^?]+)"), PlaylistSource.TwitchChannel),
};
#endregion
#region METHODS
public static (VideoSource Type, string ID) GetVideoSource(string url)
{
foreach ((Regex Regex, VideoSource Type) Source in VideoSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success) return (Source.Type, sourceMatch.Groups["id"].Value);
}
return (VideoSource.Null, null);
}
public static (PlaylistSource Type, string ID) GetPlaylistSource(string url)
{
foreach ((Regex Regex, PlaylistSource Type) Source in PlaylistSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success) return (Source.Type, sourceMatch.Groups["id"].Value);
}
return (PlaylistSource.Null, null);
}
#endregion
}
}

View File

@@ -100,9 +100,15 @@ namespace VDownload.Core.Services.Sources.Twitch
return (true, login, expirationDate);
}
catch (WebException)
catch (WebException wex)
{
return (false, null, null);
if (wex.Response != null)
{
JObject wexInfo = JObject.Parse(new StreamReader(wex.Response.GetResponseStream()).ReadToEnd());
if ((int)wexInfo["status"] == 401) return (false, null, null);
else throw;
}
else throw;
}
}

View File

@@ -5,6 +5,7 @@ using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
@@ -37,8 +38,11 @@ namespace VDownload.Core.Services.Sources.Twitch
#region STANDARD METHODS
// GET CHANNEL METADATA
public async Task GetMetadataAsync()
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
@@ -62,8 +66,11 @@ namespace VDownload.Core.Services.Sources.Twitch
}
// GET CHANNEL VIDEOS
public async Task GetVideosAsync(int numberOfVideos)
public async Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();

View File

@@ -41,6 +41,7 @@ namespace VDownload.Core.Services.Sources.Twitch
#region PROPERTIES
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; }
@@ -56,8 +57,11 @@ namespace VDownload.Core.Services.Sources.Twitch
#region STANDARD METHODS
// GET CLIP METADATA
public async Task GetMetadataAsync()
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
@@ -74,7 +78,10 @@ namespace VDownload.Core.Services.Sources.Twitch
// Get response
client.QueryString.Add("id", ID);
JToken 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"];
@@ -84,8 +91,11 @@ namespace VDownload.Core.Services.Sources.Twitch
Thumbnail = new Uri((string)response["thumbnail_url"]);
}
public async Task GetStreamsAsync()
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);

View File

@@ -20,16 +20,8 @@ namespace VDownload.Core.Services.Sources.Twitch
{
#region CONSTANTS
// METADATA TIME FORMATS
private static readonly string[] TimeFormats = new[]
{
@"h\hm\ms\s",
@"m\ms\s",
@"s\s",
};
// 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+)");
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)");
@@ -52,6 +44,7 @@ namespace VDownload.Core.Services.Sources.Twitch
#region PROPERTIES
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; }
@@ -67,8 +60,11 @@ namespace VDownload.Core.Services.Sources.Twitch
#region STANDARD METHODS
// GET VOD METADATA
public async Task GetMetadataAsync()
public async Task GetMetadataAsync(CancellationToken cancellationToken = default)
{
// Set cancellation token
cancellationToken.ThrowIfCancellationRequested();
// Get access token
string accessToken = await Auth.ReadAccessTokenAsync();
if (accessToken == null) throw new TwitchAccessTokenNotFoundException();
@@ -91,18 +87,24 @@ namespace VDownload.Core.Services.Sources.Twitch
}
internal void GetMetadataAsync(JToken response)
{
// 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 = TimeSpan.ParseExact((string)response["duration"], TimeFormats, null);
Duration = ParseDuration((string)response["duration"]);
Views = (long)response["view_count"];
Thumbnail = (string)response["thumbnail_url"] == string.Empty ? null : new Uri((string)response["thumbnail_url"]);
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()
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);
@@ -126,7 +128,7 @@ namespace VDownload.Core.Services.Sources.Twitch
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 = (int)Math.Round(double.Parse(line2.Groups["frame_rate"].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;
@@ -161,12 +163,7 @@ namespace VDownload.Core.Services.Sources.Twitch
List<(Uri ChunkUrl, TimeSpan ChunkDuration)> chunksList = await ExtractChunksFromM3U8Async(audioVideoStream.Url);
// Passive trim
if ((bool)Config.GetValue("twitch_vod_passive_trim"))
{
var trimResult = PassiveVideoTrim(chunksList, trimStart, trimEnd, Duration);
trimStart = trimResult.TrimStart;
trimEnd = trimResult.TrimEnd;
}
if ((bool)Config.GetValue("twitch_vod_passive_trim")) (trimStart, trimEnd) = PassiveVideoTrim(chunksList, trimStart, trimEnd, Duration);
// Download
StorageFile rawFile = await downloadingFolder.CreateFileAsync("raw.ts");
@@ -301,6 +298,20 @@ namespace VDownload.Core.Services.Sources.Twitch
});
}
// PARSE DURATION TO SECONDS
private static TimeSpan ParseDuration(string duration)
{
char[] separators = { 'h', 'm', 's' };
string[] durationParts = duration.Split(separators, StringSplitOptions.RemoveEmptyEntries).Reverse().ToArray();
TimeSpan timeSpan = new TimeSpan(
durationParts.Count() > 2 ? int.Parse(durationParts[2]) : 0,
durationParts.Count() > 1 ? int.Parse(durationParts[1]) : 0,
int.Parse(durationParts[0]));
return timeSpan;
}
#endregion

View File

@@ -121,10 +121,14 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Enums\AudioFileExtension.cs" />
<Compile Include="Enums\DefaultLocationType.cs" />
<Compile Include="Enums\MediaFileExtension.cs" />
<Compile Include="Enums\MediaType.cs" />
<Compile Include="Enums\PlaylistSource.cs" />
<Compile Include="Enums\StreamType.cs" />
<Compile Include="Enums\VideoFileExtension.cs" />
<Compile Include="Enums\VideoSource.cs" />
<Compile Include="EventArgsObjects\VideoAddEventArgs.cs" />
<Compile Include="EventArgsObjects\VideoSearchEventArgs.cs" />
<Compile Include="EventArgsObjects\PlaylistSearchEventArgs.cs" />
<Compile Include="Exceptions\TwitchAccessTokenNotFoundException.cs" />
@@ -136,6 +140,7 @@
<Compile Include="Objects\Stream.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\Channel.cs" />
<Compile Include="Services\Sources\Twitch\Clip.cs" />