new_version_init

This commit is contained in:
2024-02-13 02:59:40 +01:00
Unverified
parent e36c1404ee
commit 91f9b645bd
352 changed files with 6777 additions and 8326 deletions

View File

@@ -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.Api.GQL.GetVideoToken.Request
{
public class GetVideoTokenExtensions
{
[JsonProperty("durationMilliseconds")]
public int DurationMilliseconds { get; set; }
[JsonProperty("operationName")]
public string OperationName { get; set; }
[JsonProperty("requestID")]
public string RequestID { get; set; }
}
}

View File

@@ -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.Search.Models.GetVideoToken.Request
{
public class GetVideoTokenRequest
{
[JsonProperty("operationName")]
public string OperationName { get; set; }
[JsonProperty("query")]
public string Query { get; set; }
[JsonProperty("variables")]
public GetVideoTokenVariables Variables { get; set; }
}
}

View File

@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Sources.Twitch.Search.Models.GetVideoToken.Request
{
public class GetVideoTokenVariables
{
[JsonProperty("isLive")]
public bool IsLive { get; set; }
[JsonProperty("login")]
public string Login { get; set; }
[JsonProperty("isVod")]
public bool IsVod { get; set; }
[JsonProperty("vodID")]
public string VodID { get; set; }
[JsonProperty("playerType")]
public string PlayerType { get; set; }
}
}

View File

@@ -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.Api.GQL.GetVideoToken.Request
{
public class GetVideoTokenVideoPlaybackAccessToken
{
[JsonProperty("value")]
public string Value { get; set; }
[JsonProperty("signature")]
public string Signature { get; set; }
[JsonProperty("__typename")]
public string Typename { get; set; }
}
}

View File

