Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

Commit

Permalink
Merge branch 'master' into less_cli_output
Browse files Browse the repository at this point in the history
  • Loading branch information
lay295 authored Dec 21, 2020
2 parents 4bd8d05 + 7455155 commit 11d0664
Show file tree
Hide file tree
Showing 29 changed files with 602 additions and 67 deletions.
8 changes: 8 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Copyright 2020 lay295

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,27 @@ https://www.youtube.com/watch?v=0W3MhfhnYjk
## Linux? MacOS?
Sorry the GUI version is only avaliable for Windows :( but there is a command line version avaliable.
This is a cross platform client that can do the main functions of the program without a GUI. It works on Windows and Linux, haven't tested it on MacOS though.

[Some documentation here](https://github.com/lay295/TwitchDownloader/blob/master/TwitchDownloaderCLI/README.md), for example, you could copy/paste this into a .bat file on Windows and you can download a VOD, download chat, then render it in a single go. I've never really made a command line utility before, so things may change in the future. If you're on Linux, make sure fontconfig and libfontconfig1 are installed. (apt-get install fontconfig libfontconfig1)
```
@echo off
set /p vodid="Enter VOD ID: "
TwitchDownloaderCLI -m VideoDownload --id %vodid% --ffmpeg-path "ffmpeg.exe" -o %vodid%.mp4
TwitchDownloaderCLI -m ChatDownload --id %vodid% -o %vodid%_chat.json
TwitchDownloaderCLI -m ChatRender -i %vodid%_chat.json -h 1080 -w 422 --framerate 30 --update-rate 0 --font-size 18 -o %vodid%_chat.mp4
```
```
=======

### Linux - Getting started

1. Go to releases and download the latest version for Linux
2. Extract the `TwitchDownloaderCLI`
3. Browse to where you extracted the file and give it executable rights by opening a terminal and executing the following:
```
sudo chmod +x TwitchDownloaderCLI
```
4. You can now start using the donwloader, for example:
```
TwitchDownloaderCLI -m VideoDownload --id <vod-id-here> -o out.mp4
```
For Arch Linux, there's an [AUR Package](https://aur.archlinux.org/packages/twitch-downloader-bin/)
2 changes: 2 additions & 0 deletions TwitchDownloaderCLI/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,7 @@ class Options
public bool DownloadFfmpeg { get; set; }
[Option("ffmpeg-path", HelpText = "Path to ffmpeg executable.")]
public string FfmpegPath { get; set; }
[Option("temp-path", Default = "", HelpText = "Path to temporary folder to use for cache.")]
public string TempFolder { get; set; }
}
}
7 changes: 7 additions & 0 deletions TwitchDownloaderCLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ private static void DownloadVideo(Options inputOptions)
downloadOptions.CropEnding = inputOptions.CropEndingTime == 0.0 ? false : true;
downloadOptions.CropEndingTime = inputOptions.CropEndingTime;
downloadOptions.FfmpegPath = inputOptions.FfmpegPath == null || inputOptions.FfmpegPath == "" ? ffmpegPath : Path.GetFullPath(inputOptions.FfmpegPath);
downloadOptions.TempFolder = inputOptions.TempFolder;

VideoDownloader videoDownloader = new VideoDownloader(downloadOptions);
Progress<ProgressReport> progress = new Progress<ProgressReport>();
Expand Down Expand Up @@ -223,6 +224,12 @@ private static void RenderChat(Options inputOptions)
renderOptions.InputArgs = inputOptions.InputArgs;
renderOptions.OutputArgs = inputOptions.OutputArgs;
renderOptions.FfmpegPath = inputOptions.FfmpegPath == null || inputOptions.FfmpegPath == "" ? ffmpegPath : Path.GetFullPath(inputOptions.FfmpegPath);
renderOptions.TempFolder = inputOptions.TempFolder;

if (renderOptions.GenerateMask && renderOptions.BackgroundColor.Alpha == 255)
{
Console.WriteLine("[WARNING] - Generate mask option has been selected with an opaque background. You most likely want to set a transparent background with --background-color \"#00000000\"");
}

ChatRenderer chatDownloader = new ChatRenderer(renderOptions);
Progress<ProgressReport> progress = new Progress<ProgressReport>();
Expand Down
3 changes: 3 additions & 0 deletions TwitchDownloaderCLI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Downloads ffmpeg and exits.
**-\-ffmpeg-path**
Path to ffmpeg executable.

**-\-temp-path**
Path to temporary folder for cache.

## Arguments for mode VideoDownload
**-u/-\-id**
The ID of the VOD to download, currently only accepts Integer IDs and will accept URLs in the future.
Expand Down
90 changes: 60 additions & 30 deletions TwitchDownloaderCore/ChatRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ namespace TwitchDownloaderCore
{
public class ChatRenderer
{
static ChatRenderOptions renderOptions;
ChatRenderOptions renderOptions;
static SKPaint imagePaint = new SKPaint() { IsAntialias = true, FilterQuality = SKFilterQuality.High };
static SKPaint emotePaint = new SKPaint() { IsAntialias = true, FilterQuality = SKFilterQuality.High };
static SKFontManager fontManager = SKFontManager.CreateDefault();
static ConcurrentDictionary<char, SKPaint> fallbackCache = new ConcurrentDictionary<char, SKPaint>();
static ConcurrentDictionary<int, SKPaint> fallbackCache = new ConcurrentDictionary<int, SKPaint>();

public ChatRenderer(ChatRenderOptions RenderOptions)
{
Expand All @@ -34,7 +34,7 @@ public ChatRenderer(ChatRenderOptions RenderOptions)

public async Task RenderVideoAsync(IProgress<ProgressReport> progress, CancellationToken cancellationToken)
{
string tempFolder = Path.Combine(Path.GetTempPath(), "TwitchDownloader");
string tempFolder = renderOptions.TempFolder == "" ? Path.Combine(Path.GetTempPath(), "TwitchDownloader") : Path.Combine(renderOptions.TempFolder, "TwitchDownloader");
string downloadFolder = Path.Combine(tempFolder, "Chat Render", Guid.NewGuid().ToString());
string cacheFolder = Path.Combine(tempFolder, "cache");
try
Expand Down Expand Up @@ -77,7 +77,9 @@ await Task.Run(() =>
continue;
if (comment.message.user_notice_params != null && comment.message.user_notice_params.msg_id != null)
{
if (comment.message.user_notice_params.msg_id != "highlighted-message" && comment.message.user_notice_params.msg_id != "")
if (comment.message.user_notice_params.msg_id != "highlighted-message" && comment.message.user_notice_params.msg_id != "sub" && comment.message.user_notice_params.msg_id != "resub" && comment.message.user_notice_params.msg_id != "subgift" && comment.message.user_notice_params.msg_id != "")
continue;
if (!renderOptions.SubMessages && (comment.message.user_notice_params.msg_id == "sub" || comment.message.user_notice_params.msg_id == "resub" || comment.message.user_notice_params.msg_id == "subgift"))
continue;
}

Expand All @@ -88,17 +90,28 @@ await Task.Run(() =>
List<SKBitmap> imageList = new List<SKBitmap>();
SKBitmap sectionImage = new SKBitmap(canvasSize.Width, canvasSize.Height);
int default_x = renderOptions.PaddingLeft;
bool accentMessage = false;

List<GifEmote> currentGifEmotes = new List<GifEmote>();
List<SKBitmap> emoteList = new List<SKBitmap>();
List<SKRect> emotePositionList = new List<SKRect>();
new SKCanvas(sectionImage).Clear(renderOptions.BackgroundColor);

if (renderOptions.Timestamp)
sectionImage = DrawTimestamp(sectionImage, imageList, messageFont, renderOptions, comment, canvasSize, ref drawPos, ref default_x);
sectionImage = DrawBadges(sectionImage, imageList, renderOptions, chatBadges, comment, canvasSize, ref drawPos);
sectionImage = DrawUsername(sectionImage, imageList, renderOptions, nameFont, comment.commenter.display_name, userColor, canvasSize, ref drawPos);
sectionImage = DrawMessage(sectionImage, imageList, renderOptions, currentGifEmotes, messageFont, emojiCache, chatEmotes, thirdPartyEmotes, cheerEmotes, comment, canvasSize, ref drawPos, ref default_x, emoteList, emotePositionList);
if (comment.message.user_notice_params != null && comment.message.user_notice_params.msg_id != null && (comment.message.user_notice_params.msg_id == "sub" || comment.message.user_notice_params.msg_id == "resub" || comment.message.user_notice_params.msg_id == "subgift"))
{
accentMessage = true;
drawPos.X += (int)(8 * renderOptions.EmoteScale);
default_x += (int)(8 * renderOptions.EmoteScale);
sectionImage = DrawMessage(sectionImage, imageList, renderOptions, currentGifEmotes, messageFont, emojiCache, chatEmotes, thirdPartyEmotes, cheerEmotes, comment, canvasSize, ref drawPos, ref default_x, emoteList, emotePositionList);
}
else
{
if (renderOptions.Timestamp)
sectionImage = DrawTimestamp(sectionImage, imageList, messageFont, renderOptions, comment, canvasSize, ref drawPos, ref default_x);
sectionImage = DrawBadges(sectionImage, imageList, renderOptions, chatBadges, comment, canvasSize, ref drawPos);
sectionImage = DrawUsername(sectionImage, imageList, renderOptions, nameFont, comment.commenter.display_name, userColor, canvasSize, ref drawPos);
sectionImage = DrawMessage(sectionImage, imageList, renderOptions, currentGifEmotes, messageFont, emojiCache, chatEmotes, thirdPartyEmotes, cheerEmotes, comment, canvasSize, ref drawPos, ref default_x, emoteList, emotePositionList);
}

int finalHeight = 0;
foreach (var img in imageList)
Expand All @@ -114,6 +127,9 @@ await Task.Run(() =>
img.Dispose();
}

if (accentMessage)
finalImageCanvas.DrawRect(renderOptions.PaddingLeft, 0, (int)(4 * renderOptions.EmoteScale), finalHeight - (int)Math.Floor(.4 * renderOptions.SectionHeight), new SKPaint() { Color = SKColor.Parse("#7b2cf2") });

string imagePath = Path.Combine(downloadFolder, Guid.NewGuid() + ".png");
finalComments.Add(new TwitchComment() { Section = imagePath, SecondsOffset = Double.Parse(comment.content_offset_seconds.ToString()), GifEmotes = currentGifEmotes, NormalEmotes = emoteList, NormalEmotesPositions = emotePositionList });
using (Stream s = File.OpenWrite(imagePath))
Expand All @@ -127,13 +143,14 @@ await Task.Run(() =>
}
finalImage.Dispose();
finalImageCanvas.Dispose();

int percent = (int)Math.Floor(((double)finalComments.Count / (double)chatJson.comments.Count) * 100);
CheckCancelation(cancellationToken, downloadFolder);
}
});

progress.Report(new ProgressReport() { reportType = ReportType.MessageInfo, data = "Rendering Video 0%" });
await Task.Run(() => RenderVideo(renderOptions, new Queue<TwitchComment>(finalComments.ToArray()), chatJson, progress), cancellationToken);
await Task.Run(() => RenderVideo(renderOptions, new Queue<TwitchComment>(finalComments.OrderBy(x => x.SecondsOffset)), chatJson, progress), cancellationToken);
progress.Report(new ProgressReport() { reportType = ReportType.Message, data = "Cleaning up..." });
Cleanup(downloadFolder);
}
Expand Down Expand Up @@ -532,7 +549,7 @@ public static SKBitmap DrawUsername(SKBitmap sectionImage, List<SKBitmap> imageL
SKPaint userPaint = nameFont;
if (userName.Any(isNotAscii))
{
userPaint = GetFallbackFont(userName.First(), renderOptions);
userPaint = GetFallbackFont((int)userName.First(), renderOptions);
userPaint.Color = userColor;
}
float textWidth = userPaint.MeasureText(userName + ":");
Expand Down Expand Up @@ -609,25 +626,28 @@ public static SKBitmap DrawMessage(SKBitmap sectionImage, List<SKBitmap> imageLi
{
if (Regex.Match(output, emojiRegex).Success)
{
Match m = Regex.Match(output, emojiRegex);
for (var k = 0; k < m.Value.Length; k += char.IsSurrogatePair(m.Value, k) ? 2 : 1)
MatchCollection matches = Regex.Matches(output, emojiRegex);
foreach (Match m in matches)
{
string codepoint = String.Format("{0:X4}", char.ConvertToUtf32(m.Value, k)).ToLower();
codepoint = codepoint.Replace("fe0f", "");
if (codepoint != "" && emojiCache.ContainsKey(codepoint))
for (var k = 0; k < m.Value.Length; k += char.IsSurrogatePair(m.Value, k) ? 2 : 1)
{
SKBitmap emojiBitmap = emojiCache[codepoint];
float emojiSize = (emojiBitmap.Width / 4) * (float)renderOptions.EmoteScale;
if (drawPos.X + (20 * renderOptions.EmoteScale) + 3 > canvasSize.Width)
sectionImage = AddImageSection(sectionImage, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, default_x);

using (SKCanvas sectionImageCanvas = new SKCanvas(sectionImage))
string codepoint = String.Format("{0:X4}", char.ConvertToUtf32(m.Value, k)).ToLower();
codepoint = codepoint.Replace("fe0f", "");
if (codepoint != "" && emojiCache.ContainsKey(codepoint))
{
float emojiLeft = (float)drawPos.X;
float emojiTop = (float)Math.Floor((renderOptions.SectionHeight - emojiSize) / 2.0);
SKRect emojiRect = new SKRect(emojiLeft, emojiTop, emojiLeft + emojiSize, emojiTop + emojiSize);
sectionImageCanvas.DrawBitmap(emojiBitmap, emojiRect, imagePaint);
drawPos.X += (int)Math.Floor(emojiSize + (int)Math.Floor(3 * renderOptions.EmoteScale));
SKBitmap emojiBitmap = emojiCache[codepoint];
float emojiSize = (emojiBitmap.Width / 4) * (float)renderOptions.EmoteScale;
if (drawPos.X + (20 * renderOptions.EmoteScale) + 3 > canvasSize.Width)
sectionImage = AddImageSection(sectionImage, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, default_x);

using (SKCanvas sectionImageCanvas = new SKCanvas(sectionImage))
{
float emojiLeft = (float)drawPos.X;
float emojiTop = (float)Math.Floor((renderOptions.SectionHeight - emojiSize) / 2.0);
SKRect emojiRect = new SKRect(emojiLeft, emojiTop, emojiLeft + emojiSize, emojiTop + emojiSize);
sectionImageCanvas.DrawBitmap(emojiBitmap, emojiRect, imagePaint);
drawPos.X += (int)Math.Floor(emojiSize + (int)Math.Floor(3 * renderOptions.EmoteScale));
}
}
}
}
Expand All @@ -644,7 +664,17 @@ public static SKBitmap DrawMessage(SKBitmap sectionImage, List<SKBitmap> imageLi

for (int j = 0; j < charList.Count; j++)
{
if (new StringInfo(charList[j].ToString()).LengthInTextElements == 0 || !renderFont.ContainsGlyphs(charList[j].ToString()))
if (char.IsHighSurrogate(charList[j]) && j+1 < charList.Count && char.IsLowSurrogate(charList[j+1]))
{
if (messageBuffer != "")
sectionImage = DrawText(sectionImage, messageBuffer, messageFont, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, true, default_x);
SKPaint fallbackFont = GetFallbackFont(char.ConvertToUtf32(charList[j], charList[j+1]), renderOptions);
fallbackFont.Color = renderOptions.MessageColor;
sectionImage = DrawText(sectionImage, charList[j].ToString() + charList[j+1].ToString(), fallbackFont, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, false, default_x);
messageBuffer = "";
j++;
}
else if (new StringInfo(charList[j].ToString()).LengthInTextElements == 0 || !renderFont.ContainsGlyphs(charList[j].ToString()))
{
if (messageBuffer != "")
sectionImage = DrawText(sectionImage, messageBuffer, messageFont, imageList, renderOptions, currentGifEmotes, canvasSize, ref drawPos, true, default_x);
Expand Down Expand Up @@ -790,7 +820,7 @@ private SKColor GenerateUserColor(SKColor userColor, SKColor background_color)

return userColor;
}
public static SKPaint GetFallbackFont(char input, ChatRenderOptions renderOptions)
public static SKPaint GetFallbackFont(int input, ChatRenderOptions renderOptions)
{
if (fallbackCache.ContainsKey(input))
return fallbackCache[input];
Expand Down
2 changes: 2 additions & 0 deletions TwitchDownloaderCore/Options/ChatRenderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,7 @@ public string OutputFileMask
public string InputArgs { get; set; }
public string OutputArgs { get; set; }
public string FfmpegPath { get; set; }
public string TempFolder { get; set; }
public bool SubMessages { get; set; }
}
}
1 change: 1 addition & 0 deletions TwitchDownloaderCore/Options/VideoDownloadOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ public class VideoDownloadOptions
public int DownloadThreads { get; set; }
public string Oauth { get; set; }
public string FfmpegPath { get; set; }
public string TempFolder { get; set; }
}
}
Loading

0 comments on commit 11d0664

Please sign in to comment.