325 lines
12 KiB
C#
325 lines
12 KiB
C#
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.GetClipToken.Response;
|
|
using VDownload.Sources.Twitch.Api.GQL.GetVideoToken.Response;
|
|
using VDownload.Sources.Twitch.Api.Helix.GetClips.Response;
|
|
using VDownload.Sources.Twitch.Api.Helix.GetUsers.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
|
|
{
|
|
}
|
|
|
|
|
|
|
|
public class TwitchSearchService : SourceSearchService, 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 PRIVATE METHODS
|
|
|
|
protected override IEnumerable<SearchRegexVideo> GetVideoRegexes()
|
|
{
|
|
return [
|
|
.._configurationService.Twitch.Search.Vod.Regexes.Select(x => new SearchRegexVideo
|
|
{
|
|
Regex = new Regex(x),
|
|
SearchFunction = async (id) => await GetVod(id)
|
|
}),
|
|
.._configurationService.Twitch.Search.Clip.Regexes.Select(x => new SearchRegexVideo
|
|
{
|
|
Regex = new Regex(x),
|
|
SearchFunction = async (id) => await GetClip(id)
|
|
}),
|
|
];
|
|
}
|
|
|
|
protected override IEnumerable<SearchRegexPlaylist> GetPlaylistRegexes()
|
|
{
|
|
return [
|
|
.. _configurationService.Twitch.Search.Channel.Regexes.Select(x => new SearchRegexPlaylist
|
|
{
|
|
Regex = new Regex(x),
|
|
SearchFunction = async (id, maxVideoCount) => await GetChannel(id, maxVideoCount)
|
|
}),
|
|
];
|
|
}
|
|
|
|
protected async Task<TwitchVod> GetVod(string id)
|
|
{
|
|
byte[] token = await GetToken();
|
|
|
|
GetVideosResponse info = await _apiService.HelixGetVideo(id, token);
|
|
|
|
if (info.Data is null)
|
|
{
|
|
throw CreateExceptionVodNotFound();
|
|
}
|
|
|
|
Api.Helix.GetVideos.Response.Data vodResponse = info.Data[0];
|
|
|
|
TwitchVod vod = await ParseVod(vodResponse);
|
|
|
|
return vod;
|
|
}
|
|
|
|
protected async Task<TwitchClip> GetClip(string id)
|
|
{
|
|
byte[] token = await GetToken();
|
|
|
|
GetClipsResponse info = await _apiService.HelixGetClip(id, token);
|
|
|
|
if (info.Data.Count == 0)
|
|
{
|
|
throw CreateExceptionClipNotFound();
|
|
}
|
|
|
|
Api.Helix.GetClips.Response.Data clipResponse = info.Data[0];
|
|
|
|
TwitchClip clip = await ParseClip(clipResponse);
|
|
|
|
return clip;
|
|
}
|
|
|
|
protected async Task<TwitchChannel> GetChannel(string id, int count)
|
|
{
|
|
byte[] token = await GetToken();
|
|
|
|
Api.Helix.GetUsers.Response.Data userResponse;
|
|
try
|
|
{
|
|
GetUsersResponse info = await _apiService.HelixGetUser(id, token);
|
|
if (info.Data.Count <= 0)
|
|
{
|
|
throw CreateExceptionChannelNotFound();
|
|
}
|
|
userResponse = info.Data[0];
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
// TODO: Add logging
|
|
throw;
|
|
}
|
|
|
|
TwitchChannel channel = new TwitchChannel
|
|
{
|
|
Id = userResponse.Id,
|
|
Name = userResponse.DisplayName,
|
|
Description = userResponse.Description,
|
|
Url = new Uri(string.Format(_configurationService.Twitch.Search.Channel.Url, id)),
|
|
};
|
|
|
|
List<Task<TwitchVod>> tasks = new List<Task<TwitchVod>>();
|
|
string? cursor = null;
|
|
List<Api.Helix.GetVideos.Response.Data> videosList;
|
|
count = count == 0 ? int.MaxValue : count;
|
|
int videos = 0;
|
|
do
|
|
{
|
|
videos = count > 100 ? 100 : count;
|
|
GetVideosResponse videosResponse = await _apiService.HelixGetUserVideos(channel.Id, token, videos, cursor);
|
|
|
|
if (!tasks.Any() && !videosResponse.Data.Any())
|
|
{
|
|
throw CreateExceptionEmptyPlaylist();
|
|
}
|
|
|
|
videosList = videosResponse.Data;
|
|
cursor = videosResponse.Pagination.Cursor;
|
|
tasks.AddRange(videosList.Select(ParseVod));
|
|
}
|
|
while (tasks.Count < count && videosList.Count == videos);
|
|
|
|
await Task.WhenAll(tasks);
|
|
|
|
channel.AddRange(tasks.Select(x => x.Result));
|
|
|
|
return channel;
|
|
}
|
|
|
|
protected async Task<TwitchVod> ParseVod(Api.Helix.GetVideos.Response.Data data)
|
|
{
|
|
Task<IEnumerable<TwitchVodStream>> streamsTask = GetVodStreams(data.Id);
|
|
|
|
Thumbnail thumbnailConfig = _configurationService.Twitch.Search.Vod.Thumbnail;
|
|
Regex liveThumbnailRegex = new Regex(_configurationService.Twitch.Search.Vod.LiveThumbnailUrlRegex);
|
|
Uri? thumbnail = null;
|
|
if (!liveThumbnailRegex.IsMatch(data.ThumbnailUrl))
|
|
{
|
|
thumbnail = new Uri(data.ThumbnailUrl.Replace("%{width}", thumbnailConfig.Width.ToString()).Replace("%{height}", thumbnailConfig.Height.ToString()));
|
|
}
|
|
TwitchVod vod = new TwitchVod
|
|
{
|
|
Id = data.Id,
|
|
Title = data.Title,
|
|
Description = data.Description,
|
|
Author = data.UserName,
|
|
PublishDate = data.PublishedAt,
|
|
Duration = ParseVodDuration(data.Duration),
|
|
Views = data.ViewCount,
|
|
ThumbnailUrl = thumbnail,
|
|
Url = new Uri(data.Url),
|
|
};
|
|
|
|
await streamsTask;
|
|
foreach (TwitchVodStream stream in streamsTask.Result)
|
|
{
|
|
vod.Streams.Add(stream);
|
|
}
|
|
|
|
return vod;
|
|
}
|
|
|
|
protected async Task<TwitchClip> ParseClip(Api.Helix.GetClips.Response.Data data)
|
|
{
|
|
Task<IEnumerable<TwitchClipStream>> streamsTask = GetClipStreams(data.Id);
|
|
|
|
TwitchClip clip = new TwitchClip
|
|
{
|
|
Id = data.Id,
|
|
Title = data.Title,
|
|
Author = data.BroadcasterName,
|
|
Creator = data.CreatorName,
|
|
PublishDate = data.CreatedAt,
|
|
Duration = TimeSpan.FromSeconds(Math.Round(data.Duration)),
|
|
Views = data.ViewCount,
|
|
ThumbnailUrl = new Uri(data.ThumbnailUrl),
|
|
Url = new Uri(data.Url),
|
|
};
|
|
|
|
await streamsTask;
|
|
foreach (TwitchClipStream stream in streamsTask.Result)
|
|
{
|
|
clip.Streams.Add(stream);
|
|
}
|
|
|
|
return clip;
|
|
}
|
|
|
|
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 async Task<IEnumerable<TwitchClipStream>> GetClipStreams(string id)
|
|
{
|
|
GetClipTokenResponse clipToken = await _apiService.GQLGetClipToken(id);
|
|
|
|
List<TwitchClipStream> streams = new List<TwitchClipStream>();
|
|
foreach (GetClipTokenVideoQuality streamData in clipToken.Data.Clip.VideoQualities)
|
|
{
|
|
TwitchClipStream stream = _videoStreamFactoryService.CreateClipStream();
|
|
stream.Name = $"{streamData.Quality}p{Math.Round(streamData.FrameRate)}";
|
|
stream.Height = int.Parse(streamData.Quality);
|
|
stream.FrameRate = streamData.FrameRate;
|
|
stream.Url = new Uri(streamData.SourceURL);
|
|
stream.Signature = clipToken.Data.Clip.PlaybackAccessToken.Signature;
|
|
stream.Token = clipToken.Data.Clip.PlaybackAccessToken.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() ?? throw CreateExceptionNotAuthenticated();
|
|
|
|
TwitchValidationResult validation = await _twitchAuthenticationService.ValidateToken(token);
|
|
if (!validation.Success)
|
|
{
|
|
throw CreateExceptionTokenValidationUnsuccessful();
|
|
}
|
|
return token;
|
|
}
|
|
|
|
protected MediaSearchException CreateExceptionNotAuthenticated() => new MediaSearchException("TwitchNotAuthenticated");
|
|
protected MediaSearchException CreateExceptionTokenValidationUnsuccessful() => new MediaSearchException("TwitchTokenValidationUnsuccessful");
|
|
protected MediaSearchException CreateExceptionChannelNotFound() => new MediaSearchException("TwitchChannelNotFound");
|
|
protected MediaSearchException CreateExceptionVodNotFound() => new MediaSearchException("TwitchVodNotFound");
|
|
protected MediaSearchException CreateExceptionClipNotFound() => new MediaSearchException("TwitchClipNotFound");
|
|
|
|
#endregion
|
|
}
|
|
}
|