Skip to content

Commit

Permalink
Add {channel_id}, {clipper} and {clipper_id} filename parameters (
Browse files Browse the repository at this point in the history
#1247)

* Add support for channel_id, clipper, and clipper_id filename params

* Update tests

* Implement new filename params support in GUI

* Add new filename params to settings window & update translations

* Add clipper to ChatRoot

* Support clip name params in chat updater page

* Fallback to web request if null

* Support new filename params in non-url mass downloaders

* Bump ChatRootVersion

* TaskData.Streamer -> TaskData.StreamerName

* Return streamer displayName instead of login

* Forgot to stage

* Store streamer/clipper logins in chatroot

* Use array instead of list

* Rename broadcaster comment variable

* Ignore case when comparing channel text
  • Loading branch information
ScrubN authored Dec 12, 2024
1 parent 8bb2605 commit 8bb9fc1
Show file tree
Hide file tree
Showing 33 changed files with 377 additions and 85 deletions.
67 changes: 37 additions & 30 deletions TwitchDownloaderCore.Tests/ToolTests/FilenameServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,31 @@ namespace TwitchDownloaderCore.Tests.ToolTests
{
public class FilenameServiceTests
{
private static (string title, string id, DateTime date, string channel, TimeSpan trimStart, TimeSpan trimEnd, int 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");
private static (string title, string id, DateTime date, string channel, string channelId, TimeSpan trimStart, TimeSpan trimEnd, int viewCount, string game, string clipper, string clipperId) GetExampleInfo() =>
("A Title", "abc123", new DateTime(1984, 11, 1, 9, 43, 21), "streamer8", "123456789", new TimeSpan(0, 1, 2, 3, 4), new TimeSpan(0, 5, 6, 7, 8), 123456789, "A Game", "viewer8", "987654321");

[Theory]
[InlineData("{title}", "A Title")]
[InlineData("{id}", "abc123")]
[InlineData("{channel}", "streamer8")]
[InlineData("{channel_id}", "123456789")]
[InlineData("{date}", "11-1-84")]
[InlineData("{trim_start}", "01-02-03")]
[InlineData("{trim_end}", "05-06-07")]
[InlineData("{length}", "04-04-04")]
[InlineData("{views}", "123456789")]
[InlineData("{game}", "A Game")]
[InlineData("{clipper}", "viewer8")]
[InlineData("{clipper_id}", "987654321")]
[InlineData("{date_custom=\"s\"}", "1984-11-01T09_43_21")]
[InlineData("{trim_start_custom=\"hh\\-mm\\-ss\"}", "01-02-03")]
[InlineData("{trim_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, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(template, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(template, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.Equal(expected, result);
}
Expand All @@ -36,9 +39,9 @@ public void CorrectlyGeneratesIndividualTemplates(string template, string expect
[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, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(template, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(template, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.Equal(expected, result);
}
Expand All @@ -48,9 +51,9 @@ public void CorrectlyInterpretsMultipleCustomParameters()
{
const string TEMPLATE = "{date_custom=\"yyyy\"} {date_custom=\"MM\"} {date_custom=\"dd\"} {trim_start_custom=\"hh\\-mm\\-ss\"} {trim_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, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.Equal(EXPECTED, result);
}
Expand All @@ -60,9 +63,9 @@ 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, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.Equal(expected, result);
}
Expand All @@ -72,25 +75,29 @@ 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, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.Equal(expected, result);
}

[Theory]
[InlineData("{title}", ""*:<>?|/\")]
[InlineData("{id}", ""*:<>?|/\")]
[InlineData("{channel}", ""*:<>?|/\")]
[InlineData("{game}", ""*:<>?|/\")]
public void CorrectlyReplacesInvalidCharactersForNonCustomTemplates(string template, string expected)
[InlineData("{title}")]
[InlineData("{id}")]
[InlineData("{channel}")]
[InlineData("{channel_id}")]
[InlineData("{clipper}")]
[InlineData("{clipper_id}")]
[InlineData("{game}")]
public void CorrectlyReplacesInvalidCharactersForNonCustomTemplates(string template)
{
const string INVALID_CHARS = "\"*:<>?|/\\";
const string EXPECTED = ""*:<>?|/\";

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

Assert.Equal(expected, result);
Assert.Equal(EXPECTED, result);
}

[Theory]
Expand All @@ -104,7 +111,7 @@ public void CorrectlyReplacesInvalidCharactersForCustomTemplates(string template
const string INVALID_CHARS = "\"*:<>?|/\\\\";
var template = templateStart + INVALID_CHARS + "'\"}";

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

Assert.Equal(EXPECTED, result);
}
Expand All @@ -116,9 +123,9 @@ public void CorrectlyReplacesInvalidCharactersForSubFolders()
const string FULL_WIDTH_CHARS = ""*:<>?|";
const string TEMPLATE = INVALID_CHARS + "\\{title}";
var expected = Path.Combine(FULL_WIDTH_CHARS, "A Title");
var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.Equal(expected, result);
}
Expand All @@ -127,10 +134,10 @@ public void CorrectlyReplacesInvalidCharactersForSubFolders()
public void RandomStringIsRandom()
{
const string TEMPLATE = "{random_string}";
var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result2 = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);
var result2 = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.NotEqual(result, result2);
}
Expand All @@ -140,20 +147,20 @@ public void DoesNotInterpretBogusTemplateParameter()
{
const string TEMPLATE = "{foobar}";
const string EXPECTED = "{foobar}";
var (title, id, date, channel, trimStart, trimEnd, viewCount, game) = GetExampleInfo();
var (title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId) = GetExampleInfo();

var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, trimStart, trimEnd, viewCount, game);
var result = FilenameService.GetFilename(TEMPLATE, title, id, date, channel, channelId, trimStart, trimEnd, viewCount, game, clipper, clipperId);

Assert.Equal(EXPECTED, result);
}

[Fact]
public void GetFilenameDoesNotThrow_WhenNullOrDefaultInput()
{
const string TEMPLATE = "{title}_{id}_{date}_{channel}_{trim_start}_{trim_end}_{length}_{views}_{game}_{date_custom=\"s\"}_{trim_start_custom=\"hh\\-mm\\-ss\"}_{trim_end_custom=\"hh\\-mm\\-ss\"}_{length_custom=\"hh\\-mm\\-ss\"}";
const string EXPECTED = "__1-1-01__00-00-00_00-00-00_00-00-00_0__0001-01-01T00_00_00_00-00-00_00-00-00_00-00-00";
const string TEMPLATE = "{title}_{id}_{date}_{channel}_{channel_id}_{trim_start}_{trim_end}_{length}_{views}_{game}_{clipper}_{clipper_id}_{date_custom=\"s\"}_{trim_start_custom=\"hh\\-mm\\-ss\"}_{trim_end_custom=\"hh\\-mm\\-ss\"}_{length_custom=\"hh\\-mm\\-ss\"}";
const string EXPECTED = "__1-1-01___00-00-00_00-00-00_00-00-00_0____0001-01-01T00_00_00_00-00-00_00-00-00_00-00-00";

var result = FilenameService.GetFilename(TEMPLATE, default, default, default, default, default, default, default, default);
var result = FilenameService.GetFilename(TEMPLATE, default, default, default, default, default, default, default, default, default, default, default);

Assert.Equal(EXPECTED, result);
}
Expand Down
40 changes: 33 additions & 7 deletions TwitchDownloaderCore/Chat/ChatJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,25 +192,51 @@ private static async Task UpgradeChatJson(ChatRoot chatRoot)

if (chatRoot.streamer is null)
{
var broadcaster = new Lazy<Comment>(() =>
chatRoot.comments
.Where(x => x.message.user_badges != null)
.FirstOrDefault(x => x.message.user_badges.Any(b => b._id.Equals("broadcaster"))));
var broadcasterComment = chatRoot.comments
.Where(x => x.message.user_badges != null)
.FirstOrDefault(x => x.message.user_badges.Any(b => b._id.Equals("broadcaster")));

if (!int.TryParse(chatRoot.video.user_id, out var assumedId))
{
if (chatRoot.comments.FirstOrDefault(x => int.TryParse(x.channel_id, out assumedId)) is null)
{
if (!int.TryParse(broadcaster.Value?.commenter._id, out assumedId))
if (!int.TryParse(broadcasterComment?.commenter._id, out assumedId))
{
assumedId = 0;
}
}
}

var assumedName = chatRoot.video.user_name ?? broadcaster.Value?.commenter.display_name ?? await TwitchHelper.GetStreamerName(assumedId);
var assumedName = chatRoot.video.user_name ?? broadcasterComment?.commenter.display_name;
var assumedLogin = broadcasterComment?.commenter.name;

chatRoot.streamer = new Streamer { id = assumedId, name = assumedName };
if ((assumedName is null || assumedLogin is null) && assumedId != 0)
{
try
{
var userInfo = await TwitchHelper.GetUserInfo(new[] { assumedId.ToString() });
assumedName ??= userInfo.data.users.FirstOrDefault()?.displayName;
assumedLogin ??= userInfo.data.users.FirstOrDefault()?.login;
}
catch { /* ignored */ }
}

chatRoot.streamer = new Streamer
{
name = assumedName,
login = assumedLogin,
id = assumedId
};
}

if (chatRoot.streamer.login is null && chatRoot.streamer.id != 0)
{
try
{
var userInfo = await TwitchHelper.GetUserInfo(new[] { chatRoot.streamer.id.ToString() });
chatRoot.streamer.login = userInfo.data.users.FirstOrDefault()?.login;
}
catch { /* ignored */ }
}

if (chatRoot.video.user_name is not null)
Expand Down
8 changes: 8 additions & 0 deletions TwitchDownloaderCore/ChatDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF
}

chatRoot.streamer.name = videoInfoResponse.data.video.owner.displayName;
chatRoot.streamer.login = videoInfoResponse.data.video.owner.login;
chatRoot.streamer.id = int.Parse(videoInfoResponse.data.video.owner.id);
chatRoot.video.description = videoInfoResponse.data.video.description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd();
chatRoot.video.title = videoInfoResponse.data.video.title;
Expand Down Expand Up @@ -371,7 +372,14 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF

videoId = clipInfoResponse.data.clip.video.id;
chatRoot.streamer.name = clipInfoResponse.data.clip.broadcaster.displayName;
chatRoot.streamer.login = clipInfoResponse.data.clip.broadcaster.login;
chatRoot.streamer.id = int.Parse(clipInfoResponse.data.clip.broadcaster.id);
chatRoot.clipper = new Clipper
{
name = clipInfoResponse.data.clip.curator.displayName,
login = clipInfoResponse.data.clip.curator.login,
id = int.Parse(clipInfoResponse.data.clip.curator.id),
};
chatRoot.video.title = clipInfoResponse.data.clip.title;
chatRoot.video.created_at = clipInfoResponse.data.clip.createdAt;
chatRoot.video.start = (double)clipInfoResponse.data.clip.videoOffsetSeconds + (downloadOptions.TrimBeginning ? downloadOptions.TrimBeginningTime : 0);
Expand Down
7 changes: 7 additions & 0 deletions TwitchDownloaderCore/ChatUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ private async Task UpdateVideoInfo(int totalSteps, int currentStep, Cancellation
return;
}

chatRoot.clipper ??= new Clipper
{
name = clipInfo.curator.displayName,
login = clipInfo.curator.login,
id = int.Parse(clipInfo.curator.id),
};

chatRoot.video.title = clipInfo.title;
chatRoot.video.created_at = clipInfo.createdAt;
chatRoot.video.length = clipInfo.durationSeconds;
Expand Down
6 changes: 5 additions & 1 deletion TwitchDownloaderCore/Tools/FilenameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ namespace TwitchDownloaderCore.Tools
{
public static class FilenameService
{
public static string GetFilename(string template, [AllowNull] string title, [AllowNull] string id, DateTime date, [AllowNull] string channel, TimeSpan trimStart, TimeSpan trimEnd, long viewCount, [AllowNull] string game)
public static string GetFilename(string template, [AllowNull] string title, [AllowNull] string id, DateTime date, [AllowNull] string channel, [AllowNull] string channelId, TimeSpan trimStart, TimeSpan trimEnd, long viewCount,
[AllowNull] string game, [AllowNull] string clipper = null, [AllowNull] string clipperId = null)
{
var videoLength = trimEnd - trimStart;

var stringBuilder = new StringBuilder(template)
.Replace("{title}", ReplaceInvalidFilenameChars(title))
.Replace("{id}", ReplaceInvalidFilenameChars(id))
.Replace("{channel}", ReplaceInvalidFilenameChars(channel))
.Replace("{channel_id}", ReplaceInvalidFilenameChars(channelId))
.Replace("{clipper}", ReplaceInvalidFilenameChars(clipper))
.Replace("{clipper_id}", ReplaceInvalidFilenameChars(clipperId))
.Replace("{date}", date.ToString("M-d-yy"))
.Replace("{random_string}", Path.GetRandomFileName().Remove(8)) // Remove the period
.Replace("{trim_start}", TimeSpanHFormat.ReusableInstance.Format(@"HH\-mm\-ss", trimStart))
Expand Down
Loading

0 comments on commit 8bb9fc1

Please sign in to comment.