@@ -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.Api.GQL.GetVideoToken.Request;
namespace VDownload.Sources.Twitch.Api.GQL.GetVideoToken.Response
{
public class GetVideoTokenData
{
[JsonProperty("videoPlaybackAccessToken")]
public GetVideoTokenVideoPlaybackAccessToken VideoPlaybackAccessToken { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Sources.Twitch.Api.GQL.GetVideoToken.Request;
namespace VDownload.Sources.Twitch.Api.GQL.GetVideoToken.Response
{
public class GetVideoTokenResponse
{
[JsonProperty("data")]
public GetVideoTokenData Data { get; set; }
[JsonProperty("extensions")]
public GetVideoTokenExtensions Extensions { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System;
namespace VDownload.Sources.Twitch.Api.Helix.GetVideos.Response
{
public class Data
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("stream_id")]
public object StreamId { get; set; }
[JsonProperty("user_id")]
public string UserId { get; set; }
[JsonProperty("user_login")]
public string UserLogin { get; set; }
[JsonProperty("user_name")]
public string UserName { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; }
[JsonProperty("published_at")]
public DateTime PublishedAt { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("thumbnail_url")]
public string ThumbnailUrl { get; set; }
[JsonProperty("viewable")]
public string Viewable { get; set; }
[JsonProperty("view_count")]
public int ViewCount { get; set; }
[JsonProperty("language")]
public string Language { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("duration")]
public string Duration { get; set; }
[JsonProperty("muted_segments")]
public List<MutedSegment> MutedSegments { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace VDownload.Sources.Twitch.Api.Helix.GetVideos.Response
{
public class GetVideosResponse
{
[JsonProperty("data")]
public List<Data> Data { get; set; }
[JsonProperty("pagination")]
public Pagination Pagination { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace VDownload.Sources.Twitch.Api.Helix.GetVideos.Response
{
public class MutedSegment
{
[JsonProperty("duration")]
public int Duration { get; set; }
[JsonProperty("offset")]
public int Offset { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using Newtonsoft.Json;
namespace VDownload.Sources.Twitch.Api.Helix.GetVideos.Response
{
public class Pagination
{
[JsonProperty("cursor")]
public string Cursor { get; set; }
}
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Services.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
{
public interface ITwitchApiService
{
Task<string> AuthValidate(byte[] token);
Task<GetVideoTokenResponse> GQLGetVideoToken(string id);
Task<GetVideosResponse> HelixGetVideos(string id, byte[] token);
Task<string> UsherGetVideoPlaylist(string id, string videoToken, string videoTokenSignature);
}
public class TwitchApiService : ITwitchApiService
{
#region SERVICES
private readonly TwitchApiAuthConfiguration _apiAuthConfiguration;
private readonly TwitchApiHelixConfiguration _apiHelixConfiguration;
private readonly TwitchApiGQLConfiguration _apiGQLConfiguration;
private readonly TwitchApiUsherConfiguration _apiUsherConfiguration;
private readonly IHttpClientService _httpClientService;
#endregion
#region CONSTRUCTORS
public TwitchApiService(TwitchConfiguration configuration, IHttpClientService httpClientService)
{
_apiAuthConfiguration = configuration.Api.Auth;
_apiHelixConfiguration = configuration.Api.Helix;
_apiGQLConfiguration = configuration.Api.GQL;
_apiUsherConfiguration = configuration.Api.Usher;
_httpClientService = httpClientService;
}
#endregion
#region PUBLIC METHODS
public async Task<string> AuthValidate(byte[] token)
{
Token tokenData = new Token(_apiAuthConfiguration.TokenSchema, token);
HttpRequest request = new HttpRequest(HttpMethodType.GET, _apiAuthConfiguration.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);
HttpRequest request = new HttpRequest(HttpMethodType.GET, _apiHelixConfiguration.Endpoints.GetVideos);
request.Query.Add("id", id);
request.Headers.Add("Authorization", tokenData.ToString());
request.Headers.Add("Client-Id", _apiHelixConfiguration.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,
Variables = new GetVideoTokenVariables
{
IsLive = false,
Login = string.Empty,
IsVod = true,
VodID = id,
PlayerType = "embed"
}
};
HttpRequest request = new HttpRequest(HttpMethodType.POST, _apiGQLConfiguration.Endpoint)
{
Body = requestBody,
};
request.Headers.Add("Client-Id", _apiGQLConfiguration.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);
HttpRequest request = new HttpRequest(HttpMethodType.GET, url);
request.Query.Add("token", videoToken);
request.Query.Add("sig", videoTokenSignature);
request.Query.Add("allow_source", true);
request.Query.Add("allow_audio_only", true);
request.Query.Add("platform", "web");
request.Query.Add("player_backend", "mediaplayer");
request.Query.Add("playlist_include_framerate", true);
request.Query.Add("supported_codecs", "av1,h264");
return await _httpClientService.SendRequestAsync(request);
}
#endregion
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\VDownload.Services\VDownload.Services.HttpClient\VDownload.Services.HttpClient.csproj" />
<ProjectReference Include="..\VDownload.Sources.Twitch.Configuration\VDownload.Sources.Twitch.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Sources.Twitch.Authentication.Models
{
internal class ValidateResponseFail
{
[JsonProperty("status", Required = Required.Always)]
public int Status { get; set; }
[JsonProperty("message", Required = Required.Always)]
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Sources.Twitch.Authentication.Models
{
internal class ValidateResponseSuccess
{
[JsonProperty("client_id", Required = Required.Always)]
public string ClientId { get; set; }
[JsonProperty("login", Required = Required.Always)]
public string Login { get; set; }
[JsonProperty("scopes", Required = Required.Always)]
public List<string> Scopes { get; set; }
[JsonProperty("user_id", Required = Required.Always)]
public string UserId { get; set; }
[JsonProperty("expires_in", Required = Required.Always)]
public int ExpiresIn { get; set; }
}
}

View File

@@ -0,0 +1,160 @@
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.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<byte[]?> GetToken();
Task SetToken(byte[] token);
Task DeleteToken();
Task<TwitchValidationResult> ValidateToken(byte[] token);
bool AuthenticationPageClosePredicate(string url);
#endregion
}
public class TwitchAuthenticationService : ITwitchAuthenticationService
{
#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; }
#endregion
#region CONSTRUCTORS
public TwitchAuthenticationService(TwitchConfiguration configuration, IHttpClientService httpClientService, IAuthenticationService authenticationService, IEncryptionService encryptionService)
{
_authenticationConfiguration = configuration.Authentication;
_apiAuthConfiguration = configuration.Api.Auth;
_httpClientService = httpClientService;
_authenticationService = authenticationService;
_encryptionService = encryptionService;
AuthenticationPageUrl = string.Format(_authenticationConfiguration.Url, _authenticationConfiguration.ClientId, _authenticationConfiguration.RedirectUrl, _authenticationConfiguration.ResponseType, string.Join(' ', _authenticationConfiguration.Scopes));
AuthenticationPageRedirectUrlRegex = _authenticationConfiguration.RedirectUrlRegex;
}
#endregion
#region PUBLIC METHODS
public async Task<byte[]?> GetToken()
{
await _authenticationService.Load();
byte[]? tokenEncrypted = _authenticationService.AuthenticationData.Twitch.Token;
if (tokenEncrypted is not null && tokenEncrypted.Length == 0)
{
tokenEncrypted = null;
}
if (tokenEncrypted is not null)
{
tokenEncrypted = _encryptionService.Decrypt(tokenEncrypted);
}
return tokenEncrypted;
}
public async Task SetToken(byte[] token)
{
Task loadTask = _authenticationService.Load();
byte[] tokenEncrypted = _encryptionService.Encrypt(token);
await loadTask;
_authenticationService.AuthenticationData.Twitch.Token = tokenEncrypted;
await _authenticationService.Save();
}
public async Task DeleteToken()
{
await _authenticationService.Load();
_authenticationService.AuthenticationData.Twitch.Token = null;
await _authenticationService.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);
try
{
ValidateResponseSuccess success = JsonConvert.DeserializeObject<ValidateResponseSuccess>(response);
return new TwitchValidationResult(success);
}
catch (JsonSerializationException)
{}
try
{
ValidateResponseFail fail = JsonConvert.DeserializeObject<ValidateResponseFail>(response);
return new TwitchValidationResult(fail);
}
catch (JsonSerializationException)
{}
throw new Exception(response);
}
public bool AuthenticationPageClosePredicate(string url)
{
bool close = url.StartsWith(_authenticationConfiguration.RedirectUrl);
return close;
}
#endregion
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Sources.Twitch.Authentication.Models;
namespace VDownload.Sources.Twitch.Authentication
{
public class TwitchValidationResult
{
#region PROPERTIES
public bool Success { get; private set; }
public int? FailStatusCode { get; private set; }
public string? FailMessage { get; private set; }
public TwitchValidationTokenData TokenData { get; private set; }
public DateTime ValidationDate { get; private set; }
#endregion
#region CONSTRUCTORS
private TwitchValidationResult()
{
ValidationDate = DateTime.Now;
}
internal TwitchValidationResult(ValidateResponseFail fail)
{
Success = false;
FailStatusCode = fail.Status;
FailMessage = fail.Message;
}
internal TwitchValidationResult(ValidateResponseSuccess success)
{
Success = true;
TokenData = new TwitchValidationTokenData(success);
}
#endregion
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Sources.Twitch.Authentication.Models;
namespace VDownload.Sources.Twitch.Authentication
{
public class TwitchValidationTokenData
{
#region PROPERTIES
public string ClientId { get; private set; }
public string Login { get; private set; }
public IEnumerable<string> Scopes { get; private set; }
public string UserId { get; private set; }
public DateTime ExpirationDate { get; private set; }
#endregion
#region CONSTRUCTORS
internal TwitchValidationTokenData(ValidateResponseSuccess response)
{
ClientId = response.ClientId;
Login = response.Login;
Scopes = response.Scopes;
UserId = response.UserId;
ExpirationDate = DateTime.Now.AddSeconds(response.ExpiresIn);
}
#endregion
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</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" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
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
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,36 @@
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
}
}

View File

@@ -0,0 +1,33 @@
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
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,33 @@
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
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,40 @@
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
}
}

View File

@@ -0,0 +1,35 @@
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 TwitchConfiguration
{
#region PROPERTIES
public TwitchApiConfiguration Api { get; protected set; }
public TwitchSearchConfiguration Search { get; protected set; }
public TwitchAuthenticationConfiguration Authentication { get; protected set; }
#endregion
#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
}
}

View File

@@ -0,0 +1,38 @@
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
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,165 @@
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
}
}

View File

@@ -0,0 +1,16 @@
<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>

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VDownload.Common;
namespace VDownload.Sources.Twitch
{
public abstract class TwitchPlaylist : Playlist
{
}
}

View File

@@ -0,0 +1,21 @@
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
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Sources.Twitch
{
public class TwitchVod : TwitchVideo
{
}
}

View File

@@ -0,0 +1,29 @@
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
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\VDownload.Common\VDownload.Common.csproj" />
</ItemGroup>
</Project>