diff --git a/README.md b/README.md
index 37a0ca6d..f6e917e7 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,8 @@ https://user-images.githubusercontent.com/1060681/197653099-c3fd12c2-f03a-4580-8
## What can it do?
- Download Twitch VODs
- Download Twitch Clips
-- Download chat for VODS and Clips, in either a [JSON with all the information](https://pastebin.com/raw/YDgRe6X4) or a [simple text file](https://pastebin.com/raw/016azeQX)
+- Download chat for VODS and Clips, in either a [JSON with all the original information](https://pastebin.com/raw/YDgRe6X4), a browser HTML file, or a [plain text file](https://pastebin.com/raw/016azeQX)
+- Update the contents of a previously generated JSON chat file with an option to save as another format
- Use a previously generated JSON chat file to render the chat with FFZ, BTTV and 7TV support (including GIFS)
# GUI
@@ -52,61 +53,73 @@ Check twitch-downloader-gui on [github](https://github.com/mohad12211/twitch-dow
## MacOS?
-No GUI is avaiable for MacOS yet :(
+No GUI is available for MacOS yet :(
# CLI
-The CLI is cross platform and performs the main functions of the program. It works on Windows, Linux, and MacOS.*
+### [See the full CLI documentation here](TwitchDownloaderCLI/README.md).
-*Only Intel Macs have been tested
+The CLI is cross-platform and implements the main functions of the program. It works on Windows, Linux, and MacOS*.
-### [CLI Documentation here](TwitchDownloaderCLI/README.md).
+*Only Intel Macs have been tested
-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)`.
-
-For example, you could copy/paste this into a `.bat` file on Windows, to download a VOD, chat, and then render in a single go.
-```
+With the CLI, it is possible to automate video processing using external scripts. For example, you could copy-paste the following code into a `.bat` file on Windows to download a VOD and its chat, and then render the chat, all from a single input.
+```bat
@echo off
set /p vodid="Enter VOD ID: "
TwitchDownloaderCLI.exe videodownload --id %vodid% --ffmpeg-path "ffmpeg.exe" -o %vodid%.mp4
-TwitchDownloaderCLI.exe chatdownload --id %vodid% -o %vodid%_chat.json
+TwitchDownloaderCLI.exe chatdownload --id %vodid% -o %vodid%_chat.json -E
TwitchDownloaderCLI.exe 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](https://github.com/lay295/TwitchDownloader/releases/) and download the latest version for Linux.
-2. Extract `TwitchDownloaderCLI`
-3. Browse to where you extracted the file and give it executable rights in Terminal:
+## Windows - Getting started
+
+1. Go to [Releases](https://github.com/lay295/TwitchDownloader/releases/) and download the latest version for Windows or [build from source](#building-from-source).
+2. Extract `TwitchDownloaderCLI.exe`.
+3. Browse to where you extracted the file in the terminal.
+4. If you do not have ffmpeg, you can install it via [Chocolatey package manager](https://community.chocolatey.org/), or you can get it as a standalone file from [ffmpeg.org](https://ffmpeg.org/download.html) or by using TwitchDownloaderCLI:
+```
+TwitchDownloaderCLI.exe ffmpeg --download
+```
+5. You can now start using the downloader, for example:
+```
+TwitchDownloaderCLI.exe videodownload --id -o out.mp4
+```
+
+## Linux – Getting started
+
+1. Ensure both `fontconfig` and `libfontconfig1` are installed. `apt-get install fontconfig libfontconfig1` on Ubuntu.
+2. Go to [Releases](https://github.com/lay295/TwitchDownloader/releases/) and download the latest binary for Linux, grab the [AUR Package](https://aur.archlinux.org/packages/twitch-downloader-bin/) for Arch Linux, or [build from source](#building-from-source).
+3. Extract `TwitchDownloaderCLI`.
+4. Browse to where you extracted the file and give it executable rights in the terminal:
```
sudo chmod +x TwitchDownloaderCLI
```
-4. If you do not have ffmpeg, you should install it via your distro package manager, however you can also get it as a standalone file from [ffmpeg.org](https://ffmpeg.org/download.html) or by using TwitchDownloaderCLI:
+5. a) If you do not have ffmpeg, you should install it via your distro package manager, however you can also get it as a standalone file from [ffmpeg.org](https://ffmpeg.org/download.html) or by using TwitchDownloaderCLI:
```
./TwitchDownloaderCLI ffmpeg --download
```
-If downloaded as a standalone file, you must also give it executable rights with:
+5. b) If downloaded as a standalone file, you must also give it executable rights with:
```
sudo chmod +x ffmpeg
```
-5. You can now start using the downloader, for example:
+6. You can now start using the downloader, for example:
```
./TwitchDownloaderCLI videodownload --id -o out.mp4
```
-For Arch Linux, there's an [AUR Package](https://aur.archlinux.org/packages/twitch-downloader-bin/)
-### MacOS – Getting started
-1. Go to [Releases](https://github.com/lay295/TwitchDownloader/releases/) and download the latest version for MacOS.
-2. Extract `TwitchDownloaderCLI`
-3. Browse to where you extracted the file and give it executable rights in Terminal:
+## MacOS – Getting started
+1. Go to [Releases](https://github.com/lay295/TwitchDownloader/releases/) and download the latest binary for MacOS or [build from source](#building-from-source).
+2. Extract `TwitchDownloaderCLI`.
+3. Browse to where you extracted the file and give it executable rights in the terminal:
```
chmod +x TwitchDownloaderCLI
```
-4. If you do not have ffmpeg, you can install it via [Homebrew package manager](https://brew.sh/), or you can get it as a standalone file from [ffmpeg.org](https://ffmpeg.org/download.html) or by using TwitchDownloaderCLI:
+4. a) If you do not have ffmpeg, you can install it via [Homebrew package manager](https://brew.sh/), or you can get it as a standalone file from [ffmpeg.org](https://ffmpeg.org/download.html) or by using TwitchDownloaderCLI:
```
./TwitchDownloaderCLI ffmpeg --download
```
-If downloaded as a standalone file, you must also give it executable rights with:
+4. b) If downloaded as a standalone file, you must also give it executable rights with:
```
chmod +x ffmpeg
```
@@ -115,6 +128,44 @@ chmod +x ffmpeg
./TwitchDownloaderCLI videodownload --id -o out.mp4
```
+# Building from source
+
+## Requirements
+
+- [.NET 6.0.x SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
+
+## Build Instructions
+
+1. Clone the repository:
+```
+git clone https://github.com/lay295/TwitchDownloader.git
+```
+2. Navigate to the solution folder:
+```
+cd TwitchDownloader
+```
+3. Restore the solution:
+```
+dotnet restore
+```
+4. a) Build the GUI:
+```
+dotnet publish TwitchDownloaderWPF -p:PublishProfile=Windows -p:DebugType=None -p:DebugSymbols=false
+```
+4. b) Build the CLI:
+```
+dotnet publish TwitchDownloaderCLI -p:PublishProfile= -p:DebugType=None -p:DebugSymbols=false
+```
+- Applicable Profiles: `Windows`, `Linux`, `LinuxAlpine`, `LinuxArm`, `MacOS`
+5. a) Navigate to the GUI build folder:
+```
+cd TwitchDownloaderWPF/bin/Release/net6.0-windows/publish/win-x64
+```
+5. b) Navigate to the CLI build folder:
+```
+cd TwitchDownloaderCLI/bin/Release/net6.0/publish
+```
+
# License
[MIT](./LICENSE.txt)
\ No newline at end of file
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs
index 70cf84fc..daeeb335 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadArgs.cs
@@ -10,7 +10,7 @@ public class ChatDownloadArgs
[Option('u', "id", Required = true, HelpText = "The ID of the VOD or clip to download that chat of.")]
public string Id { get; set; }
- [Option('o', "output", Required = true, HelpText = "Path to output file. File extension will be used to determine download type. Valid extensions are json, html, and txt.")]
+ [Option('o', "output", Required = true, HelpText = "Path to output file. File extension will be used to determine download type. Valid extensions are: json, html, and txt.")]
public string OutputFile { get; set; }
[Option('b', "beginning", HelpText = "Time in seconds to crop beginning.")]
@@ -31,13 +31,13 @@ public class ChatDownloadArgs
[Option("stv", Default = true, HelpText = "Enable 7tv embedding in chat download. Requires -E / --embed-images!")]
public bool? StvEmotes { get; set; }
- [Option("timestamp", Default = false, HelpText = "Enable timestamps for .txt chat downloads.")]
- public bool Timestamp { get; set; }
-
- [Option("timestamp-format", Default = TimestampFormat.Relative, HelpText = "Sets the timestamp format for .txt chat logs. Valid values are Utc, Relative, and None")]
+ [Option("timestamp-format", Default = TimestampFormat.Relative, HelpText = "Sets the timestamp format for .txt chat logs. Valid values are: Utc, Relative, and None")]
public TimestampFormat TimeFormat { get; set; }
[Option("chat-connections", Default = 4, HelpText = "Number of downloading connections for chat")]
public int ChatConnections { get; set; }
+
+ [Option("temp-path", Default = "", HelpText = "Path to temporary folder to use for cache.")]
+ public string TempFolder { get; set; }
}
}
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadUpdaterArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadUpdaterArgs.cs
deleted file mode 100644
index b5466abf..00000000
--- a/TwitchDownloaderCLI/Modes/Arguments/ChatDownloadUpdaterArgs.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using CommandLine;
-
-namespace TwitchDownloaderCLI.Modes.Arguments
-{
-
- [Verb("chatupdate", HelpText = "Updates the embeded emotes, badges, and bits of a chat download.")]
- public class ChatDownloadUpdaterArgs
- {
- [Option('i', "input", Required = true, HelpText = "Path to input file. Valid extensions are json")]
- public string InputFile { get; set; }
-
- [Option('o', "output", Required = true, HelpText = "Path to output file. Extension should match the input.")]
- public string OutputFile { get; set; }
-
- [Option('E', "embed-missing", Default = true, HelpText = "Embed missing emotes, badges, and bits. Already embedded images will be untouched")]
- public bool EmbedMissing { get; set; }
-
- [Option('U', "update-old", Default = false, HelpText = "Update old emotes, badges, and bits to the current. All embedded images will be overwritten")]
- public bool UpdateOldEmbeds { get; set; }
-
- [Option("bttv", Default = true, HelpText = "Enable BTTV embedding in chat download.")]
- public bool BttvEmotes { get; set; }
-
- [Option("ffz", Default = true, HelpText = "Enable FFZ embedding in chat download.")]
- public bool FfzEmotes { get; set; }
-
- [Option("stv", Default = true, HelpText = "Enable 7tv embedding in chat download.")]
- public bool StvEmotes { get; set; }
-
- [Option("temp-path", Default = "", HelpText = "Path to temporary folder to use for cache.")]
- public string TempFolder { get; set; }
- }
-}
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs
index b83694d8..f44e4229 100644
--- a/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs
+++ b/TwitchDownloaderCLI/Modes/Arguments/ChatRenderArgs.cs
@@ -69,10 +69,10 @@ public class ChatRenderArgs
[Option("update-rate", Default = 0.2, HelpText = "Time in seconds to update chat render output.")]
public double UpdateRate { get; set; }
- [Option("input-args", Default = "-framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt {pix_fmt} -video_size {width}x{height} -i -", HelpText = "Input (pass 1) arguments for ffmpeg chat render.")]
+ [Option("input-args", Default = "-framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt {pix_fmt} -video_size {width}x{height} -i -", HelpText = "Input arguments for ffmpeg chat render.")]
public string InputArgs { get; set; }
- [Option("output-args", Default = "-c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p \"{save_path}\"", HelpText = "Output (pass 2) arguments for ffmpeg chat render.")]
+ [Option("output-args", Default = "-c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p \"{save_path}\"", HelpText = "Output arguments for ffmpeg chat render.")]
public string OutputArgs { get; set; }
[Option("ignore-users", Default = "", HelpText = "List of usernames to ignore when rendering, separated by commas.")]
@@ -81,7 +81,7 @@ public class ChatRenderArgs
[Option("badge-filter", Default = 0, HelpText = "Bitmask of types of Chat Badges to filter out. Add the numbers of the types of badges you want to filter. For example, 6 = no broadcaster or moderator badges.\r\nKey: Other = 1, Broadcaster = 2, Moderator = 4, VIP = 8, Subscriber = 16, Predictions = 32, NoAudio/NoVideo = 64, PrimeGaming = 128")]
public int BadgeFilterMask { get; set; }
- [Option("offline", Default = false, HelpText = "Render completely offline, using only resources embedded emotes, badges, and bits in the input json.")]
+ [Option("offline", Default = false, HelpText = "Render completely offline using only embedded emotes, badges, and bits from the input json.")]
public bool Offline { get; set; }
[Option("ffmpeg-path", HelpText = "Path to ffmpeg executable.")]
diff --git a/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs b/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs
new file mode 100644
index 00000000..9b50cfdf
--- /dev/null
+++ b/TwitchDownloaderCLI/Modes/Arguments/ChatUpdateArgs.cs
@@ -0,0 +1,43 @@
+using CommandLine;
+using TwitchDownloaderCore.Options;
+
+namespace TwitchDownloaderCLI.Modes.Arguments
+{
+
+ [Verb("chatupdate", HelpText = "Updates the embeded emotes, badges, bits, and crops of a chat download and/or converts a JSON chat to another format.")]
+ public class ChatUpdateArgs
+ {
+ [Option('i', "input", Required = true, HelpText = "Path to input file. Valid extensions are: json.")]
+ public string InputFile { get; set; }
+
+ [Option('o', "output", Required = true, HelpText = "Path to output file. File extension will be used to determine new chat type. Valid extensions are: json, html, and txt.")]
+ public string OutputFile { get; set; }
+
+ [Option('E', "embed-missing", Default = false, HelpText = "Embed missing emotes, badges, and cheermotes. Already embedded images will be untouched.")]
+ public bool EmbedMissing { get; set; }
+
+ [Option('R', "replace-embeds", Default = false, HelpText = "Replace all embedded emotes, badges, and cheermotes in the file. All embedded images will be overwritten!")]
+ public bool ReplaceEmbeds { get; set; }
+
+ [Option('b', "beginning", Default = -1, HelpText = "New time in seconds for chat beginning. Comments may be added but not removed. -1 = No crop.")]
+ public int CropBeginningTime { get; set; }
+
+ [Option('e', "ending", Default = -1, HelpText = "New time in seconds for chat ending. Comments may be added but not removed. -1 = No crop.")]
+ public int CropEndingTime { get; set; }
+
+ [Option("bttv", Default = true, HelpText = "Enable BTTV embedding in chat download.")]
+ public bool? BttvEmotes { get; set; }
+
+ [Option("ffz", Default = true, HelpText = "Enable FFZ embedding in chat download.")]
+ public bool? FfzEmotes { get; set; }
+
+ [Option("stv", Default = true, HelpText = "Enable 7TV embedding in chat download.")]
+ public bool? StvEmotes { get; set; }
+
+ [Option("timestamp-format", Default = TimestampFormat.Relative, HelpText = "Sets the timestamp format for .txt chat logs. Valid values are: Utc, Relative, and None")]
+ public TimestampFormat TimeFormat { get; set; }
+
+ [Option("temp-path", Default = "", HelpText = "Path to temporary folder to use for cache.")]
+ public string TempFolder { get; set; }
+ }
+}
diff --git a/TwitchDownloaderCLI/Modes/DownloadChat.cs b/TwitchDownloaderCLI/Modes/DownloadChat.cs
index 47efc3f0..e6a0a720 100644
--- a/TwitchDownloaderCLI/Modes/DownloadChat.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadChat.cs
@@ -12,7 +12,7 @@ internal class DownloadChat
{
internal static void Download(ChatDownloadArgs inputOptions)
{
- if (inputOptions.Id == string.Empty)
+ if (string.IsNullOrWhiteSpace(inputOptions.Id))
{
Console.WriteLine("[ERROR] - Invalid ID, unable to parse.");
Environment.Exit(1);
@@ -22,24 +22,23 @@ internal static void Download(ChatDownloadArgs inputOptions)
{
DownloadFormat = Path.GetExtension(inputOptions.OutputFile)!.ToLower() switch
{
- ".json" => DownloadFormat.Json,
- ".html" => DownloadFormat.Html,
- ".htm" => DownloadFormat.Html,
- _ => DownloadFormat.Text
+ ".html" or ".htm" => ChatFormat.Html,
+ ".json" => ChatFormat.Json,
+ _ => ChatFormat.Text
},
Id = inputOptions.Id,
CropBeginning = inputOptions.CropBeginningTime > 0.0,
CropBeginningTime = inputOptions.CropBeginningTime,
CropEnding = inputOptions.CropEndingTime > 0.0,
CropEndingTime = inputOptions.CropEndingTime,
- Timestamp = inputOptions.Timestamp,
EmbedData = inputOptions.EmbedData,
Filename = inputOptions.OutputFile,
TimeFormat = inputOptions.TimeFormat,
ConnectionCount = inputOptions.ChatConnections,
BttvEmotes = (bool)inputOptions.BttvEmotes,
FfzEmotes = (bool)inputOptions.FfzEmotes,
- StvEmotes = (bool)inputOptions.StvEmotes
+ StvEmotes = (bool)inputOptions.StvEmotes,
+ TempFolder = inputOptions.TempFolder
};
ChatDownloader chatDownloader = new(downloadOptions);
diff --git a/TwitchDownloaderCLI/Modes/DownloadChatUpdater.cs b/TwitchDownloaderCLI/Modes/DownloadChatUpdater.cs
deleted file mode 100644
index fd459997..00000000
--- a/TwitchDownloaderCLI/Modes/DownloadChatUpdater.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text.Json;
-using System.Threading.Tasks;
-using TwitchDownloaderCLI.Modes.Arguments;
-using TwitchDownloaderCore;
-using TwitchDownloaderCore.Options;
-using TwitchDownloaderCore.TwitchObjects;
-
-namespace TwitchDownloaderCLI.Modes
-{
- internal class DownloadChatUpdater
- {
- internal static void Update(ChatDownloadUpdaterArgs inputOptions)
- {
- DownloadFormat inFormat = Path.GetExtension(inputOptions.InputFile)!.ToLower() switch
- {
- ".json" => DownloadFormat.Json,
- ".html" => DownloadFormat.Html,
- ".htm" => DownloadFormat.Html,
- _ => DownloadFormat.Text
- };
- DownloadFormat outFormat = Path.GetExtension(inputOptions.OutputFile)!.ToLower() switch
- {
- ".json" => DownloadFormat.Json,
- ".html" => DownloadFormat.Html,
- ".htm" => DownloadFormat.Html,
- _ => DownloadFormat.Text
- };
- // Check that both input and output are json
- if (inFormat != DownloadFormat.Json || outFormat != DownloadFormat.Json)
- {
- Console.WriteLine("[ERROR] - {0} format must be be json!", inFormat != DownloadFormat.Json ? "Input" : "Output");
- Environment.Exit(1);
- }
- if (!File.Exists(inputOptions.InputFile))
- {
- Console.WriteLine("[ERROR] - Input file does not exist!");
- Environment.Exit(1);
- }
- if (!inputOptions.EmbedMissing && !inputOptions.UpdateOldEmbeds)
- {
- Console.WriteLine("[ERROR] - Please enable either EmbedMissingEmotes or UpdateOldEmotes");
- Environment.Exit(1);
- }
-
- // Read in the old input file
- ChatRoot chatRoot = Task.Run(() => ChatRenderer.ParseJsonStatic(inputOptions.InputFile)).Result;
- if (chatRoot.streamer == null)
- {
- chatRoot.streamer = new Streamer();
- chatRoot.streamer.id = int.Parse(chatRoot.comments.First().channel_id);
- chatRoot.streamer.name = Task.Run(() => TwitchHelper.GetStreamerName(chatRoot.streamer.id)).Result;
- }
- if (chatRoot.embeddedData == null)
- {
- chatRoot.embeddedData = new EmbeddedData();
- }
-
- string cacheFolder = Path.Combine(string.IsNullOrWhiteSpace(inputOptions.TempFolder) ? Path.GetTempPath() : inputOptions.TempFolder, "TwitchDownloader", "chatupdatecache");
-
- // Clear working directory if it already exists
- if (Directory.Exists(cacheFolder))
- Directory.Delete(cacheFolder, true);
-
- // Thirdparty emotes
- if (chatRoot.embeddedData.thirdParty == null || inputOptions.UpdateOldEmbeds)
- {
- chatRoot.embeddedData.thirdParty = new List();
- }
- Console.WriteLine("Input third party emote count: " + chatRoot.embeddedData.thirdParty.Count);
- List thirdPartyEmotes = new List();
- thirdPartyEmotes = Task.Run(() => TwitchHelper.GetThirdPartyEmotes(chatRoot.streamer.id, cacheFolder, bttv: inputOptions.BttvEmotes, ffz: inputOptions.FfzEmotes, stv: inputOptions.StvEmotes, embeddedData: chatRoot.embeddedData)).Result;
- foreach (TwitchEmote emote in thirdPartyEmotes)
- {
- EmbedEmoteData newEmote = new EmbedEmoteData();
- newEmote.id = emote.Id;
- newEmote.imageScale = emote.ImageScale;
- newEmote.data = emote.ImageData;
- newEmote.name = emote.Name;
- newEmote.width = emote.Width / emote.ImageScale;
- newEmote.height = emote.Height / emote.ImageScale;
- chatRoot.embeddedData.thirdParty.Add(newEmote);
- }
- Console.WriteLine("Output third party emote count: " + chatRoot.embeddedData.thirdParty.Count);
-
- // Firstparty emotes
- if (chatRoot.embeddedData.firstParty == null || inputOptions.UpdateOldEmbeds)
- {
- chatRoot.embeddedData.firstParty = new List();
- }
- Console.WriteLine("Input first party emote count: " + chatRoot.embeddedData.firstParty.Count);
- List firstPartyEmotes = new List();
- firstPartyEmotes = Task.Run(() => TwitchHelper.GetEmotes(chatRoot.comments, cacheFolder, embeddedData: chatRoot.embeddedData)).Result;
- foreach (TwitchEmote emote in firstPartyEmotes)
- {
- EmbedEmoteData newEmote = new EmbedEmoteData();
- newEmote.id = emote.Id;
- newEmote.imageScale = emote.ImageScale;
- newEmote.data = emote.ImageData;
- newEmote.width = emote.Width / emote.ImageScale;
- newEmote.height = emote.Height / emote.ImageScale;
- chatRoot.embeddedData.firstParty.Add(newEmote);
- }
- Console.WriteLine("Output third party emote count: " + chatRoot.embeddedData.firstParty.Count);
-
- // Twitch badges
- if (chatRoot.embeddedData.twitchBadges == null || inputOptions.UpdateOldEmbeds)
- {
- chatRoot.embeddedData.twitchBadges = new List();
- }
- Console.WriteLine("Input twitch badge count: " + chatRoot.embeddedData.twitchBadges.Count);
- List twitchBadges = new List();
- twitchBadges = Task.Run(() => TwitchHelper.GetChatBadges(chatRoot.streamer.id, cacheFolder, embeddedData: chatRoot.embeddedData)).Result;
- foreach (ChatBadge badge in twitchBadges)
- {
- EmbedChatBadge newBadge = new EmbedChatBadge();
- newBadge.name = badge.Name;
- newBadge.versions = badge.VersionsData;
- chatRoot.embeddedData.twitchBadges.Add(newBadge);
- }
- Console.WriteLine("Output twitch badge count: " + chatRoot.embeddedData.twitchBadges.Count);
-
- // Twitch bits / cheers
- if (chatRoot.embeddedData.twitchBits == null || inputOptions.UpdateOldEmbeds)
- {
- chatRoot.embeddedData.twitchBits = new List();
- }
- Console.WriteLine("Input twitch bit count: " + chatRoot.embeddedData.twitchBits.Count);
- List twitchBits = new List();
- twitchBits = Task.Run(() => TwitchHelper.GetBits(cacheFolder, chatRoot.streamer.id.ToString(), embeddedData: chatRoot.embeddedData)).Result;
- foreach (CheerEmote bit in twitchBits)
- {
- EmbedCheerEmote newBit = new EmbedCheerEmote();
- newBit.prefix = bit.prefix;
- newBit.tierList = new Dictionary();
- foreach (KeyValuePair emotePair in bit.tierList)
- {
- EmbedEmoteData newEmote = new EmbedEmoteData();
- newEmote.id = emotePair.Value.Id;
- newEmote.imageScale = emotePair.Value.ImageScale;
- newEmote.data = emotePair.Value.ImageData;
- newEmote.name = emotePair.Value.Name;
- newEmote.width = emotePair.Value.Width / emotePair.Value.ImageScale;
- newEmote.height = emotePair.Value.Height / emotePair.Value.ImageScale;
- newBit.tierList.Add(emotePair.Key, newEmote);
- }
- chatRoot.embeddedData.twitchBits.Add(newBit);
- }
- Console.WriteLine("Input twitch bit count: " + chatRoot.embeddedData.twitchBits.Count);
-
- // Finally save the output to file!
- // TODO: maybe in the future we could also export as HTML here too?
- if (outFormat == DownloadFormat.Json)
- {
- using (TextWriter writer = File.CreateText(inputOptions.OutputFile))
- {
- var serializer = new Newtonsoft.Json.JsonSerializer();
- serializer.Serialize(writer, chatRoot);
- }
- }
-
- // Clear our working directory, it's highly unlikely we would reuse it anyways
- if (Directory.Exists(cacheFolder))
- Directory.Delete(cacheFolder, true);
- }
- }
-}
diff --git a/TwitchDownloaderCLI/Modes/DownloadVideo.cs b/TwitchDownloaderCLI/Modes/DownloadVideo.cs
index e5e6ad93..e72deb5f 100644
--- a/TwitchDownloaderCLI/Modes/DownloadVideo.cs
+++ b/TwitchDownloaderCLI/Modes/DownloadVideo.cs
@@ -15,7 +15,7 @@ internal static void Download(VideoDownloadArgs inputOptions)
{
FfmpegHandler.DetectFfmpeg(inputOptions.FfmpegPath);
- if (inputOptions.Id == string.Empty || !inputOptions.Id.All(char.IsDigit))
+ if (string.IsNullOrWhiteSpace(inputOptions.Id) || !inputOptions.Id.All(char.IsDigit))
{
Console.WriteLine("[ERROR] - Invalid VOD ID, unable to parse. Must be only numbers.");
Environment.Exit(1);
@@ -32,7 +32,7 @@ internal static void Download(VideoDownloadArgs inputOptions)
CropBeginningTime = inputOptions.CropBeginningTime,
CropEnding = inputOptions.CropEndingTime > 0.0,
CropEndingTime = inputOptions.CropEndingTime,
- FfmpegPath = inputOptions.FfmpegPath is null or "" ? FfmpegHandler.ffmpegExecutableName : Path.GetFullPath(inputOptions.FfmpegPath),
+ FfmpegPath = string.IsNullOrWhiteSpace(inputOptions.FfmpegPath) ? FfmpegHandler.ffmpegExecutableName : Path.GetFullPath(inputOptions.FfmpegPath),
TempFolder = inputOptions.TempFolder
};
diff --git a/TwitchDownloaderCLI/Modes/RenderChat.cs b/TwitchDownloaderCLI/Modes/RenderChat.cs
index 2d361d67..8d3fa4fd 100644
--- a/TwitchDownloaderCLI/Modes/RenderChat.cs
+++ b/TwitchDownloaderCLI/Modes/RenderChat.cs
@@ -37,16 +37,14 @@ internal static void Render(ChatRenderArgs inputOptions)
{
"normal" => SKFontStyle.Normal,
"bold" => SKFontStyle.Bold,
- "italic" => SKFontStyle.Italic,
- "italics" => SKFontStyle.Italic,
+ "italic" or "italics" => SKFontStyle.Italic,
_ => throw new NotImplementedException("Invalid message font style. Valid values are: normal, bold, and italic")
},
UsernameFontStyle = inputOptions.UsernameFontStyle.ToLower() switch
{
"normal" => SKFontStyle.Normal,
"bold" => SKFontStyle.Bold,
- "italic" => SKFontStyle.Italic,
- "italics" => SKFontStyle.Italic,
+ "italic" or "italics" => SKFontStyle.Italic,
_ => throw new NotImplementedException("Invalid username font style. Valid values are: normal, bold, and italic")
},
UpdateRate = inputOptions.UpdateRate,
@@ -59,7 +57,7 @@ internal static void Render(ChatRenderArgs inputOptions)
SubMessages = (bool)inputOptions.SubMessages,
ChatBadges = (bool)inputOptions.ChatBadges,
Timestamp = inputOptions.Timestamp,
- Offline = (bool)inputOptions.Offline,
+ Offline = inputOptions.Offline,
};
if (renderOptions.GenerateMask && renderOptions.BackgroundColor.Alpha == 255)
@@ -80,7 +78,7 @@ internal static void Render(ChatRenderArgs inputOptions)
}
}
- if (inputOptions.IgnoreUsersList != string.Empty)
+ if (inputOptions.IgnoreUsersList != "")
{
renderOptions.IgnoreUsersList = inputOptions.IgnoreUsersList.ToLower().Split(',',
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList();
@@ -90,7 +88,7 @@ internal static void Render(ChatRenderArgs inputOptions)
ChatRenderer chatRenderer = new(renderOptions);
Progress progress = new();
progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged;
- chatRenderer.ParseJson().Wait();
+ chatRenderer.ParseJsonAsync().Wait();
chatRenderer.RenderVideoAsync(progress, new CancellationToken()).Wait();
}
}
diff --git a/TwitchDownloaderCLI/Modes/UpdateChat.cs b/TwitchDownloaderCLI/Modes/UpdateChat.cs
new file mode 100644
index 00000000..80f31457
--- /dev/null
+++ b/TwitchDownloaderCLI/Modes/UpdateChat.cs
@@ -0,0 +1,72 @@
+using System;
+using System.IO;
+using System.Threading;
+using TwitchDownloaderCLI.Modes.Arguments;
+using TwitchDownloaderCLI.Tools;
+using TwitchDownloaderCore;
+using TwitchDownloaderCore.Options;
+
+namespace TwitchDownloaderCLI.Modes
+{
+ internal class UpdateChat
+ {
+ internal static void Update(ChatUpdateArgs inputOptions)
+ {
+ if (!File.Exists(inputOptions.InputFile))
+ {
+ Console.WriteLine("[ERROR] - Input file does not exist!");
+ Environment.Exit(1);
+ }
+ ChatFormat inFormat = Path.GetExtension(inputOptions.InputFile)!.ToLower() switch
+ {
+ ".html" or ".htm" => ChatFormat.Html,
+ ".json" => ChatFormat.Json,
+ _ => ChatFormat.Text
+ };
+ ChatFormat outFormat = Path.GetExtension(inputOptions.OutputFile)!.ToLower() switch
+ {
+ ".html" or ".htm" => ChatFormat.Html,
+ ".json" => ChatFormat.Json,
+ _ => ChatFormat.Text
+ };
+ if (inFormat != ChatFormat.Json)
+ {
+ Console.WriteLine("[ERROR] - Input file must be json!");
+ Environment.Exit(1);
+ }
+ if (inputOptions.InputFile == inputOptions.OutputFile)
+ {
+ Console.WriteLine("[WARNING] - Output file path is identical to input file. This is not recommended in case something goes wrong. All data will be permanantly overwritten!");
+ }
+ if (!inputOptions.EmbedMissing && !inputOptions.ReplaceEmbeds && double.IsNegative(inputOptions.CropBeginningTime) && double.IsNegative(inputOptions.CropEndingTime))
+ {
+ Console.WriteLine("[ERROR] - No update options were passed. Please pass --embed-missing, --replace-embeds, -b, or -e");
+ Environment.Exit(1);
+ }
+
+ ChatUpdateOptions updateOptions = new()
+ {
+ InputFile = inputOptions.InputFile,
+ OutputFile = inputOptions.OutputFile,
+ OutputFormat = outFormat,
+ EmbedMissing = inputOptions.EmbedMissing,
+ ReplaceEmbeds = inputOptions.ReplaceEmbeds,
+ CropBeginning = !double.IsNegative(inputOptions.CropBeginningTime),
+ CropBeginningTime = inputOptions.CropBeginningTime,
+ CropEnding = !double.IsNegative(inputOptions.CropEndingTime),
+ CropEndingTime = inputOptions.CropEndingTime,
+ BttvEmotes = (bool)inputOptions.BttvEmotes,
+ FfzEmotes = (bool)inputOptions.FfzEmotes,
+ StvEmotes = (bool)inputOptions.StvEmotes,
+ TextTimestampFormat = inputOptions.TimeFormat,
+ TempFolder = inputOptions.TempFolder
+ };
+
+ ChatUpdater chatUpdater = new(updateOptions);
+ Progress progress = new();
+ progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged;
+ chatUpdater.ParseJsonAsync().Wait();
+ chatUpdater.UpdateAsync(progress, new CancellationToken()).Wait();
+ }
+ }
+}
diff --git a/TwitchDownloaderCLI/Program.cs b/TwitchDownloaderCLI/Program.cs
index 07727d7b..139b0254 100644
--- a/TwitchDownloaderCLI/Program.cs
+++ b/TwitchDownloaderCLI/Program.cs
@@ -1,6 +1,5 @@
using CommandLine;
using System;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using TwitchDownloaderCLI.Modes;
@@ -13,7 +12,7 @@ class Program
{
static void Main(string[] args)
{
- string processFileName = Environment.ProcessPath.Split(Path.DirectorySeparatorChar).Last();
+ string processFileName = Path.GetFileName(Environment.ProcessPath);
if (args.Length == 0)
{
if (Path.GetExtension(processFileName).Equals(".exe"))
@@ -42,11 +41,11 @@ static void Main(string[] args)
preParsedArgs = PreParseArgs.Process(args);
}
- Parser.Default.ParseArguments(preParsedArgs)
+ Parser.Default.ParseArguments(preParsedArgs)
.WithParsed(DownloadVideo.Download)
.WithParsed(DownloadClip.Download)
.WithParsed(DownloadChat.Download)
- .WithParsed(DownloadChatUpdater.Update)
+ .WithParsed(UpdateChat.Update)
.WithParsed(RenderChat.Render)
.WithParsed(FfmpegHandler.ParseArgs)
.WithParsed(CacheHandler.ParseArgs)
diff --git a/TwitchDownloaderCLI/README.md b/TwitchDownloaderCLI/README.md
index 9dc00650..aa370c2f 100644
--- a/TwitchDownloaderCLI/README.md
+++ b/TwitchDownloaderCLI/README.md
@@ -1,267 +1,289 @@
# TwitchDownloaderCLI
A cross platform command line tool that can do the main functions of the GUI program, which can download VODs/Clips/Chats and render chats.
- - [Arguments for mode videodownload](#arguments-for-mode-videodownload)
- - [Arguments for mode clipdownload](#arguments-for-mode-clipdownload)
- - [Arguments for mode chatdownload](#arguments-for-mode-chatdownload)
- - [Arguments for mode chatrender](#arguments-for-mode-chatrender)
- - [Arguments for mode chatupdate](#arguments-for-mode-chatupdate)
- - [Arguments for mode ffmpeg](#arguments-for-mode-ffmpeg)
- - [Arguments for mode cache](#arguments-for-mode-cache)
- - [Example commands](#example-commands)
- - [Notes](#notes)
+- [TwitchDownloaderCLI](#twitchdownloadercli)
+ - [Arguments for mode videodownload](#arguments-for-mode-videodownload)
+ - [Arguments for mode clipdownload](#arguments-for-mode-clipdownload)
+ - [Arguments for mode chatdownload](#arguments-for-mode-chatdownload)
+ - [Arguments for mode chatupdate](#arguments-for-mode-chatupdate)
+ - [Arguments for mode chatrender](#arguments-for-mode-chatrender)
+ - [Arguments for mode ffmpeg](#arguments-for-mode-ffmpeg)
+ - [Arguments for mode cache](#arguments-for-mode-cache)
+ - [Example Commands](#example-commands)
+ - [Additional Notes](#additional-notes)
---
## Arguments for mode videodownload
-Downloads a stream VOD from Twitch
+Downloads a stream VOD or highlight from Twitch
-**-u/-\-id (REQUIRED)**
+**-u / --id (REQUIRED)**
The ID of the VOD to download, currently only accepts Integer IDs and will accept URLs in the future.
-**-o/-\-output (REQUIRED)**
+**-o / --output (REQUIRED)**
File the program will output to.
-**-q/-\-quality**
+**-q / --quality**
The quality the program will attempt to download, for example "1080p60", if not found will download highest quality stream.
-**-b/-\-beginning**
+**-b / --beginning**
Time in seconds to crop beginning. For example if I had a 10 second stream but only wanted the last 7 seconds of it I would use `-b 3` to skip the first 3 seconds.
-**-e/-\-ending**
+**-e / --ending**
Time in seconds to crop ending. For example if I had a 10 second stream but only wanted the first 4 seconds of it I would use `-e 4` to end on the 4th second.
Extra example, if I wanted only seconds 3-6 in a 10 second stream I would do `-b 3 -e 6`
-**-t/-\-threads**
+**-t / --threads**
(Default: 10) Number of download threads.
-**-\-oauth**
-OAuth access token to download subscriber only VODs. **DO NOT SHARE THIS WITH ANYONE.**
+**--oauth**
+OAuth access token to download subscriber only VODs. **DO NOT SHARE YOUR OUATH WITH ANYONE.**
-**-\-ffmpeg-path**
+**--ffmpeg-path**
Path to ffmpeg executable.
-**-\-temp-path**
+**--temp-path**
Path to temporary folder for cache.
## Arguments for mode clipdownload
-Downloads a clip from Twitch
+Downloads a clip from Twitch
-**-u/-\-id (REQUIRED)**
+**-u / --id (REQUIRED)**
The ID of the Clip to download, currently only accepts the string identifier and will accept URLs in the future.
-**-o/-\-output (REQUIRED)**
+**-o / --output (REQUIRED)**
File the program will output to.
-**-q/-\-quality**
+**-q / --quality**
The quality the program will attempt to download, for example "1080p60", if not found will download highest quality video.
## Arguments for mode chatdownload
-Downloads the chat from a VOD or clip
+Downloads the chat of a VOD, highlight, or clip
-**-u/-\-id (REQUIRED)**
+**-u / --id (REQUIRED)**
The ID of the VOD or clip to download. Does not currently accept URLs.
-**-o/-\-output (REQUIRED)**
-File the program will output to. File extension will be used to determine download type. Valid extensions are `json`, `html`, and `txt`.
+**-o / --output (REQUIRED)**
+File the program will output to. File extension will be used to determine download type. Valid extensions are: `json`, `html`, and `txt`.
-**-b/-\-beginning**
+**-b / --beginning**
Time in seconds to crop beginning. For example if I had a 10 second stream but only wanted the last 7 seconds of it I would use `-b 3` to skip the first 3 seconds.
-**-e/-\-ending**
+**-e / --ending**
Time in seconds to crop ending. For example if I had a 10 second stream but only wanted the first 4 seconds of it I would use `-e 4` to end on the 4th second.
-**-E/-\-embed-images**
+**-E / --embed-images**
(Default: false) Embed first party emotes, badges, and cheermotes into the download file for offline rendering. Useful for archival purposes, file size will be larger.
-**-\-bttv**
+**--bttv**
(Default: true) BTTV emote embedding. Requires `-E / --embed-images`.
-**-\-ffz**
+**--ffz**
(Default: true) FFZ emote embedding. Requires `-E / --embed-images`.
-**-\-stv**
+**--stv**
(Default: true) 7TV emote embedding. Requires `-E / --embed-images`.
-**-\-timestamp**
-(Default: false) Enable timestamps
+**--timestamp-format**
+(Default: Relative) Sets the timestamp format for .txt chat logs. Valid values are: `Utc`, `Relative`, and `None`.
-**-\-timestamp-format**
-(Default: Relative) Sets the timestamp format for .txt chat logs. Valid values are Utc, Relative, and None.
-
-**-\-chat-connections**
+**--chat-connections**
(Default: 4) The number of parallel downloads for chat.
+## Arguments for mode chatupdate
+Updates the embeded emotes, badges, bits, and crops of a chat download and/or converts a JSON chat to another format
+
+**-i / --input (REQUIRED)**
+Path to input file. Valid extensions are: `json`.
+
+**-o / --output (REQUIRED)**
+Path to output file. File extension will be used to determine new chat type. Valid extensions are: `json`, `html`, and `txt`.
+
+**-E / --embed-missing**
+(Default: false) Embed missing emotes, badges, and cheermotes. Already embedded images will be untouched.
+
+**-R / --replace-embeds**
+(Default: false) Replace all embedded emotes, badges, and cheermotes in the file. All embedded data will be overwritten!
+
+**b / --beginning**
+(Default: -1) New time in seconds for chat beginning. Comments may be added but not removed. -1 = No crop.
+
+**-e / --ending**
+(Default: -1) New time in seconds for chat beginning. Comments may be added but not removed. -1 = No crop.
+
+**--bttv**
+(Default: true) Enable embedding BTTV emotes.
+
+**--ffz**
+(Default: true) Enable embedding FFZ emotes.
+
+**--stv**
+(Default: true) Enable embedding 7TV emotes.
+
+**--timestamp-format**
+(Default: Relative) Sets the timestamp format for .txt chat logs. Valid values are: `Utc`, `Relative`, and `None`.
+
+**--temp-path**
+Path to temporary folder for cache.
+
+
## Arguments for mode chatrender
-Renders a chat JSON as a video
+Renders a chat JSON as a video
-**-i/-\-input (REQUIRED)**
+**-i / --input (REQUIRED)**
Path to JSON chat file input.
-**-o/-\-output (REQUIRED)**
+**-o / --output (REQUIRED)**
File the program will output to.
-**-\-background-color**
+**--background-color**
(Default: #111111) Color of background in HEX string format.
-**-\-message-color**
+**--message-color**
(Default: #ffffff) Color of messages in HEX string format.
-**-w/-\-chat-width**
+**-w / --chat-width**
(Default: 350) Width of chat render.
-**-h/-\-chat-height**
+**-h / --chat-height**
(Default: 600) Height of chat render.
-**-\-bttv**
+**--bttv**
(Default: true) Enable BTTV emotes.
-**-\-ffz**
+**--ffz**
(Default: true) Enable FFZ emotes.
-**-\-stv**
+**--stv**
(Default: true) Enable 7TV emotes.
-**-\-sub-messages**
-(Default: true) Enable sub/re-sub messages.
+**--sub-messages**
+(Default: true) Enable sub / re-sub messages.
-**-\-badges**
+**--badges**
(Default: true) Enable chat badges.
-**-\-outline**
+**--outline**
(Default: false) Enable outline around chat messages.
-**-\-outline-size**
+**--outline-size**
(Default: 4) Size of outline if outline is enabled.
-**-f/-\-font**
+**-f / --font**
(Default: Inter Embedded) Font to use.
-**-\-font-size**
+**--font-size**
(Default: 12) Font size.
-**-\-message-fontstyle**
+**--message-fontstyle**
(Default: normal) Font style of message. Valid values are **normal**, **bold**, and **italic**.
-**-\-username-fontstyle**
+**--username-fontstyle**
(Default: bold) Font style of username. Valid values are **normal**, **bold**, and **italic**.
-**-\-timestamp**
+**--timestamp**
(Default: false) Enables timestamps to left of messages, similar to VOD chat on Twitch.
-**-\-generate-mask**
+**--generate-mask**
(Default: false) Generates a mask file of the chat in addition to the rendered chat.
-**-\-framerate**
+**--framerate**
(Default: 30) Framerate of the render.
-**-\-update-rate**
+**--update-rate**
(Default: 0.2) Time in seconds to update chat render output.
-**-\-input-args**
-(Default: -framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt bgra -video_size {width}x{height} -i -) Input (pass1) arguments for ffmpeg chat render.
+**--input-args**
+(Default: -framerate {fps} -f rawvideo -analyzeduration {max_int} -probesize {max_int} -pix_fmt bgra -video_size {width}x{height} -i -) Input arguments for ffmpeg chat render.
-**-\-output-args**
-(Default: -c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p "{save_path}") Output (pass2) arguments for ffmpeg chat render.
+**--output-args**
+(Default: -c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p "{save_path}") Output arguments for ffmpeg chat render.
-**-\-ignore-users**
+**--ignore-users**
(Default: ) List of usernames to ignore when rendering, separated by commas.
-**-\-badge-filter**
+**--badge-filter**
(Default: 0) Bitmask of types of Chat Badges to filter out. Add the numbers of the types of badges you want to filter. For example, to filter out Moderator and Broadcaster badges only enter the value of 6.
-Other = `1`,
-Broadcaster = `2`,
-Moderator = `4`,
-VIP = `8`,
-Subscriber = `16`,
-Predictions = `32`,
-NoAudioVisual = `64`,
-PrimeGaming = `128`
+Other = `1`, Broadcaster = `2`, Moderator = `4`, VIP = `8`, Subscriber = `16`, Predictions = `32`, NoAudioVisual = `64`, PrimeGaming = `128`
-**-\-offline**
-Render completely offline, using only resources embedded emotes, badges, and bits in the input json.
+**--offline**
+Render completely offline using only embedded emotes, badges, and bits from the input json.
-**-\-ffmpeg-path**
+**--ffmpeg-path**
Path to ffmpeg executable.
-**-\-temp-path**
-Path to temporary folder for cache.
-
-
-## Arguments for mode chatupdate
-
-**-i/-\-input (REQUIRED)**
-Path to input file. Valid extensions are json
-
-**-o/-\-output (REQUIRED)**
-Path to output file. Extension should match the input.
-
-**-E/-\-embed-missing**
-(Default: true) Embed missing emotes, badges, and bits. Already embedded images will be untouched.
-
-**-U/-\-update-old**
-(Default: false) Update old emotes, badges, and bits to the current. All embedded images will be overwritten!
-
-**-\-bttv**
-(Default: true) Enable embedding BTTV emotes.
-
-**-\-ffz**
-(Default: true) Enable embedding FFZ emotes.
-
-**-\-stv**
-(Default: true) Enable embedding 7TV emotes.
-
-**-\-temp-path**
+**--temp-path**
Path to temporary folder for cache.
## Arguments for mode ffmpeg
-Manage standalone ffmpeg
+Manage standalone ffmpeg
-**-d/-\-download**
+**-d / --download**
(Default: false) Downloads ffmpeg as a standalone file.
## Arguments for mode cache
- Manage the working cache.
+Manage the working cache.
-**-c/-\-clear**
+**-c / --clear**
(Default: false) Clears the default cache folder.
-**-\-force-clear**
+**--force-clear**
(Default: false) Clears the default cache folder, bypassing the confirmation prompt.
---
## Example Commands
-Download a VOD
+Examples of typical use cases
+
+Download a VOD with defaults
TwitchDownloaderCLI videodownload --id 612942303 -o video.mp4
-Download a Clip
+
+Download a Clip with defaults
TwitchDownloaderCLI clipdownload --id NurturingCalmHamburgerVoHiYo -o clip.mp4
-Download a Chat (plain text with timestamps)
- TwitchDownloaderCLI chatdownload --id 612942303 --timestamp-format Relative -o chat.txt
-Download a Chat (JSON with embeded emotes from Twitch and Bttv)
+Download a Chat JSON with embeded emotes/badges from Twitch and emotes from Bttv
TwitchDownloaderCLI chatdownload --id 612942303 --embed-images --bttv=true --ffz=false --stv=false -o chat.json
+
+Download a Chat as plain text with timestamps
+
+ TwitchDownloaderCLI chatdownload --id 612942303 --timestamp-format Relative -o chat.txt
+
+Add embeds to a chat file that was downloaded without embeds
+
+ TwitchDownloaderCLI chatupdate -i chat.json -o chat_embedded.json --embed-missing
+
+Convert a JSON chat file to HTML
+
+ TwitchDownloaderCLI chatupdate -i chat.json -o chat.html
+
Render a chat with defaults
TwitchDownloaderCLI chatrender -i chat.json -o chat.mp4
-Render a chat with different heights and values
+
+Render a chat with custom video settings and message outlines
TwitchDownloaderCLI chatrender -i chat.json -h 1440 -w 720 --framerate 60 --outline -o chat.mp4
+
Render a chat with custom ffmpeg arguments
TwitchDownloaderCLI chatrender -i chat.json --output-args='-c:v libx264 -preset veryfast -crf 18 -pix_fmt yuv420p "{save_path}"' -o chat.mp4
---
-## Notes
-Due to some limitations, default true boolean flags must be assigned: `--default-true-flag=false`. Default false boolean flags must still be raised normally: `--default-false-flag`
\ No newline at end of file
+## Additional Notes
+
+String arguments, such as output file, that contain spaces should be wrapped in double quotes " .
+
+Default true boolean flags must be assigned: `--default-true-flag=false`. Default false boolean flags should still be raised normally: `--default-false-flag`
+
+For Linux users, ensure both `fontconfig` and `libfontconfig1` are installed. `apt-get install fontconfig libfontconfig1` on Ubuntu.
\ No newline at end of file
diff --git a/TwitchDownloaderCLI/Tools/PreParseArgs.cs b/TwitchDownloaderCLI/Tools/PreParseArgs.cs
index 4ecc3b5d..ced87d9d 100644
--- a/TwitchDownloaderCLI/Tools/PreParseArgs.cs
+++ b/TwitchDownloaderCLI/Tools/PreParseArgs.cs
@@ -14,50 +14,65 @@ internal static string[] Process(string[] args)
}
///
- /// Converts an argument array that uses any legacy syntax to the current syntax
+ /// Converts an argument [] using any legacy syntax to the current syntax and prints corresponding warning messages
///
///
- /// The same array but using current syntax instead
+ /// An argument [] using current syntaxes that represent the intentions of the legacy syntax
internal static string[] ConvertFromOldSyntax(string[] args, string processFileName)
{
- int argsLength = args.Length;
List processedArgs = args.ToList();
if (args.Any(x => x.Equals("--embed-emotes")))
{
- Console.WriteLine("[INFO] The program has switched from --embed-emotes to --embed-images OR -E, consider using those instead. Run \'{0} help\' for more information.", processFileName);
- for (int i = 0; i < argsLength; i++)
+ Console.WriteLine("[INFO] The program has switched from --embed-emotes to -E / --embed-images, consider using those instead. Run \'{0} help\' for more information.", processFileName);
+ processedArgs = ConvertEmbedEmoteSyntax(processedArgs);
+ }
+
+ if (args.Any(x => x is "-m" or "--mode"))
+ {
+ Console.WriteLine("[INFO] The program has switched from --mode to verbs (like \'git \'), consider using verbs instead. Run \'{0} help\' for more information.", processFileName);
+ processedArgs = ConvertModeSyntax(processedArgs);
+ }
+
+ return processedArgs.ToArray();
+ }
+
+ internal static List ConvertEmbedEmoteSyntax(List args)
+ {
+ int argsLength = args.Count;
+
+ for (int i = 0; i < argsLength; i++)
+ {
+ if (args[i].Equals("--embed-emotes"))
{
- if (processedArgs[i].Equals("--embed-emotes"))
- {
- processedArgs[i] = "-E";
- break;
- }
+ args[i] = "-E";
+ break;
}
}
- // This must always be performed last
- if (args.Any(x => x.Equals("-m") || x.Equals("--mode")))
+ return args;
+ }
+
+ internal static List ConvertModeSyntax(List args)
+ {
+ int argsLength = args.Count;
+ string[] processedArgs = new string[argsLength - 1];
+
+ int j = 1;
+ for (int i = 0; i < argsLength; i++)
{
- Console.WriteLine("[INFO] The program has switched from --mode to verbs (like \'git \'), consider using verbs instead. Run \'{0} help\' for more information.", processFileName);
- int j = 1;
- for (int i = 0; i < argsLength; i++)
+ if (args[i].Equals("-m") || args[i].Equals("--mode"))
{
- if (processedArgs[i].Equals("-m") || processedArgs[i].Equals("--mode"))
- {
- // Copy the runmode to the verb position
- processedArgs[0] = processedArgs[i + 1];
- i++;
- continue;
- }
- processedArgs[j] = processedArgs[i];
- j++;
+ // Copy the runmode to the verb position
+ processedArgs[0] = args[i + 1];
+ i++;
+ continue;
}
- // Remove last element as it will be a duplicate of second last element
- processedArgs.RemoveAt(processedArgs.Count - 1);
+ processedArgs[j] = args[i];
+ j++;
}
- return processedArgs.ToArray();
+ return processedArgs.ToList();
}
}
}
diff --git a/TwitchDownloaderCLI/Tools/ProgressHandler.cs b/TwitchDownloaderCLI/Tools/ProgressHandler.cs
index aedcc9f9..eb0aeb8e 100644
--- a/TwitchDownloaderCLI/Tools/ProgressHandler.cs
+++ b/TwitchDownloaderCLI/Tools/ProgressHandler.cs
@@ -5,38 +5,51 @@ namespace TwitchDownloaderCLI.Tools
{
internal class ProgressHandler
{
- private static string previousStatus = string.Empty;
- private static bool was_last_message_percent = false;
+ private static string previousMessage = "";
+ private static bool previousMessageWasStatusInfo = false;
internal static void Progress_ProgressChanged(object sender, ProgressReport e)
{
- if (e.reportType == ReportType.Message)
+ if (e.ReportType == ReportType.Status)
{
- if (was_last_message_percent)
+ if (previousMessageWasStatusInfo)
{
- was_last_message_percent = false;
+ previousMessageWasStatusInfo = false;
Console.WriteLine();
}
- string currentStatus = "[STATUS] - " + e.data;
- if (currentStatus != previousStatus)
+
+ string currentStatus = "[STATUS] - " + e.Data;
+ if (currentStatus != previousMessage)
{
- previousStatus = currentStatus;
+ previousMessage = currentStatus;
Console.WriteLine(currentStatus);
}
}
- else if (e.reportType == ReportType.Log)
+ else if (e.ReportType == ReportType.StatusInfo)
{
- if (was_last_message_percent)
+ string currentStatus = "\r[STATUS] - " + e.Data;
+ if (currentStatus != previousMessage)
{
- was_last_message_percent = false;
- Console.WriteLine();
+ previousMessageWasStatusInfo = true;
+
+ // This ensures the previous message is fully overwritten
+ currentStatus = currentStatus.PadRight(previousMessage.Length);
+
+ previousMessage = currentStatus.TrimEnd();
+ Console.Write(currentStatus);
}
- Console.WriteLine("[LOG] - " + e.data);
}
- else if (e.reportType == ReportType.MessageInfo)
+ else if (e.ReportType == ReportType.Log)
{
- Console.Write("\r[STATUS] - " + e.data);
- was_last_message_percent = true;
+ if (previousMessageWasStatusInfo)
+ {
+ previousMessageWasStatusInfo = false;
+ Console.WriteLine();
+ }
+
+ string currentStatus = "[LOG] - " + e.Data;
+ previousMessage = currentStatus;
+ Console.WriteLine(currentStatus);
}
}
}
diff --git a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
index a078acb4..d1162425 100644
--- a/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
+++ b/TwitchDownloaderCLI/TwitchDownloaderCLI.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/TwitchDownloaderCore/ChatDownloader.cs b/TwitchDownloaderCore/ChatDownloader.cs
index 0aced38a..266fd442 100644
--- a/TwitchDownloaderCore/ChatDownloader.cs
+++ b/TwitchDownloaderCore/ChatDownloader.cs
@@ -7,24 +7,27 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using System.Web;
using TwitchDownloaderCore.Options;
+using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects;
using TwitchDownloaderCore.TwitchObjects.Gql;
namespace TwitchDownloaderCore
{
- public class ChatDownloader
+ public sealed class ChatDownloader
{
- ChatDownloadOptions downloadOptions;
- enum DownloadType { Clip, Video }
+ private readonly ChatDownloadOptions downloadOptions;
+ private enum DownloadType { Clip, Video }
public ChatDownloader(ChatDownloadOptions DownloadOptions)
{
downloadOptions = DownloadOptions;
+ downloadOptions.TempFolder = Path.Combine(
+ string.IsNullOrWhiteSpace(downloadOptions.TempFolder) ? Path.GetTempPath() : downloadOptions.TempFolder,
+ "TwitchDownloader");
}
- private async Task DownloadSection(IProgress progress, CancellationToken cancellationToken, double videoStart, double videoEnd, string videoId, SortedSet comments, object commentLock)
+ private static async Task DownloadSection(double videoStart, double videoEnd, string videoId, SortedSet comments, object commentLock, IProgress progress, CancellationToken cancellationToken)
{
using (WebClient client = new WebClient())
{
@@ -81,7 +84,7 @@ private async Task DownloadSection(IProgress progress, Cancellat
cursor = commentResponse.data.video.comments.edges.Last().cursor;
int percent = (int)Math.Floor((latestMessage - videoStart) / videoDuration * 100);
- progress.Report(new ProgressReport() { reportType = ReportType.Percent, data = percent });
+ progress.Report(new ProgressReport() { ReportType = ReportType.Percent, Data = percent });
cancellationToken.ThrowIfCancellationRequested();
@@ -92,7 +95,7 @@ private async Task DownloadSection(IProgress progress, Cancellat
}
}
- private List ConvertComments(CommentVideo video)
+ private static List ConvertComments(CommentVideo video)
{
List returnList = new List();
@@ -167,13 +170,17 @@ private List ConvertComments(CommentVideo video)
public async Task DownloadAsync(IProgress progress, CancellationToken cancellationToken)
{
- DownloadType downloadType = downloadOptions.Id.All(x => char.IsDigit(x)) ? DownloadType.Video : DownloadType.Clip;
+ if (string.IsNullOrWhiteSpace(downloadOptions.Id))
+ {
+ throw new NullReferenceException("Null or empty video/clip ID");
+ }
+ DownloadType downloadType = downloadOptions.Id.All(char.IsDigit) ? DownloadType.Video : DownloadType.Clip;
List comments = new List();
ChatRoot chatRoot = new ChatRoot() { FileInfo = new ChatRootInfo() { Version = new ChatRootVersion(1, 1, 0) }, streamer = new Streamer(), video = new Video(), comments = comments };
- string videoId = "";
- string videoTitle = "";
+ string videoId = downloadOptions.Id;
+ string videoTitle;
DateTime videoCreatedAt;
double videoStart = 0.0;
double videoEnd = 0.0;
@@ -183,8 +190,12 @@ public async Task DownloadAsync(IProgress progress, Cancellation
if (downloadType == DownloadType.Video)
{
- videoId = downloadOptions.Id;
GqlVideoResponse taskVideoInfo = await TwitchHelper.GetVideoInfo(int.Parse(videoId));
+ if (taskVideoInfo.data.video == null)
+ {
+ throw new NullReferenceException("Invalid VOD, deleted/expired VOD possibly?");
+ }
+
chatRoot.streamer.name = taskVideoInfo.data.video.owner.displayName;
chatRoot.streamer.id = int.Parse(taskVideoInfo.data.video.owner.id);
videoTitle = taskVideoInfo.data.video.title;
@@ -195,12 +206,12 @@ public async Task DownloadAsync(IProgress progress, Cancellation
}
else
{
- GqlClipResponse taskClipInfo = await TwitchHelper.GetClipInfo(downloadOptions.Id);
-
+ GqlClipResponse taskClipInfo = await TwitchHelper.GetClipInfo(videoId);
if (taskClipInfo.data.clip.video == null || taskClipInfo.data.clip.videoOffsetSeconds == null)
- throw new Exception("Invalid VOD for clip, deleted/expired VOD possibly?");
+ {
+ throw new NullReferenceException("Invalid VOD for clip, deleted/expired VOD possibly?");
+ }
- videoId = taskClipInfo.data.clip.video.id;
downloadOptions.CropBeginning = true;
downloadOptions.CropBeginningTime = (int)taskClipInfo.data.clip.videoOffsetSeconds;
downloadOptions.CropEnding = true;
@@ -235,13 +246,13 @@ public async Task DownloadAsync(IProgress progress, Cancellation
percentages.Add(0);
var taskProgress = new Progress(progressReport =>
{
- if (progressReport.reportType != ReportType.Percent)
+ if (progressReport.ReportType != ReportType.Percent)
{
progress.Report(progressReport);
}
else
{
- int percent = (int)(progressReport.data);
+ int percent = (int)progressReport.Data;
if (percent > 100)
{
percent = 100;
@@ -251,15 +262,17 @@ public async Task DownloadAsync(IProgress progress, Cancellation
percent = 0;
for (int j = 0; j < connectionCount; j++)
+ {
percent += percentages[j];
- percent = percent / connectionCount;
+ }
+ percent /= connectionCount;
- progress.Report(new ProgressReport() { reportType = ReportType.MessageInfo, data = $"Downloading {percent}%" });
- progress.Report(new ProgressReport() { reportType = ReportType.Percent, data = percent });
+ progress.Report(new ProgressReport() { ReportType = ReportType.StatusInfo, Data = $"Downloading {percent}%" });
+ progress.Report(new ProgressReport() { ReportType = ReportType.Percent, Data = percent });
}
});
double start = videoStart + chunk * i;
- tasks.Add(DownloadSection(taskProgress, cancellationToken, start, start + chunk, videoId, commentsSet, commentLock));
+ tasks.Add(DownloadSection(start, start + chunk, videoId, commentsSet, commentLock, taskProgress, cancellationToken));
}
await Task.WhenAll(tasks);
@@ -267,25 +280,17 @@ public async Task DownloadAsync(IProgress progress, Cancellation
comments = commentsSet.DistinctBy(x => x._id).ToList();
chatRoot.comments = comments;
- if (downloadOptions.EmbedData && (downloadOptions.DownloadFormat == DownloadFormat.Json || downloadOptions.DownloadFormat == DownloadFormat.Html))
+ if (downloadOptions.EmbedData && (downloadOptions.DownloadFormat is ChatFormat.Json or ChatFormat.Html))
{
- progress.Report(new ProgressReport() { reportType = ReportType.Message, data = "Downloading + Embedding Images" });
+ progress.Report(new ProgressReport() { ReportType = ReportType.Status, Data = "Downloading + Embedding Images" });
chatRoot.embeddedData = new EmbeddedData();
- List firstPartyReturnList = new List();
- List thirdPartyReturnList = new List();
- List badgesReturnList = new List();
- List bitsReturnList = new List();
-
- string cacheFolder = Path.Combine(Path.GetTempPath(), "TwitchDownloader", "cache");
- List thirdPartyEmotes = new List();
- List firstPartyEmotes = new List();
- List twitchBadges = new List();
- List twitchBits = new List();
-
- thirdPartyEmotes = await TwitchHelper.GetThirdPartyEmotes(chatRoot.streamer.id, cacheFolder, bttv: downloadOptions.BttvEmotes, ffz: downloadOptions.FfzEmotes, stv: downloadOptions.StvEmotes);
- firstPartyEmotes = await TwitchHelper.GetEmotes(comments, cacheFolder);
- twitchBadges = await TwitchHelper.GetChatBadges(chatRoot.streamer.id, cacheFolder);
- twitchBits = await TwitchHelper.GetBits(cacheFolder, chatRoot.streamer.id.ToString());
+
+ // This is the exact same process as in ChatUpdater.cs but not in a task oriented manner
+ // TODO: Combine this with ChatUpdater in a different file
+ List thirdPartyEmotes = await TwitchHelper.GetThirdPartyEmotes(chatRoot.streamer.id, downloadOptions.TempFolder, bttv: downloadOptions.BttvEmotes, ffz: downloadOptions.FfzEmotes, stv: downloadOptions.StvEmotes);
+ List firstPartyEmotes = await TwitchHelper.GetEmotes(comments, downloadOptions.TempFolder);
+ List twitchBadges = await TwitchHelper.GetChatBadges(chatRoot.streamer.id, downloadOptions.TempFolder);
+ List twitchBits = await TwitchHelper.GetBits(downloadOptions.TempFolder, chatRoot.streamer.id.ToString());
foreach (TwitchEmote emote in thirdPartyEmotes)
{
@@ -296,7 +301,7 @@ public async Task DownloadAsync(IProgress progress, Cancellation
newEmote.name = emote.Name;
newEmote.width = emote.Width / emote.ImageScale;
newEmote.height = emote.Height / emote.ImageScale;
- thirdPartyReturnList.Add(newEmote);
+ chatRoot.embeddedData.thirdParty.Add(newEmote);
}
foreach (TwitchEmote emote in firstPartyEmotes)
{
@@ -306,14 +311,14 @@ public async Task DownloadAsync(IProgress progress, Cancellation
newEmote.data = emote.ImageData;
newEmote.width = emote.Width / emote.ImageScale;
newEmote.height = emote.Height / emote.ImageScale;
- firstPartyReturnList.Add(newEmote);
+ chatRoot.embeddedData.firstParty.Add(newEmote);
}
foreach (ChatBadge badge in twitchBadges)
{
EmbedChatBadge newBadge = new EmbedChatBadge();
newBadge.name = badge.Name;
newBadge.versions = badge.VersionsData;
- badgesReturnList.Add(newBadge);
+ chatRoot.embeddedData.twitchBadges.Add(newBadge);
}
foreach (CheerEmote bit in twitchBits)
{
@@ -331,193 +336,24 @@ public async Task DownloadAsync(IProgress progress, Cancellation
newEmote.height = emotePair.Value.Height / emotePair.Value.ImageScale;
newBit.tierList.Add(emotePair.Key, newEmote);
}
- bitsReturnList.Add(newBit);
+ chatRoot.embeddedData.twitchBits.Add(newBit);
}
-
- chatRoot.embeddedData.thirdParty = thirdPartyReturnList;
- chatRoot.embeddedData.firstParty = firstPartyReturnList;
- chatRoot.embeddedData.twitchBadges = badgesReturnList;
- chatRoot.embeddedData.twitchBits = bitsReturnList;
}
- if (downloadOptions.DownloadFormat == DownloadFormat.Json)
+ switch (downloadOptions.DownloadFormat)
{
- using (TextWriter writer = File.CreateText(downloadOptions.Filename))
- {
- var serializer = new JsonSerializer();
- serializer.Serialize(writer, chatRoot);
- }
- }
- else if (downloadOptions.DownloadFormat == DownloadFormat.Text)
- {
- using (StreamWriter sw = new StreamWriter(downloadOptions.Filename))
- {
- foreach (var comment in chatRoot.comments)
- {
- string username = comment.commenter.display_name;
- string message = comment.message.body;
- if (downloadOptions.TimeFormat == TimestampFormat.Utc)
- {
- string timestamp = comment.created_at.ToString("u").Replace("Z", " UTC");
- sw.WriteLine(String.Format("[{0}] {1}: {2}", timestamp, username, message));
- }
- else if (downloadOptions.TimeFormat == TimestampFormat.Relative)
- {
- TimeSpan time = new TimeSpan(0, 0, (int)comment.content_offset_seconds);
- string timestamp = time.ToString(@"h\:mm\:ss");
- sw.WriteLine(String.Format("[{0}] {1}: {2}", timestamp, username, message));
- }
- else if (downloadOptions.TimeFormat == TimestampFormat.None)
- {
- sw.WriteLine(String.Format("{0}: {1}", username, message));
- }
- }
-
- sw.Flush();
- sw.Close();
- }
+ case ChatFormat.Json:
+ ChatJson.Serialize(downloadOptions.Filename, chatRoot);
+ break;
+ case ChatFormat.Html:
+ await ChatHtml.SerializeAsync(downloadOptions.Filename, chatRoot, downloadOptions.EmbedData);
+ break;
+ case ChatFormat.Text:
+ await ChatText.SerializeAsync(downloadOptions.Filename, chatRoot, downloadOptions.TimeFormat);
+ break;
+ default:
+ throw new NotImplementedException("Requested output chat format is not implemented");
}
- else if (downloadOptions.DownloadFormat == DownloadFormat.Html)
- {
- Dictionary thirdEmoteData = null;
- EmoteResponse emotes = await TwitchHelper.GetThirdPartyEmoteData(chatRoot.streamer.id.ToString(), true, true, true);
- thirdEmoteData = new Dictionary();
- List itemList = new List();
- itemList.AddRange(emotes.BTTV);
- itemList.AddRange(emotes.FFZ);
- itemList.AddRange(emotes.STV);
-
- foreach (var item in itemList)
- {
- if (!thirdEmoteData.ContainsKey(item.Code))
- {
- if (downloadOptions.EmbedData)
- {
- EmbedEmoteData embedEmoteData = chatRoot.embeddedData.thirdParty.FirstOrDefault(x => x.id == item.Id);
- if (embedEmoteData != null)
- {
- embedEmoteData.url = item.ImageUrl.Replace("[scale]", "1");
- thirdEmoteData[item.Code] = embedEmoteData;
- }
- }
- else
- {
- EmbedEmoteData embedEmoteData = new EmbedEmoteData();
- embedEmoteData.url = item.ImageUrl.Replace("[scale]", "1");
- thirdEmoteData[item.Code] = embedEmoteData;
- }
- }
- }
-
- List templateStrings = new List(Properties.Resources.template.Split('\n'));
- StringBuilder finalString = new StringBuilder();
-
- for (int i = 0; i < templateStrings.Count; i++)
- {
- switch (templateStrings[i].TrimEnd('\r', '\n'))
- {
- case "":
- finalString.AppendLine(HttpUtility.HtmlEncode(Path.GetFileNameWithoutExtension(downloadOptions.Filename)));
- break;
- case "/* [CUSTOM CSS] */":
- if (downloadOptions.EmbedData)
- {
- foreach (var emote in chatRoot.embeddedData.firstParty)
- {
- finalString.AppendLine(".first-" + emote.id + " { content:url(\"data:image/png;base64, " + Convert.ToBase64String(emote.data) + "\"); }");
- }
- foreach (var emote in chatRoot.embeddedData.thirdParty)
- {
- finalString.AppendLine(".third-" + emote.id + " { content:url(\"data:image/png;base64, " + Convert.ToBase64String(emote.data) + "\"); }");
- }
- }
- break;
- case "":
- foreach (Comment comment in chatRoot.comments)
- {
- TimeSpan time = new TimeSpan(0, 0, (int)comment.content_offset_seconds);
- string timestamp = time.ToString(@"h\:mm\:ss");
- finalString.Append($"