twitch vod downloading done
ffmpeg essentials fix Project reorganized git lfs ffmpeg removed ffmpeg added
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using VDownload.Models;
|
||||
|
||||
namespace VDownload.Sources.Common
|
||||
{
|
||||
public interface ISourceSearchService
|
||||
{
|
||||
Task<Video> SearchVideo(string url);
|
||||
Task<Playlist> SearchPlaylist(string url, int maxVideoCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Common
|
||||
{
|
||||
public class MediaSearchException : Exception
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public MediaSearchException() : base() { }
|
||||
|
||||
public MediaSearchException(string message) : base(message) { }
|
||||
|
||||
public MediaSearchException(string message, Exception inner) : base(message, inner) { }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\VDownload.Models\VDownload.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,12 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Services.HttpClient;
|
||||
using VDownload.Services.Data.Configuration;
|
||||
using VDownload.Services.Utility.HttpClient;
|
||||
using VDownload.Sources.Twitch.Api.GQL.GetVideoToken.Response;
|
||||
using VDownload.Sources.Twitch.Api.Helix.GetVideos.Response;
|
||||
using VDownload.Sources.Twitch.Configuration;
|
||||
using VDownload.Sources.Twitch.Search.Models.GetVideoToken.Request;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Api
|
||||
@@ -25,12 +20,8 @@ namespace VDownload.Sources.Twitch.Api
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
private readonly TwitchApiAuthConfiguration _apiAuthConfiguration;
|
||||
private readonly TwitchApiHelixConfiguration _apiHelixConfiguration;
|
||||
private readonly TwitchApiGQLConfiguration _apiGQLConfiguration;
|
||||
private readonly TwitchApiUsherConfiguration _apiUsherConfiguration;
|
||||
|
||||
private readonly IHttpClientService _httpClientService;
|
||||
protected readonly IConfigurationService _configurationService;
|
||||
protected readonly IHttpClientService _httpClientService;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -38,13 +29,9 @@ namespace VDownload.Sources.Twitch.Api
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TwitchApiService(TwitchConfiguration configuration, IHttpClientService httpClientService)
|
||||
public TwitchApiService(IConfigurationService configurationService, IHttpClientService httpClientService)
|
||||
{
|
||||
_apiAuthConfiguration = configuration.Api.Auth;
|
||||
_apiHelixConfiguration = configuration.Api.Helix;
|
||||
_apiGQLConfiguration = configuration.Api.GQL;
|
||||
_apiUsherConfiguration = configuration.Api.Usher;
|
||||
|
||||
_configurationService = configurationService;
|
||||
_httpClientService = httpClientService;
|
||||
}
|
||||
|
||||
@@ -56,31 +43,30 @@ namespace VDownload.Sources.Twitch.Api
|
||||
|
||||
public async Task<string> AuthValidate(byte[] token)
|
||||
{
|
||||
Token tokenData = new Token(_apiAuthConfiguration.TokenSchema, token);
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.GET, _apiAuthConfiguration.Endpoints.Validate);
|
||||
Token tokenData = new Token(_configurationService.Twitch.Api.Auth.TokenSchema, token);
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.GET, _configurationService.Twitch.Api.Auth.Endpoints.Validate);
|
||||
request.Headers.Add("Authorization", $"{tokenData}");
|
||||
return await _httpClientService.SendRequestAsync(request);
|
||||
}
|
||||
|
||||
public async Task<GetVideosResponse> HelixGetVideos(string id, byte[] token)
|
||||
{
|
||||
Token tokenData = new Token(_apiHelixConfiguration.TokenSchema, token);
|
||||
Token tokenData = new Token(_configurationService.Twitch.Api.Helix.TokenSchema, token);
|
||||
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.GET, _apiHelixConfiguration.Endpoints.GetVideos);
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.GET, _configurationService.Twitch.Api.Helix.Endpoints.GetVideos);
|
||||
request.Query.Add("id", id);
|
||||
request.Headers.Add("Authorization", tokenData.ToString());
|
||||
request.Headers.Add("Client-Id", _apiHelixConfiguration.ClientId);
|
||||
request.Headers.Add("Client-Id", _configurationService.Twitch.Api.Helix.ClientId);
|
||||
|
||||
return await _httpClientService.SendRequestAsync<GetVideosResponse>(request);
|
||||
}
|
||||
|
||||
public async Task<GetVideoTokenResponse> GQLGetVideoToken(string id)
|
||||
{
|
||||
TwitchApiGQLQueriesQueryExtendedConfiguration configuration = _apiGQLConfiguration.Queries.GetVideoToken;
|
||||
GetVideoTokenRequest requestBody = new GetVideoTokenRequest
|
||||
{
|
||||
OperationName = configuration.OperationName,
|
||||
Query = configuration.Query,
|
||||
OperationName = _configurationService.Twitch.Api.Gql.Queries.GetVideoToken.OperationName,
|
||||
Query = _configurationService.Twitch.Api.Gql.Queries.GetVideoToken.Query,
|
||||
Variables = new GetVideoTokenVariables
|
||||
{
|
||||
IsLive = false,
|
||||
@@ -91,17 +77,17 @@ namespace VDownload.Sources.Twitch.Api
|
||||
}
|
||||
};
|
||||
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.POST, _apiGQLConfiguration.Endpoint)
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.POST, _configurationService.Twitch.Api.Gql.Endpoint)
|
||||
{
|
||||
Body = requestBody,
|
||||
};
|
||||
request.Headers.Add("Client-Id", _apiGQLConfiguration.ClientId);
|
||||
request.Headers.Add("Client-Id", _configurationService.Twitch.Api.Gql.ClientId);
|
||||
return await _httpClientService.SendRequestAsync<GetVideoTokenResponse>(request);
|
||||
}
|
||||
|
||||
public async Task<string> UsherGetVideoPlaylist(string id, string videoToken, string videoTokenSignature)
|
||||
{
|
||||
string url = string.Format(_apiUsherConfiguration.Endpoints.GetVideoPlaylist, id);
|
||||
string url = string.Format(_configurationService.Twitch.Api.Usher.Endpoints.GetVideoPlaylist, id);
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.GET, url);
|
||||
request.Query.Add("token", videoToken);
|
||||
request.Query.Add("sig", videoTokenSignature);
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.HttpClient\VDownload.Services.HttpClient.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch.Configuration\VDownload.Sources.Twitch.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Configuration\VDownload.Services.Data.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Utility\VDownload.Services.Utility.HttpClient\VDownload.Services.Utility.HttpClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Services.Authentication;
|
||||
using VDownload.Services.Encryption;
|
||||
using VDownload.Services.HttpClient;
|
||||
using VDownload.Services.Data.Authentication;
|
||||
using VDownload.Services.Data.Configuration;
|
||||
using VDownload.Services.Utility.Encryption;
|
||||
using VDownload.Sources.Twitch.Api;
|
||||
using VDownload.Sources.Twitch.Authentication.Models;
|
||||
using VDownload.Sources.Twitch.Configuration;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Authentication
|
||||
{
|
||||
public interface ITwitchAuthenticationService
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
string AuthenticationPageUrl { get; }
|
||||
Regex AuthenticationPageRedirectUrlRegex { get; }
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region METHODS
|
||||
|
||||
Task DeleteToken();
|
||||
Task<byte[]?> GetToken();
|
||||
Task SetToken(byte[] token);
|
||||
Task DeleteToken();
|
||||
Task<TwitchValidationResult> ValidateToken(byte[] token);
|
||||
bool AuthenticationPageClosePredicate(string url);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
@@ -43,21 +21,10 @@ namespace VDownload.Sources.Twitch.Authentication
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
private TwitchAuthenticationConfiguration _authenticationConfiguration;
|
||||
private TwitchApiAuthConfiguration _apiAuthConfiguration;
|
||||
|
||||
private IHttpClientService _httpClientService;
|
||||
private IAuthenticationService _authenticationService;
|
||||
private IEncryptionService _encryptionService;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public string AuthenticationPageUrl { get; private set; }
|
||||
public Regex AuthenticationPageRedirectUrlRegex { get; private set; }
|
||||
protected readonly IConfigurationService _configurationService;
|
||||
protected readonly ITwitchApiService _apiService;
|
||||
protected readonly IAuthenticationDataService _authenticationDataService;
|
||||
protected readonly IEncryptionService _encryptionService;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -65,17 +32,12 @@ namespace VDownload.Sources.Twitch.Authentication
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TwitchAuthenticationService(TwitchConfiguration configuration, IHttpClientService httpClientService, IAuthenticationService authenticationService, IEncryptionService encryptionService)
|
||||
public TwitchAuthenticationService(IConfigurationService configurationService, ITwitchApiService apiService, IAuthenticationDataService authenticationDataService, IEncryptionService encryptionService)
|
||||
{
|
||||
_authenticationConfiguration = configuration.Authentication;
|
||||
_apiAuthConfiguration = configuration.Api.Auth;
|
||||
|
||||
_httpClientService = httpClientService;
|
||||
_authenticationService = authenticationService;
|
||||
_configurationService = configurationService;
|
||||
_apiService = apiService;
|
||||
_authenticationDataService = authenticationDataService;
|
||||
_encryptionService = encryptionService;
|
||||
|
||||
AuthenticationPageUrl = string.Format(_authenticationConfiguration.Url, _authenticationConfiguration.ClientId, _authenticationConfiguration.RedirectUrl, _authenticationConfiguration.ResponseType, string.Join(' ', _authenticationConfiguration.Scopes));
|
||||
AuthenticationPageRedirectUrlRegex = _authenticationConfiguration.RedirectUrlRegex;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -86,11 +48,11 @@ namespace VDownload.Sources.Twitch.Authentication
|
||||
|
||||
public async Task<byte[]?> GetToken()
|
||||
{
|
||||
await _authenticationService.Load();
|
||||
await _authenticationDataService.Load();
|
||||
|
||||
byte[]? tokenEncrypted = _authenticationService.AuthenticationData.Twitch.Token;
|
||||
byte[]? tokenEncrypted = _authenticationDataService.Data.Twitch.Token;
|
||||
|
||||
if (tokenEncrypted is not null && tokenEncrypted.Length == 0)
|
||||
if (tokenEncrypted is not null && tokenEncrypted.Length == 0)
|
||||
{
|
||||
tokenEncrypted = null;
|
||||
}
|
||||
@@ -105,30 +67,27 @@ namespace VDownload.Sources.Twitch.Authentication
|
||||
|
||||
public async Task SetToken(byte[] token)
|
||||
{
|
||||
Task loadTask = _authenticationService.Load();
|
||||
Task loadTask = _authenticationDataService.Load();
|
||||
|
||||
byte[] tokenEncrypted = _encryptionService.Encrypt(token);
|
||||
|
||||
await loadTask;
|
||||
|
||||
_authenticationService.AuthenticationData.Twitch.Token = tokenEncrypted;
|
||||
_authenticationDataService.Data.Twitch.Token = tokenEncrypted;
|
||||
|
||||
await _authenticationService.Save();
|
||||
await _authenticationDataService.Save();
|
||||
}
|
||||
|
||||
public async Task DeleteToken()
|
||||
{
|
||||
await _authenticationService.Load();
|
||||
_authenticationService.AuthenticationData.Twitch.Token = null;
|
||||
await _authenticationService.Save();
|
||||
await _authenticationDataService.Load();
|
||||
_authenticationDataService.Data.Twitch.Token = null;
|
||||
await _authenticationDataService.Save();
|
||||
}
|
||||
|
||||
public async Task<TwitchValidationResult> ValidateToken(byte[] token)
|
||||
{
|
||||
Token tokenData = new Token(_apiAuthConfiguration.TokenSchema, token);
|
||||
HttpRequest request = new HttpRequest(HttpMethodType.GET, _apiAuthConfiguration.Endpoints.Validate);
|
||||
request.Headers.Add("Authorization", $"{tokenData}");
|
||||
string response = await _httpClientService.SendRequestAsync(request);
|
||||
string response = await _apiService.AuthValidate(token);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -136,7 +95,7 @@ namespace VDownload.Sources.Twitch.Authentication
|
||||
return new TwitchValidationResult(success);
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{}
|
||||
{ }
|
||||
|
||||
try
|
||||
{
|
||||
@@ -144,17 +103,11 @@ namespace VDownload.Sources.Twitch.Authentication
|
||||
return new TwitchValidationResult(fail);
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{}
|
||||
{ }
|
||||
|
||||
throw new Exception(response);
|
||||
}
|
||||
|
||||
public bool AuthenticationPageClosePredicate(string url)
|
||||
{
|
||||
bool close = url.StartsWith(_authenticationConfiguration.RedirectUrl);
|
||||
return close;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Authentication\VDownload.Services.Authentication.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Encryption\VDownload.Services.Encryption.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.HttpClient\VDownload.Services.HttpClient.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch.Configuration\VDownload.Sources.Twitch.Configuration.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch\VDownload.Sources.Twitch.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Authentication\VDownload.Services.Data.Authentication.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Configuration\VDownload.Services.Data.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Utility\VDownload.Services.Utility.Encryption\VDownload.Services.Utility.Encryption.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch.Api\VDownload.Sources.Twitch.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Text.Json.Serialization;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Api
|
||||
{
|
||||
[ConfigurationKeyName("auth")]
|
||||
public Auth Auth { get; set; }
|
||||
|
||||
[ConfigurationKeyName("helix")]
|
||||
public Helix Helix { get; set; }
|
||||
|
||||
[ConfigurationKeyName("gql")]
|
||||
public Gql Gql { get; set; }
|
||||
|
||||
[ConfigurationKeyName("usher")]
|
||||
public Usher Usher { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Auth
|
||||
{
|
||||
[ConfigurationKeyName("token_schema")]
|
||||
public string TokenSchema { get; set; }
|
||||
|
||||
[ConfigurationKeyName("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
[ConfigurationKeyName("endpoints")]
|
||||
public EndpointsAuth Endpoints { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models
|
||||
{
|
||||
public class Authentication
|
||||
{
|
||||
[ConfigurationKeyName("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[ConfigurationKeyName("redirect_url")]
|
||||
public string RedirectUrl { get; set; }
|
||||
|
||||
[ConfigurationKeyName("redirect_url_regex")]
|
||||
public string RedirectUrlRegex { get; set; }
|
||||
|
||||
[ConfigurationKeyName("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
[ConfigurationKeyName("response_type")]
|
||||
public string ResponseType { get; set; }
|
||||
|
||||
[ConfigurationKeyName("scopes")]
|
||||
public List<string> Scopes { get; } = new List<string>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Download
|
||||
{
|
||||
[ConfigurationKeyName("vod")]
|
||||
public Vod Vod { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class EndpointsAuth
|
||||
{
|
||||
[ConfigurationKeyName("validate")]
|
||||
public string Validate { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models
|
||||
{
|
||||
public class EndpointsHelix
|
||||
{
|
||||
[ConfigurationKeyName("get_videos")]
|
||||
public string GetVideos { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models
|
||||
{
|
||||
public class EndpointsUsher
|
||||
{
|
||||
[ConfigurationKeyName("get_video_playlist")]
|
||||
public string GetVideoPlaylist { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class GetVideoToken
|
||||
{
|
||||
[ConfigurationKeyName("operation_name")]
|
||||
public string OperationName { get; set; }
|
||||
|
||||
[ConfigurationKeyName("query")]
|
||||
public string Query { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Gql
|
||||
{
|
||||
[ConfigurationKeyName("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
[ConfigurationKeyName("endpoint")]
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
[ConfigurationKeyName("queries")]
|
||||
public Queries Queries { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Helix
|
||||
{
|
||||
[ConfigurationKeyName("token_schema")]
|
||||
public string TokenSchema { get; set; }
|
||||
|
||||
[ConfigurationKeyName("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
[ConfigurationKeyName("endpoints")]
|
||||
public EndpointsHelix Endpoints { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Queries
|
||||
{
|
||||
[ConfigurationKeyName("get_video_token")]
|
||||
public GetVideoToken GetVideoToken { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Search
|
||||
{
|
||||
[ConfigurationKeyName("general_regexes")]
|
||||
public List<string> GeneralRegexes { get; } = new List<string>();
|
||||
|
||||
[ConfigurationKeyName("vod")]
|
||||
public Vod Vod { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Thumbnail
|
||||
{
|
||||
[ConfigurationKeyName("width")]
|
||||
public int Width { get; set; }
|
||||
|
||||
[ConfigurationKeyName("height")]
|
||||
public int Height { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Usher
|
||||
{
|
||||
[ConfigurationKeyName("endpoints")]
|
||||
public EndpointsUsher Endpoints { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
namespace VDownload.Sources.Twitch.Configuration.Models{
|
||||
|
||||
public class Vod
|
||||
{
|
||||
[ConfigurationKeyName("regexes")]
|
||||
public List<string> Regexes { get; } = new List<string>();
|
||||
|
||||
[ConfigurationKeyName("thumbnail")]
|
||||
public Thumbnail Thumbnail { get; set; }
|
||||
|
||||
[ConfigurationKeyName("stream_playlist_regex")]
|
||||
public string StreamPlaylistRegex { get; set; }
|
||||
|
||||
[ConfigurationKeyName("chunk_regex")]
|
||||
public string ChunkRegex { get; set; }
|
||||
|
||||
[ConfigurationKeyName("file_name")]
|
||||
public string FileName { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiAuthConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string TokenSchema { get; protected set; }
|
||||
public string ClientId { get; protected set; }
|
||||
public TwitchApiAuthEndpointsConfiguration Endpoints { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiAuthConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
TokenSchema = configuration["token_schema"];
|
||||
ClientId = configuration["client_id"];
|
||||
Endpoints = new TwitchApiAuthEndpointsConfiguration(configuration.GetSection("endpoints"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiAuthEndpointsConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string Validate { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiAuthEndpointsConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
Validate = configuration["validate"];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public TwitchApiAuthConfiguration Auth { get; protected set; }
|
||||
public TwitchApiHelixConfiguration Helix { get; protected set; }
|
||||
public TwitchApiGQLConfiguration GQL { get; protected set; }
|
||||
public TwitchApiUsherConfiguration Usher { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
Auth = new TwitchApiAuthConfiguration(configuration.GetSection("auth"));
|
||||
Helix = new TwitchApiHelixConfiguration(configuration.GetSection("helix"));
|
||||
GQL = new TwitchApiGQLConfiguration(configuration.GetSection("gql"));
|
||||
Usher = new TwitchApiUsherConfiguration(configuration.GetSection("usher"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiGQLConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string ClientId { get; protected set; }
|
||||
public string Endpoint { get; protected set; }
|
||||
public TwitchApiGQLQueriesConfiguration Queries { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiGQLConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
ClientId = configuration["client_id"];
|
||||
Endpoint = configuration["endpoint"];
|
||||
Queries = new TwitchApiGQLQueriesConfiguration(configuration.GetSection("queries"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiGQLQueriesConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public TwitchApiGQLQueriesQueryExtendedConfiguration GetVideoToken { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiGQLQueriesConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
GetVideoToken = new TwitchApiGQLQueriesQueryExtendedConfiguration(configuration.GetSection("get_video_token"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiGQLQueriesQueryConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string Query { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiGQLQueriesQueryConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
Query = configuration["query"];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiGQLQueriesQueryExtendedConfiguration : TwitchApiGQLQueriesQueryConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string OperationName { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiGQLQueriesQueryExtendedConfiguration(IConfigurationSection configuration) : base(configuration)
|
||||
{
|
||||
OperationName = configuration["operation_name"];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiHelixConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string TokenSchema { get; protected set; }
|
||||
public string ClientId { get; protected set; }
|
||||
public TwitchApiHelixEndpointsConfiguration Endpoints { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiHelixConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
TokenSchema = configuration["token_schema"];
|
||||
ClientId = configuration["client_id"];
|
||||
Endpoints = new TwitchApiHelixEndpointsConfiguration(configuration.GetSection("endpoints"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiHelixEndpointsConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string GetVideos { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiHelixEndpointsConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
GetVideos = configuration["get_videos"];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiUsherConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public TwitchApiUsherEndpointsConfiguration Endpoints { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiUsherConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
Endpoints = new TwitchApiUsherEndpointsConfiguration(configuration.GetSection("endpoints"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchApiUsherEndpointsConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string GetVideoPlaylist { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchApiUsherEndpointsConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
GetVideoPlaylist = configuration["get_video_playlist"];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchAuthenticationConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public string Url { get; protected set; }
|
||||
public string RedirectUrl { get; protected set; }
|
||||
public Regex RedirectUrlRegex { get; protected set; }
|
||||
public string ClientId { get; protected set; }
|
||||
public string ResponseType { get; protected set; }
|
||||
public IEnumerable<string> Scopes { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchAuthenticationConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
Url = configuration["url"];
|
||||
RedirectUrl = configuration["redirect_url"];
|
||||
RedirectUrlRegex = new Regex(configuration["redirect_url_regex"]);
|
||||
ClientId = configuration["client_id"];
|
||||
ResponseType = configuration["response_type"];
|
||||
Scopes = configuration.GetSection("scopes").Get<IEnumerable<string>>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -3,33 +3,24 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Sources.Twitch.Configuration.Models;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
[ConfigurationKeyName("api")]
|
||||
public Api Api { get; set; }
|
||||
|
||||
public TwitchApiConfiguration Api { get; protected set; }
|
||||
public TwitchSearchConfiguration Search { get; protected set; }
|
||||
public TwitchAuthenticationConfiguration Authentication { get; protected set; }
|
||||
[ConfigurationKeyName("search")]
|
||||
public Search Search { get; set; }
|
||||
|
||||
#endregion
|
||||
[ConfigurationKeyName("download")]
|
||||
public Download Download { get; set; }
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TwitchConfiguration(IConfiguration configuration)
|
||||
{
|
||||
IConfigurationSection section = configuration.GetSection("sources").GetSection("twitch");
|
||||
Api = new TwitchApiConfiguration(section.GetSection("api"));
|
||||
Search = new TwitchSearchConfiguration(section.GetSection("search"));
|
||||
Authentication = new TwitchAuthenticationConfiguration(section.GetSection("authentication"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
[ConfigurationKeyName("authentication")]
|
||||
public Authentication Authentication { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Configuration
|
||||
{
|
||||
public class TwitchSearchConfiguration
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
public IEnumerable<Regex> GeneralRegexes { get; protected set; }
|
||||
public IEnumerable<Regex> VodRegexes { get; protected set; }
|
||||
public Regex VodStreamPlaylistRegex { get; protected set; }
|
||||
public int VodThumbnailWidth { get; protected set; }
|
||||
public int VodThumbnailHeight { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
internal TwitchSearchConfiguration(IConfigurationSection configuration)
|
||||
{
|
||||
GeneralRegexes = configuration.GetSection("general_regexes").Get<IEnumerable<string>>().Select(x => new Regex(x));
|
||||
VodRegexes = configuration.GetSection("vod_regexes").Get<IEnumerable<string>>().Select(x => new Regex(x));
|
||||
VodStreamPlaylistRegex = new Regex(configuration["vod_stream_playlist_regex"]);
|
||||
VodThumbnailWidth = int.Parse(configuration["vod_thumbnail_width"]);
|
||||
VodThumbnailHeight = int.Parse(configuration["vod_thumbnail_height"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Models.Internal
|
||||
{
|
||||
internal class TwitchVodChunk
|
||||
{
|
||||
public required string Location { get; init; }
|
||||
public required string Url { get; init; }
|
||||
public required long Index { get; init; }
|
||||
public required TimeSpan Duration { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Common;
|
||||
using VDownload.Models;
|
||||
|
||||
namespace VDownload.Sources.Twitch
|
||||
namespace VDownload.Sources.Twitch.Models
|
||||
{
|
||||
public abstract class TwitchPlaylist : Playlist
|
||||
{
|
||||
@@ -0,0 +1,16 @@
|
||||
using VDownload.Models;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Models
|
||||
{
|
||||
public abstract class TwitchVideo : Video
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
protected TwitchVideo()
|
||||
{
|
||||
Source = Source.Twitch;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch
|
||||
namespace VDownload.Sources.Twitch.Models
|
||||
{
|
||||
public class TwitchVod : TwitchVideo
|
||||
{
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks.Dataflow;
|
||||
using VDownload.Models;
|
||||
using VDownload.Services.Data.Configuration;
|
||||
using VDownload.Services.Data.Settings;
|
||||
using VDownload.Sources.Twitch.Models.Internal;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Models
|
||||
{
|
||||
public class TwitchVodStream : VideoStream
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
protected readonly HttpClient _httpClient;
|
||||
|
||||
protected readonly IConfigurationService _configurationService;
|
||||
protected readonly ISettingsService _settingsService;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PROPERTIES
|
||||
|
||||
public string UrlM3U8 { get; set; }
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public string VideoCodec { get; set; }
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TwitchVodStream(HttpClient httpClient, IConfigurationService configurationService, ISettingsService settingsService)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
|
||||
_configurationService = configurationService;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async override Task<VideoStreamDownloadResult> Download(string taskTemporaryDirectory, IProgress<double> onProgress, CancellationToken token, TimeSpan trimStart, TimeSpan trimEnd)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
string m3u8 = await _httpClient.GetStringAsync(UrlM3U8, token);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
string m3u8BaseUrl = Path.GetDirectoryName(UrlM3U8).Replace("https:\\", "https://").Replace("http:\\", "http://").Replace('\\', '/');
|
||||
Regex regex = new Regex(_configurationService.Twitch.Download.Vod.ChunkRegex);
|
||||
MatchCollection matches = regex.Matches(m3u8);
|
||||
long index = 0;
|
||||
|
||||
List<TwitchVodChunk> chunks = new List<TwitchVodChunk>();
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
string filename = match.Groups["file"].Value;
|
||||
string durationString = match.Groups["duration"].Value;
|
||||
|
||||
TimeSpan chunkDuration = TimeSpan.FromSeconds(double.Parse(durationString, CultureInfo.InvariantCulture));
|
||||
string url = $"{m3u8BaseUrl}/{filename}";
|
||||
string location = Path.Combine(taskTemporaryDirectory, filename);
|
||||
|
||||
chunks.Add(new TwitchVodChunk
|
||||
{
|
||||
Url = url,
|
||||
Index = index,
|
||||
Duration = chunkDuration,
|
||||
Location = location
|
||||
});
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
TimeSpan duration = TimeSpan.FromTicks(chunks.Sum(x => x.Duration.Ticks));
|
||||
|
||||
if (_settingsService.Data.Twitch.Vod.PassiveTrimming)
|
||||
{
|
||||
PassiveTrimming(chunks, ref trimStart, ref trimEnd, ref duration);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
long downloadedCount = 0;
|
||||
Action taskEnd = () =>
|
||||
{
|
||||
downloadedCount++;
|
||||
double progress = ((double)downloadedCount / chunks.Count) * 100;
|
||||
onProgress.Report(progress);
|
||||
};
|
||||
ActionBlock<TwitchVodChunk> block = new ActionBlock<TwitchVodChunk>(x => DownloadChunk(x, token, taskEnd), new ExecutionDataflowBlockOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = _settingsService.Data.Twitch.Vod.MaxNumberOfParallelDownloads
|
||||
});
|
||||
foreach (TwitchVodChunk chunk in chunks)
|
||||
{
|
||||
block.Post(chunk);
|
||||
}
|
||||
block.Complete();
|
||||
await block.Completion;
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
string file = Path.Combine(taskTemporaryDirectory, _configurationService.Twitch.Download.Vod.FileName);
|
||||
MergeFiles(file, chunks.Select(x => x.Location), token, true);
|
||||
|
||||
return new VideoStreamDownloadResult
|
||||
{
|
||||
File = file,
|
||||
NewTrimStart = trimStart,
|
||||
NewTrimEnd = trimEnd,
|
||||
NewDuration = duration,
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private void MergeFiles(string destinationPath, IEnumerable<string> sourceFiles, CancellationToken token, bool deleteSource = false)
|
||||
{
|
||||
using (FileStream outputStream = File.Create(destinationPath))
|
||||
{
|
||||
foreach (string path in sourceFiles)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
using (FileStream inputStream = File.OpenRead(path))
|
||||
{
|
||||
inputStream.CopyTo(outputStream);
|
||||
}
|
||||
|
||||
if (deleteSource)
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PassiveTrimming(List<TwitchVodChunk> chunks, ref TimeSpan trimStart, ref TimeSpan trimEnd, ref TimeSpan duration)
|
||||
{
|
||||
while (chunks.First().Duration <= trimStart)
|
||||
{
|
||||
TwitchVodChunk chunk = chunks.First();
|
||||
TimeSpan chunkDuration = chunk.Duration;
|
||||
trimStart -= chunkDuration;
|
||||
trimEnd -= chunkDuration;
|
||||
duration -= chunkDuration;
|
||||
chunks.Remove(chunk);
|
||||
}
|
||||
|
||||
while (chunks.Last().Duration <= duration.Subtract(trimEnd))
|
||||
{
|
||||
TwitchVodChunk chunk = chunks.Last();
|
||||
TimeSpan chunkDuration = chunk.Duration;
|
||||
duration -= chunkDuration;
|
||||
chunks.Remove(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadChunk(TwitchVodChunk chunk, CancellationToken token, Action onTaskEndSuccessfully)
|
||||
{
|
||||
int retriesCount = 0;
|
||||
while (true)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
byte[] data = await _httpClient.GetByteArrayAsync(chunk.Url, token);
|
||||
await File.WriteAllBytesAsync(chunk.Location, data, token);
|
||||
onTaskEndSuccessfully.Invoke();
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException)
|
||||
{
|
||||
if (_settingsService.Data.Twitch.Vod.ChunkDownloadingError.Retry && retriesCount < _settingsService.Data.Twitch.Vod.ChunkDownloadingError.RetriesCount)
|
||||
{
|
||||
retriesCount++;
|
||||
await Task.Delay(_settingsService.Data.Twitch.Vod.ChunkDownloadingError.RetryDelay);
|
||||
}
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\VDownload.Models\VDownload.Models.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Configuration\VDownload.Services.Data.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Settings\VDownload.Services.Data.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,165 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Common;
|
||||
using VDownload.Common.Exceptions;
|
||||
using VDownload.Common.Models;
|
||||
using VDownload.Common.Services;
|
||||
using VDownload.Services.HttpClient;
|
||||
using VDownload.Sources.Twitch.Api;
|
||||
using VDownload.Sources.Twitch.Api.GQL.GetVideoToken.Response;
|
||||
using VDownload.Sources.Twitch.Api.Helix.GetVideos.Response;
|
||||
using VDownload.Sources.Twitch.Authentication;
|
||||
using VDownload.Sources.Twitch.Configuration;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Search
|
||||
{
|
||||
public interface ITwitchSearchService : ISourceSearchService
|
||||
{
|
||||
Task<TwitchVideo> SearchVideo(string url);
|
||||
Task<TwitchPlaylist> SearchPlaylist(string url, int maxVideoCount);
|
||||
}
|
||||
|
||||
public class TwitchSearchService : ITwitchSearchService
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
private readonly TwitchApiHelixConfiguration _apiHelixConfiguration;
|
||||
private readonly TwitchApiGQLConfiguration _apiGQLConfiguration;
|
||||
private readonly TwitchSearchConfiguration _searchConfiguration;
|
||||
|
||||
private readonly ITwitchApiService _apiService;
|
||||
private readonly ITwitchAuthenticationService _twitchAuthenticationService;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TwitchSearchService(TwitchConfiguration configuration, ITwitchApiService apiService, ITwitchAuthenticationService twitchAuthenticationService)
|
||||
{
|
||||
_apiHelixConfiguration = configuration.Api.Helix;
|
||||
_apiGQLConfiguration = configuration.Api.GQL;
|
||||
_searchConfiguration = configuration.Search;
|
||||
|
||||
_apiService = apiService;
|
||||
_twitchAuthenticationService = twitchAuthenticationService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
async Task<Video> ISourceSearchService.SearchVideo(string url) => await SearchVideo(url);
|
||||
public async Task<TwitchVideo> SearchVideo(string url)
|
||||
{
|
||||
foreach (Regex regex in _searchConfiguration.VodRegexes)
|
||||
{
|
||||
Match match = regex.Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
string id = match.Groups[1].Value;
|
||||
return await GetVod(id);
|
||||
}
|
||||
}
|
||||
throw new MediaSearchException("Invalid url");
|
||||
}
|
||||
|
||||
async Task<Playlist> ISourceSearchService.SearchPlaylist(string url, int maxVideoCount) => await SearchPlaylist(url, maxVideoCount);
|
||||
public async Task<TwitchPlaylist> SearchPlaylist(string url, int maxVideoCount)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private async Task<byte[]> GetToken()
|
||||
{
|
||||
byte[]? token = await _twitchAuthenticationService.GetToken();
|
||||
if (token is null)
|
||||
{
|
||||
throw new MediaSearchException("Not authenticated to Twitch");
|
||||
}
|
||||
TwitchValidationResult validation = await _twitchAuthenticationService.ValidateToken(token);
|
||||
if (!validation.Success)
|
||||
{
|
||||
throw new MediaSearchException("Twitch authentication error");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private async Task<TwitchVod> GetVod(string id)
|
||||
{
|
||||
Task<IEnumerable<TwitchVodStream>> streamsTask = GetVodStreams(id);
|
||||
|
||||
byte[] token = await GetToken();
|
||||
GetVideosResponse info = await _apiService.HelixGetVideos(id, token);
|
||||
Data vodResponse = info.Data[0];
|
||||
|
||||
TwitchVod vod = new TwitchVod
|
||||
{
|
||||
Title = vodResponse.Title,
|
||||
Description = vodResponse.Description,
|
||||
Author = vodResponse.UserName,
|
||||
PublishDate = vodResponse.PublishedAt,
|
||||
Duration = ParseVodDuration(vodResponse.Duration),
|
||||
ViewCount = vodResponse.ViewCount,
|
||||
ThumbnailUrl = vodResponse.ThumbnailUrl.Replace("%{width}", _searchConfiguration.VodThumbnailWidth.ToString()).Replace("%{height}", _searchConfiguration.VodThumbnailHeight.ToString()),
|
||||
Url = vodResponse.Url,
|
||||
};
|
||||
|
||||
await streamsTask;
|
||||
foreach (TwitchVodStream stream in streamsTask.Result)
|
||||
{
|
||||
vod.Streams.Add(stream);
|
||||
}
|
||||
|
||||
return vod;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<TwitchVodStream>> GetVodStreams(string id)
|
||||
{
|
||||
GetVideoTokenResponse videoToken = await _apiService.GQLGetVideoToken(id);
|
||||
|
||||
string playlist = await _apiService.UsherGetVideoPlaylist(id, videoToken.Data.VideoPlaybackAccessToken.Value, videoToken.Data.VideoPlaybackAccessToken.Signature);
|
||||
|
||||
MatchCollection matches = _searchConfiguration.VodStreamPlaylistRegex.Matches(playlist);
|
||||
|
||||
List<TwitchVodStream> streams = new List<TwitchVodStream>();
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
streams.Add(new TwitchVodStream
|
||||
{
|
||||
StreamIdentifier = match.Groups["id"].Value,
|
||||
Codecs = match.Groups["codecs"].Value,
|
||||
Width = int.Parse(match.Groups["width"].Value),
|
||||
Height = int.Parse(match.Groups["height"].Value),
|
||||
UrlM3U8 = match.Groups["url"].Value
|
||||
});
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
private TimeSpan ParseVodDuration(string duration)
|
||||
{
|
||||
int hours = int.Parse(duration.Split('h')[0]);
|
||||
duration = duration.Split('h')[1];
|
||||
int minutes = int.Parse(duration.Split('m')[0]);
|
||||
duration = duration.Split('m')[1];
|
||||
int seconds = int.Parse(duration.Split('s')[0]);
|
||||
return TimeSpan.FromSeconds(seconds) + TimeSpan.FromMinutes(minutes) + TimeSpan.FromHours(hours);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\VDownload.Common\VDownload.Common.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch.Api\VDownload.Sources.Twitch.Api.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch.Authentication\VDownload.Sources.Twitch.Authentication.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch\VDownload.Sources.Twitch.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Settings.Models
|
||||
{
|
||||
public class ChunkDownloadingError
|
||||
{
|
||||
[JsonProperty("error_retry")]
|
||||
public bool Retry { get; set; } = true;
|
||||
|
||||
[JsonProperty("retries_count")]
|
||||
public int RetriesCount { get; set; } = 10;
|
||||
|
||||
[JsonProperty("retry_delay")]
|
||||
public int RetryDelay { get; set; } = 5000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Settings.Models
|
||||
{
|
||||
public class Vod
|
||||
{
|
||||
[JsonProperty("passive_trimming")]
|
||||
public bool PassiveTrimming { get; set; } = true;
|
||||
|
||||
[JsonProperty("chunk_downloading_error")]
|
||||
public ChunkDownloadingError ChunkDownloadingError { get; set; } = new ChunkDownloadingError();
|
||||
|
||||
[JsonProperty("max_number_of_parallel_downloads")]
|
||||
public int MaxNumberOfParallelDownloads { get; set; } = 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Sources.Twitch.Settings.Models;
|
||||
|
||||
namespace VDownload.Sources.Twitch.Settings
|
||||
{
|
||||
public class TwitchSettings
|
||||
{
|
||||
[JsonProperty("vod")]
|
||||
public Vod Vod { get; set; } = new Vod();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Models;
|
||||
using VDownload.Services.Data.Configuration;
|
||||
using VDownload.Sources.Common;
|
||||
using VDownload.Sources.Twitch.Api;
|
||||
using VDownload.Sources.Twitch.Api.GQL.GetVideoToken.Response;
|
||||
using VDownload.Sources.Twitch.Api.Helix.GetVideos.Response;
|
||||
using VDownload.Sources.Twitch.Authentication;
|
||||
using VDownload.Sources.Twitch.Configuration.Models;
|
||||
using VDownload.Sources.Twitch.Models;
|
||||
|
||||
namespace VDownload.Sources.Twitch
|
||||
{
|
||||
public interface ITwitchSearchService : ISourceSearchService
|
||||
{
|
||||
Task<TwitchPlaylist> SearchPlaylist(string url, int maxVideoCount);
|
||||
Task<TwitchVideo> SearchVideo(string url);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class TwitchSearchService : ITwitchSearchService
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
protected readonly IConfigurationService _configurationService;
|
||||
protected readonly ITwitchApiService _apiService;
|
||||
protected readonly ITwitchAuthenticationService _twitchAuthenticationService;
|
||||
protected readonly ITwitchVideoStreamFactoryService _videoStreamFactoryService;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TwitchSearchService(IConfigurationService configurationService, ITwitchApiService apiService, ITwitchAuthenticationService authenticationService, ITwitchVideoStreamFactoryService videoStreamFactoryService)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_apiService = apiService;
|
||||
_twitchAuthenticationService = authenticationService;
|
||||
_videoStreamFactoryService = videoStreamFactoryService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
async Task<Video> ISourceSearchService.SearchVideo(string url) => await SearchVideo(url);
|
||||
public async Task<TwitchVideo> SearchVideo(string url)
|
||||
{
|
||||
foreach (Regex regex in _configurationService.Twitch.Search.Vod.Regexes.Select(x => new Regex(x)))
|
||||
{
|
||||
Match match = regex.Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
string id = match.Groups[1].Value;
|
||||
return await GetVod(id);
|
||||
}
|
||||
}
|
||||
throw new MediaSearchException("Invalid url"); // TODO : Change to string resource
|
||||
}
|
||||
|
||||
async Task<Playlist> ISourceSearchService.SearchPlaylist(string url, int maxVideoCount) => await SearchPlaylist(url, maxVideoCount);
|
||||
public async Task<TwitchPlaylist> SearchPlaylist(string url, int maxVideoCount)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
protected async Task<TwitchVod> GetVod(string id)
|
||||
{
|
||||
Task<IEnumerable<TwitchVodStream>> streamsTask = GetVodStreams(id);
|
||||
|
||||
byte[] token = await GetToken();
|
||||
GetVideosResponse info = await _apiService.HelixGetVideos(id, token);
|
||||
Data vodResponse = info.Data[0];
|
||||
|
||||
Thumbnail thumbnail = _configurationService.Twitch.Search.Vod.Thumbnail;
|
||||
TwitchVod vod = new TwitchVod
|
||||
{
|
||||
Title = vodResponse.Title,
|
||||
Description = vodResponse.Description,
|
||||
Author = vodResponse.UserName,
|
||||
PublishDate = vodResponse.PublishedAt,
|
||||
Duration = ParseVodDuration(vodResponse.Duration),
|
||||
Views = vodResponse.ViewCount,
|
||||
ThumbnailUrl = new Uri(vodResponse.ThumbnailUrl.Replace("%{width}", thumbnail.Width.ToString()).Replace("%{height}", thumbnail.Height.ToString())),
|
||||
Url = new Uri(vodResponse.Url),
|
||||
};
|
||||
|
||||
await streamsTask;
|
||||
foreach (TwitchVodStream stream in streamsTask.Result)
|
||||
{
|
||||
vod.Streams.Add(stream);
|
||||
}
|
||||
|
||||
return vod;
|
||||
}
|
||||
|
||||
protected async Task<IEnumerable<TwitchVodStream>> GetVodStreams(string id)
|
||||
{
|
||||
GetVideoTokenResponse videoToken = await _apiService.GQLGetVideoToken(id);
|
||||
|
||||
string playlist = await _apiService.UsherGetVideoPlaylist(id, videoToken.Data.VideoPlaybackAccessToken.Value, videoToken.Data.VideoPlaybackAccessToken.Signature);
|
||||
|
||||
Regex regex = new Regex(_configurationService.Twitch.Search.Vod.StreamPlaylistRegex);
|
||||
MatchCollection matches = regex.Matches(playlist);
|
||||
|
||||
List<TwitchVodStream> streams = new List<TwitchVodStream>();
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
TwitchVodStream stream = _videoStreamFactoryService.CreateVodStream();
|
||||
stream.Name = match.Groups["id"].Value;
|
||||
stream.VideoCodec = match.Groups["video_codec"].Value;
|
||||
stream.AudioCodec = match.Groups["audio_codec"].Value;
|
||||
stream.Width = int.Parse(match.Groups["width"].Value);
|
||||
stream.Height = int.Parse(match.Groups["height"].Value);
|
||||
stream.UrlM3U8 = match.Groups["url"].Value;
|
||||
streams.Add(stream);
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
protected TimeSpan ParseVodDuration(string duration)
|
||||
{
|
||||
IEnumerable<string> parts = duration.Split(['h', 'm', 's'])[..^1].Reverse();
|
||||
string? seconds = parts.ElementAtOrDefault(0);
|
||||
string? minutes = parts.ElementAtOrDefault(1);
|
||||
string? hours = parts.ElementAtOrDefault(2);
|
||||
|
||||
TimeSpan timeSpan = TimeSpan.Zero;
|
||||
if (!string.IsNullOrEmpty(seconds))
|
||||
{
|
||||
int secondsInt = int.Parse(seconds);
|
||||
timeSpan += TimeSpan.FromSeconds(secondsInt);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(minutes))
|
||||
{
|
||||
int minutesInt = int.Parse(minutes);
|
||||
timeSpan += TimeSpan.FromMinutes(minutesInt);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(hours))
|
||||
{
|
||||
int hoursInt = int.Parse(hours);
|
||||
timeSpan += TimeSpan.FromHours(hoursInt);
|
||||
}
|
||||
return timeSpan;
|
||||
}
|
||||
|
||||
protected async Task<byte[]> GetToken()
|
||||
{
|
||||
byte[]? token = await _twitchAuthenticationService.GetToken();
|
||||
if (token is null)
|
||||
{
|
||||
throw new MediaSearchException("Not authenticated to Twitch"); // TODO : Change to string resource
|
||||
}
|
||||
TwitchValidationResult validation = await _twitchAuthenticationService.ValidateToken(token);
|
||||
if (!validation.Success)
|
||||
{
|
||||
throw new MediaSearchException("Twitch authentication error"); // TODO : Change to string resource
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Common.Models;
|
||||
|
||||
namespace VDownload.Sources.Twitch
|
||||
{
|
||||
public abstract class TwitchVideo : Video
|
||||
{
|
||||
#region CONSTRUCTORS
|
||||
|
||||
protected TwitchVideo()
|
||||
{
|
||||
_source = Common.Source.Twitch;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using VDownload.Services.Data.Configuration;
|
||||
using VDownload.Services.Data.Settings;
|
||||
using VDownload.Sources.Twitch.Models;
|
||||
|
||||
namespace VDownload.Sources.Twitch
|
||||
{
|
||||
public interface ITwitchVideoStreamFactoryService
|
||||
{
|
||||
TwitchVodStream CreateVodStream();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class TwitchVideoStreamFactoryService : ITwitchVideoStreamFactoryService
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
protected readonly HttpClient _httpClient;
|
||||
|
||||
protected readonly IConfigurationService _configurationService;
|
||||
protected readonly ISettingsService _settingsService;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TwitchVideoStreamFactoryService(HttpClient httpClient, IConfigurationService configurationService, ISettingsService settingsService)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_configurationService = configurationService;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public TwitchVodStream CreateVodStream() => new TwitchVodStream(_httpClient, _configurationService, _settingsService);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Common;
|
||||
|
||||
namespace VDownload.Sources.Twitch
|
||||
{
|
||||
public partial class TwitchVodStream : VideoStream
|
||||
{
|
||||
#region PROPERTIES
|
||||
|
||||
[ObservableProperty]
|
||||
private string _urlM3U8;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _height;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _width;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _codecs;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\VDownload.Common\VDownload.Common.csproj" />
|
||||
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Configuration\VDownload.Services.Data.Configuration.csproj" />
|
||||
<ProjectReference Include="..\..\VDownload.Sources.Common\VDownload.Sources.Common.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch.Authentication\VDownload.Sources.Twitch.Authentication.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch.Models\VDownload.Sources.Twitch.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
94
VDownload.Sources/VDownload.Sources/SearchService.cs
Normal file
94
VDownload.Sources/VDownload.Sources/SearchService.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using VDownload.Models;
|
||||
using VDownload.Services.Data.Configuration;
|
||||
using VDownload.Sources.Common;
|
||||
using VDownload.Sources.Twitch;
|
||||
using VDownload.Sources.Twitch.Configuration;
|
||||
using VDownload.Sources.Twitch.Search;
|
||||
|
||||
namespace VDownload.Sources
|
||||
{
|
||||
public interface ISearchService
|
||||
{
|
||||
Task<Playlist> SearchPlaylist(string url, int maxVideoCount);
|
||||
Task<Video> SearchVideo(string url);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class SearchService : ISearchService
|
||||
{
|
||||
#region FIELDS
|
||||
|
||||
private readonly List<(Regex Regex, ISourceSearchService Service)> _urlMappings;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public SearchService(IConfigurationService configurationService, ITwitchSearchService twitchSearchService)
|
||||
{
|
||||
_urlMappings =
|
||||
[
|
||||
.. configurationService.Twitch.Search.GeneralRegexes.Select(x => (new Regex(x), (ISourceSearchService)twitchSearchService)),
|
||||
];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async Task<Video> SearchVideo(string url)
|
||||
{
|
||||
BaseUrlCheck(url);
|
||||
|
||||
foreach ((Regex Regex, ISourceSearchService Service) mapping in _urlMappings)
|
||||
{
|
||||
if (mapping.Regex.IsMatch(url))
|
||||
{
|
||||
return await mapping.Service.SearchVideo(url);
|
||||
}
|
||||
}
|
||||
throw new MediaSearchException("Source is not supported"); // TODO : Change to string resource
|
||||
}
|
||||
|
||||
public async Task<Playlist> SearchPlaylist(string url, int maxVideoCount)
|
||||
{
|
||||
BaseUrlCheck(url);
|
||||
|
||||
foreach ((Regex Regex, ISourceSearchService Service) mapping in _urlMappings)
|
||||
{
|
||||
if (mapping.Regex.IsMatch(url))
|
||||
{
|
||||
return await mapping.Service.SearchPlaylist(url, maxVideoCount);
|
||||
}
|
||||
}
|
||||
throw new MediaSearchException("Source is not supported"); // TODO : Change to string resource
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private void BaseUrlCheck(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
throw new MediaSearchException("Url cannot be empty"); // TODO : Change to string resource
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
15
VDownload.Sources/VDownload.Sources/VDownload.Sources.csproj
Normal file
15
VDownload.Sources/VDownload.Sources/VDownload.Sources.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\VDownload.Services\VDownload.Services.Data\VDownload.Services.Data.Configuration\VDownload.Services.Data.Configuration.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Common\VDownload.Sources.Common.csproj" />
|
||||
<ProjectReference Include="..\VDownload.Sources.Twitch\VDownload.Sources.Twitch\VDownload.Sources.Twitch.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user