Skip to content

Commit

Permalink
Merge pull request #8 from lay295/master
Browse files Browse the repository at this point in the history
sync
  • Loading branch information
superbonaci authored Nov 28, 2023
2 parents 47022d7 + fa95c9b commit 4d9f801
Show file tree
Hide file tree
Showing 17 changed files with 317 additions and 54 deletions.
6 changes: 4 additions & 2 deletions TwitchDownloaderCore/ChatUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public async Task UpdateAsync(IProgress<ProgressReport> progress, CancellationTo
int currentStep = 0;
int totalSteps = 2;
if (_updateOptions.CropBeginning || _updateOptions.CropEnding) totalSteps++;
if (_updateOptions.EmbedMissing || _updateOptions.ReplaceEmbeds) totalSteps++;
if (_updateOptions.OutputFormat is ChatFormat.Json or ChatFormat.Html
&& (_updateOptions.EmbedMissing || _updateOptions.ReplaceEmbeds)) totalSteps++;

currentStep++;
await UpdateVideoInfo(totalSteps, currentStep, progress, cancellationToken);
Expand All @@ -52,7 +53,8 @@ public async Task UpdateAsync(IProgress<ProgressReport> progress, CancellationTo
}

// If we are updating/replacing embeds
if (_updateOptions.EmbedMissing || _updateOptions.ReplaceEmbeds)
if (_updateOptions.OutputFormat is ChatFormat.Json or ChatFormat.Html
&& (_updateOptions.EmbedMissing || _updateOptions.ReplaceEmbeds))
{
currentStep++;
await UpdateEmbeds(currentStep, totalSteps, progress, cancellationToken);
Expand Down
13 changes: 7 additions & 6 deletions TwitchDownloaderCore/ClipDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,33 +86,34 @@ void DownloadProgressHandler(StreamCopyProgress streamProgress)
private async Task<string> GetDownloadUrl()
{
var listLinks = await TwitchHelper.GetClipLinks(downloadOptions.Id);
var clip = listLinks[0].data.clip;

if (listLinks[0].data.clip.playbackAccessToken is null)
if (clip.playbackAccessToken is null)
{
throw new NullReferenceException("Invalid Clip, deleted possibly?");
}

if (listLinks[0].data.clip.videoQualities is null || listLinks[0].data.clip.videoQualities.Count == 0)
if (clip.videoQualities is null || clip.videoQualities.Length == 0)
{
throw new NullReferenceException("Clip has no video qualities, deleted possibly?");
}

string downloadUrl = "";

foreach (var quality in listLinks[0].data.clip.videoQualities)
foreach (var quality in clip.videoQualities)
{
if (quality.quality + "p" + (quality.frameRate.ToString() == "30" ? "" : quality.frameRate.ToString()) == downloadOptions.Quality)
if (quality.quality + "p" + (Math.Round(quality.frameRate) == 30 ? "" : Math.Round(quality.frameRate).ToString("F0")) == downloadOptions.Quality)
{
downloadUrl = quality.sourceURL;
}
}

if (downloadUrl == "")
{
downloadUrl = listLinks[0].data.clip.videoQualities.First().sourceURL;
downloadUrl = clip.videoQualities.First().sourceURL;
}

return downloadUrl + "?sig=" + listLinks[0].data.clip.playbackAccessToken.signature + "&token=" + HttpUtility.UrlEncode(listLinks[0].data.clip.playbackAccessToken.value);
return downloadUrl + "?sig=" + clip.playbackAccessToken.signature + "&token=" + HttpUtility.UrlEncode(clip.playbackAccessToken.value);
}

private static async Task DownloadFileTaskAsync(string url, string destinationFile, int throttleKib, IProgress<StreamCopyProgress> progress, CancellationToken cancellationToken)
Expand Down
32 changes: 32 additions & 0 deletions TwitchDownloaderCore/Tools/ClipQualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using TwitchDownloaderCore.TwitchObjects.Gql;

namespace TwitchDownloaderCore.Tools
{
public class ClipQualityComparer : IComparer<VideoQuality>
{
public int Compare(VideoQuality x, VideoQuality y)
{
if (x is null)
{
if (y is null) return 0;
return -1;
}

if (y is null) return 1;

if (int.TryParse(x.quality, out var xQuality) | int.TryParse(y.quality, out var yQuality))
{
if (xQuality < yQuality) return 1;
if (xQuality > yQuality) return -1;

if (x.frameRate < y.frameRate) return 1;
if (x.frameRate > y.frameRate) return -1;
return 0;
}

return Math.Clamp(string.Compare(x.quality, y.quality, StringComparison.Ordinal), -1, 1) * -1;
}
}
}
2 changes: 1 addition & 1 deletion TwitchDownloaderCore/Tools/CommentTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public int Compare(Comment x, Comment y)
return -1;
}

if (y is null) return -1;
if (y is null) return 1;

// In the off chance that it causes problems with old chats, we will first compare offsets before comparing creation dates.
var xOffset = x.content_offset_seconds;
Expand Down
40 changes: 20 additions & 20 deletions TwitchDownloaderCore/Tools/FilenameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,6 @@ namespace TwitchDownloaderCore.Tools
{
public static class FilenameService
{
private static string[] GetTemplateSubfolders(ref string fullPath)
{
var returnString = fullPath.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
fullPath = returnString[^1];
Array.Resize(ref returnString, returnString.Length - 1);

for (var i = 0; i < returnString.Length; i++)
{
returnString[i] = RemoveInvalidFilenameChars(returnString[i]);
}

return returnString;
}

public static string GetFilename(string template, string title, string id, DateTime date, string channel, TimeSpan cropStart, TimeSpan cropEnd, string viewCount, string game)
{
var videoLength = cropEnd - cropStart;
Expand All @@ -30,8 +16,8 @@ public static string GetFilename(string template, string title, string id, DateT
.Replace("{title}", RemoveInvalidFilenameChars(title))
.Replace("{id}", id)
.Replace("{channel}", RemoveInvalidFilenameChars(channel))
.Replace("{date}", date.ToString("Mdyy"))
.Replace("{random_string}", Path.GetRandomFileName().Replace(".", ""))
.Replace("{date}", date.ToString("M-d-yy"))
.Replace("{random_string}", Path.GetRandomFileName().Remove(8)) // Remove the period
.Replace("{crop_start}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", cropStart))
.Replace("{crop_end}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", cropEnd))
.Replace("{length}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", videoLength))
Expand All @@ -40,25 +26,25 @@ public static string GetFilename(string template, string title, string id, DateT

if (template.Contains("{date_custom="))
{
var dateRegex = new Regex("{date_custom=\"(.*)\"}");
var dateRegex = new Regex("{date_custom=\"(.*?)\"}");
ReplaceCustomWithFormattable(stringBuilder, dateRegex, date);
}

if (template.Contains("{crop_start_custom="))
{
var cropStartRegex = new Regex("{crop_start_custom=\"(.*)\"}");
var cropStartRegex = new Regex("{crop_start_custom=\"(.*?)\"}");
ReplaceCustomWithFormattable(stringBuilder, cropStartRegex, cropStart);
}

if (template.Contains("{crop_end_custom="))
{
var cropEndRegex = new Regex("{crop_end_custom=\"(.*)\"}");
var cropEndRegex = new Regex("{crop_end_custom=\"(.*?)\"}");
ReplaceCustomWithFormattable(stringBuilder, cropEndRegex, cropEnd);
}

if (template.Contains("{length_custom="))
{
var lengthRegex = new Regex("{length_custom=\"(.*)\"}");
var lengthRegex = new Regex("{length_custom=\"(.*?)\"}");
ReplaceCustomWithFormattable(stringBuilder, lengthRegex, videoLength);
}

Expand All @@ -83,6 +69,20 @@ private static void ReplaceCustomWithFormattable(StringBuilder sb, Regex regex,
} while (true);
}

private static string[] GetTemplateSubfolders(ref string fullPath)
{
var returnString = fullPath.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
fullPath = returnString[^1];
Array.Resize(ref returnString, returnString.Length - 1);

for (var i = 0; i < returnString.Length; i++)
{
returnString[i] = RemoveInvalidFilenameChars(returnString[i]);
}

return returnString;
}

private static readonly char[] FilenameInvalidChars = Path.GetInvalidFileNameChars();

private static string RemoveInvalidFilenameChars(string filename) => filename.ReplaceAny(FilenameInvalidChars, '_');
Expand Down
8 changes: 6 additions & 2 deletions TwitchDownloaderCore/TwitchHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Threading;
using System.Threading.Tasks;
using TwitchDownloaderCore.Chat;
using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects;
using TwitchDownloaderCore.TwitchObjects.Api;
using TwitchDownloaderCore.TwitchObjects.Gql;
Expand Down Expand Up @@ -80,7 +81,7 @@ public static async Task<GqlClipResponse> GetClipInfo(object clipId)
return await response.Content.ReadFromJsonAsync<GqlClipResponse>();
}

public static async Task<List<GqlClipTokenResponse>> GetClipLinks(string clipId)
public static async Task<GqlClipTokenResponse[]> GetClipLinks(string clipId)
{
var request = new HttpRequestMessage()
{
Expand All @@ -91,7 +92,10 @@ public static async Task<List<GqlClipTokenResponse>> GetClipLinks(string clipId)
request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<GqlClipTokenResponse>>();

var gqlClipTokenResponses = await response.Content.ReadFromJsonAsync<GqlClipTokenResponse[]>();
Array.Sort(gqlClipTokenResponses[0].data.clip.videoQualities, new ClipQualityComparer());
return gqlClipTokenResponses;
}

public static async Task<GqlVideoSearchResponse> GetGqlVideos(string channelName, string cursor = "", int limit = 50)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System.Collections.Generic;

namespace TwitchDownloaderCore.TwitchObjects.Gql
namespace TwitchDownloaderCore.TwitchObjects.Gql
{
public class ClipToken
{
public string id { get; set; }
public PlaybackAccessToken playbackAccessToken { get; set; }
public List<VideoQuality> videoQualities { get; set; }
public VideoQuality[] videoQualities { get; set; }
public string __typename { get; set; }
}

Expand Down
155 changes: 155 additions & 0 deletions TwitchDownloaderTests/FilenameServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using TwitchDownloaderCore.Extensions;
using TwitchDownloaderCore.Tools;

namespace TwitchDownloaderTests
{
public class FilenameServiceTests
{
private static (string title, string id, DateTime date, string channel, TimeSpan cropStart, TimeSpan cropEnd, string viewCount, string game) GetExampleInfo() =>
("A Title", "abc123", new DateTime(1984, 11, 1, 9, 43, 21), "streamer8", new TimeSpan(0, 1, 2, 3, 4), new TimeSpan(0, 5, 6, 7, 8), "123456789", "A Game");

[Theory]
[InlineData("{title}", "A Title")]
[InlineData("{id}", "abc123")]
[InlineData("{channel}", "streamer8")]
[InlineData("{date}", "11-1-84")]
[InlineData("{crop_start}", "01-02-03")]
[InlineData("{crop_end}", "05-06-07")]
[InlineData("{length}", "04-04-04")]
[InlineData("{views}", "123456789")]
[InlineData("{game}", "A Game")]
[InlineData("{date_custom=\"s\"}", "1984-11-01T09_43_21")]
[InlineData("{crop_start_custom=\"hh\\-mm\\-ss\"}", "01-02-03")]
[InlineData("{crop_end_custom=\"hh\\-mm\\-ss\"}", "05-06-07")]
[InlineData("{length_custom=\"hh\\-mm\\-ss\"}", "04-04-04")]
public void CorrectlyGeneratesIndividualTemplates(string template, string expected)
{
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(template, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.Equal(expected, result);
}

[Theory]
[InlineData("[{date_custom=\"M-dd-yy\"}] {channel} - {title}", "[11-01-84] streamer8 - A Title")]
[InlineData("[{channel}] [{date_custom=\"M-dd-yy\"}] [{game}] {title} ({id}) - {views} views", "[streamer8] [11-01-84] [A Game] A Title (abc123) - 123456789 views")]
[InlineData("{title} by {channel} playing {game} on {date_custom=\"M dd, yyyy\"} for {length_custom=\"h'h 'm'm 's's'\"} with {views} views", "A Title by streamer8 playing A Game on 11 01, 1984 for 4h 4m 4s with 123456789 views")]
public void CorrectlyGeneratesLargeTemplates(string template, string expected)
{
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(template, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.Equal(expected, result);
}

[Fact]
public void CorrectlyInterpretsMultipleCustomParameters()
{
const string TEMPLATE = "{date_custom=\"yyyy\"} {date_custom=\"MM\"} {date_custom=\"dd\"} {crop_start_custom=\"hh\\-mm\\-ss\"} {crop_end_custom=\"hh\\-mm\\-ss\"} {length_custom=\"hh\\-mm\\-ss\"}";
const string EXPECTED = "1984 11 01 01-02-03 05-06-07 04-04-04";
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.Equal(EXPECTED, result);
}

[Fact]
public void CorrectlyGeneratesSubFolders_WithForwardSlash()
{
const string TEMPLATE = "{channel}/{date_custom=\"yyyy\"}/{date_custom=\"MM\"}/{date_custom=\"dd\"}/{title}.mp4";
var expected = Path.Combine("streamer8", "1984", "11", "01", "A Title.mp4");
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.Equal(expected, result);
}

[Fact]
public void CorrectlyGeneratesSubFolders_WithBackSlash()
{
const string TEMPLATE = "{channel}\\{date_custom=\"yyyy\"}\\{date_custom=\"MM\"}\\{date_custom=\"dd\"}\\{title}";
var expected = Path.Combine("streamer8", "1984", "11", "01", "A Title");
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.Equal(expected, result);
}

[Theory]
[InlineData("{title}")]
[InlineData("{id}")]
[InlineData("{channel}")]
[InlineData("{views}")]
[InlineData("{game}")]
public void CorrectlyReplacesInvalidCharactersForNonCustomTemplates(string template)
{
const char EXPECTED = '_';
var invalidChars = new string(Path.GetInvalidFileNameChars());

var result = FilenameService.GetFilename(template, invalidChars, invalidChars, default, invalidChars, default, default, invalidChars, invalidChars);

Assert.All(result, c => Assert.Equal(EXPECTED, c));
}

[Theory]
[InlineData("{date_custom=\"'")]
[InlineData("{crop_start_custom=\"'")]
[InlineData("{crop_end_custom=\"'")]
[InlineData("{length_custom=\"'")]
public void CorrectlyReplacesInvalidCharactersForCustomTemplates(string templateStart)
{
const char EXPECTED = '_';
var invalidChars = new string(Path.GetInvalidFileNameChars());
var template = string.Concat(
templateStart,
invalidChars.ReplaceAny("\r\n", EXPECTED), // newline chars are not supported by the custom parameters. This will not change.
"'\"}");

var result = FilenameService.GetFilename(template, invalidChars, invalidChars, default, invalidChars, default, default, invalidChars, invalidChars);

Assert.All(result, c => Assert.Equal(EXPECTED, c));
}

[Fact]
public void CorrectlyReplacesInvalidCharactersForSubFolders()
{
var invalidChars = new string(Path.GetInvalidPathChars());
var template = invalidChars + "\\{title}";
var expected = Path.Combine(new string('_', invalidChars.Length), "A Title");
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(template, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.Equal(expected, result);
}

[Fact]
public void RandomStringIsRandom()
{
const string TEMPLATE = "{random_string}";
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, cropStart, cropEnd, viewCount, game);
var result2 = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.NotEqual(result, result2);
}

[Fact]
public void DoesNotInterpretBogusTemplateParameter()
{
const string TEMPLATE = "{foobar}";
const string EXPECTED = "{foobar}";
var (title, id, date, channel, cropStart, cropEnd, viewCount, game) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, cropStart, cropEnd, viewCount, game);

Assert.Equal(EXPECTED, result);
}
}
}
5 changes: 4 additions & 1 deletion TwitchDownloaderWPF/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ protected override void OnStartup(StartupEventArgs e)
var windowsThemeService = new WindowsThemeService();
ThemeServiceSingleton = new ThemeService(this, windowsThemeService);

MainWindow = new MainWindow();
MainWindow = new MainWindow
{
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
MainWindow.Show();
}

Expand Down
Loading

0 comments on commit 4d9f801

Please sign in to comment.