Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #848 add CLI mode to manually concatenate ts parts #888

Merged
merged 55 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c603097
#tsmerge Update README.md
superbonaci Oct 28, 2023
baf2271
#tsmerge Update Program.cs
superbonaci Oct 28, 2023
9fc7c74
#tsmerge Add MergeTS.cs
superbonaci Oct 28, 2023
fefe961
#tsmerge Add TsMergeArgs.cs
superbonaci Oct 28, 2023
0a00e81
#tsmerge Add TsMerger.cs
superbonaci Oct 28, 2023
550c265
#tsmerge Add TsMergeOptions.cs
superbonaci Oct 28, 2023
0946fcc
Update Program.cs
superbonaci Oct 28, 2023
638d5b1
#tsmerge Update README.md
superbonaci Oct 28, 2023
36cc340
Program.cs Remove newline at the end
superbonaci Oct 28, 2023
8fb6a4d
Delete TwitchDownloaderCLI/Program.cs
superbonaci Oct 28, 2023
68d686b
Try to upload again Program.cs
superbonaci Oct 28, 2023
b1d48b1
Merge pull request #1 from lay295/master
superbonaci Nov 5, 2023
a69c641
Merge pull request #2 from lay295/master
superbonaci Nov 9, 2023
37a2e14
Merge pull request #3 from lay295/master
superbonaci Nov 12, 2023
32e6b0d
Delete TwitchDownloaderCLI/README.md
superbonaci Nov 12, 2023
a192207
Add files via upload
superbonaci Nov 12, 2023
7a41051
Delete TwitchDownloaderCLI/Program.cs
superbonaci Nov 12, 2023
b71b6b8
Add files via upload
superbonaci Nov 12, 2023
b9bfa63
Delete TwitchDownloaderCore/TsMerger.cs
superbonaci Nov 12, 2023
84172c6
Add files via upload
superbonaci Nov 12, 2023
286f71c
Delete TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
superbonaci Nov 12, 2023
e155335
Add files via upload
superbonaci Nov 12, 2023
8288281
Delete TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
superbonaci Nov 12, 2023
f8fbf31
Add files via upload
superbonaci Nov 12, 2023
ee02787
Delete TwitchDownloaderCore/TsMerger.cs
superbonaci Nov 12, 2023
393e3c0
Add files via upload
superbonaci Nov 12, 2023
00a7d4d
Delete TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
superbonaci Nov 12, 2023
81ecdbe
Add files via upload
superbonaci Nov 12, 2023
abe5b40
Delete TwitchDownloaderCore/TsMergeArgs.cs
superbonaci Nov 12, 2023
4c72064
Add files via upload
superbonaci Nov 12, 2023
60b3491
Delete TwitchDownloaderCLI/README.md
superbonaci Nov 12, 2023
263dff8
Add files via upload
superbonaci Nov 12, 2023
b1d95af
Merge pull request #4 from lay295/master
superbonaci Nov 16, 2023
b3f7bea
Merge pull request #5 from lay295/master
superbonaci Nov 18, 2023
92dcb03
Merge pull request #6 from lay295/master
superbonaci Nov 21, 2023
47022d7
Merge pull request #7 from lay295/master
superbonaci Nov 23, 2023
4d9f801
Merge pull request #8 from lay295/master
superbonaci Nov 28, 2023
10fd997
Merge pull request #9 from lay295/master
superbonaci Nov 29, 2023
2dc7026
Merge pull request #10 from lay295/master
superbonaci Nov 30, 2023
b7fe2a7
Merge pull request #11 from lay295/master
superbonaci Dec 1, 2023
4aa7309
Merge pull request #12 from lay295/master
superbonaci Dec 2, 2023
2e1a47d
Merge pull request #13 from lay295/master
superbonaci Dec 9, 2023
3dbebb9
Merge pull request #14 from lay295/master
superbonaci Dec 14, 2023
ba343bd
Merge pull request #15 from lay295/master
superbonaci Dec 15, 2023
a6ef5ee
Merge pull request #16 from lay295/master
superbonaci Dec 19, 2023
14b0728
Merge pull request #17 from lay295/master
superbonaci Dec 31, 2023
5f40948
Merge pull request #18 from lay295/master
superbonaci Jan 22, 2024
a3134c9
Cleanup code and fix Readme entry
ScrubN Jan 23, 2024
592f28f
Merge pull request #19 from lay295/master
superbonaci Jan 23, 2024
a4b4b5c
Merge pull request #20 from lay295/master
superbonaci Jan 30, 2024
5a7fb47
Merge pull request #21 from lay295/master
superbonaci Feb 2, 2024
4d607b3
Merge pull request #22 from lay295/master
superbonaci Feb 5, 2024
a11aa5b
Merge pull request #23 from lay295/master
superbonaci Feb 7, 2024
719e3e4
Merge pull request #24 from lay295/master
superbonaci Feb 13, 2024
98a3771
Cleanup
ScrubN Feb 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using CommandLine;

namespace TwitchDownloaderCLI.Modes.Arguments
{
[Verb("tsmerge", HelpText = "Concatenates multiple .ts/.tsv/.tsa/.m2t/.m2ts (MPEG Transport Stream) files into a single file")]
public class TsMergeArgs : ITwitchDownloaderArgs
{
[Option('i', "input", Required = true, HelpText = "Path a text file containing the absolute paths of the files to concatenate, separated by newlines. M3U/M3U8 is also supported.")]
public string InputList { get; set; }

[Option('o', "output", Required = true, HelpText = "Path to output file.")]
public string OutputFile { get; set; }

[Option("banner", Default = true, HelpText = "Displays a banner containing version and copyright information.")]
public bool? ShowBanner { get; set; }
}
}
35 changes: 35 additions & 0 deletions TwitchDownloaderCLI/Modes/MergeTs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Threading;
using TwitchDownloaderCLI.Modes.Arguments;
using TwitchDownloaderCLI.Tools;
using TwitchDownloaderCore;
using TwitchDownloaderCore.Options;

namespace TwitchDownloaderCLI.Modes
{
internal static class MergeTs
{
internal static void Merge(TsMergeArgs inputOptions)
{
Console.WriteLine("[INFO] The TS merger is experimental and is subject to change without notice in future releases.");

Progress<ProgressReport> progress = new();
progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged;

var mergeOptions = GetMergeOptions(inputOptions);
TsMerger tsMerger = new(mergeOptions, progress);
tsMerger.MergeAsync(new CancellationToken()).Wait();
}

private static TsMergeOptions GetMergeOptions(TsMergeArgs inputOptions)
{
TsMergeOptions mergeOptions = new()
{
OutputFile = inputOptions.OutputFile,
InputFile = inputOptions.InputList
};

return mergeOptions;
}
}
}
5 changes: 3 additions & 2 deletions TwitchDownloaderCLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private static void Main(string[] args)
config.HelpWriter = null; // Use null instead of TextWriter.Null due to how CommandLine works internally
});

var parserResult = parser.ParseArguments<VideoDownloadArgs, ClipDownloadArgs, ChatDownloadArgs, ChatUpdateArgs, ChatRenderArgs, FfmpegArgs, CacheArgs>(preParsedArgs);
var parserResult = parser.ParseArguments<VideoDownloadArgs, ClipDownloadArgs, ChatDownloadArgs, ChatUpdateArgs, ChatRenderArgs, FfmpegArgs, CacheArgs, TsMergeArgs>(preParsedArgs);
parserResult.WithNotParsed(errors => WriteHelpText(errors, parserResult, parser.Settings));

CoreLicensor.EnsureFilesExist(AppContext.BaseDirectory);
Expand All @@ -37,7 +37,8 @@ private static void Main(string[] args)
.WithParsed<ChatUpdateArgs>(UpdateChat.Update)
.WithParsed<ChatRenderArgs>(RenderChat.Render)
.WithParsed<FfmpegArgs>(FfmpegHandler.ParseArgs)
.WithParsed<CacheArgs>(CacheHandler.ParseArgs);
.WithParsed<CacheArgs>(CacheHandler.ParseArgs)
.WithParsed<TsMergeArgs>(MergeTs.Merge);
}

private static void WriteHelpText(IEnumerable<Error> errors, ParserResult<object> parserResult, ParserSettings parserSettings)
Expand Down
21 changes: 21 additions & 0 deletions TwitchDownloaderCLI/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# 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.
Also can concatenate/combine/merge Transport Stream files, either those parts downloaded with the CLI itself or from another source.

- [TwitchDownloaderCLI](#twitchdownloadercli)
- [Arguments for mode videodownload](#arguments-for-mode-videodownload)
Expand All @@ -9,6 +10,7 @@ A cross platform command line tool that can do the main functions of the GUI pro
- [Arguments for mode chatrender](#arguments-for-mode-chatrender)
- [Arguments for mode ffmpeg](#arguments-for-mode-ffmpeg)
- [Arguments for mode cache](#arguments-for-mode-cache)
- [Arguments for mode tsmerge](#arguments-for-mode-tsmerge)
- [Example Commands](#example-commands)
- [Additional Notes](#additional-notes)

Expand Down Expand Up @@ -343,6 +345,18 @@ Other = `1`, Broadcaster = `2`, Moderator = `4`, VIP = `8`, Subscriber = `16`, P
**--banner**
(Default: `true`) Displays a banner containing version and copyright information.

## Arguments for mode tsmerge
#### Concatenates multiple .ts/.tsv/.tsa/.m2t/.m2ts (MPEG Transport Stream) files into a single file

**-i / --input (REQUIRED)**
Path a text file containing the absolute paths of the files to concatenate, separated by newlines. M3U/M3U8 is also supported.

**-o / --output (REQUIRED)**
File the program will output to.

**--banner**
(Default: `true`) Displays a banner containing version and copyright information.

---

## Example Commands
Expand Down Expand Up @@ -394,6 +408,10 @@ Clear the default TwitchDownloader cache folder

./TwitchDownloaderCLI cache --clear

Concatenate several ts files into a single output file

TwitchDownloaderCLI tsmerge -i list.txt -o output.ts

Print the available operations

./TwitchDownloaderCLI help
Expand All @@ -415,3 +433,6 @@ Default true boolean flags must be assigned: `--default-true-flag=false`. Defaul
For Linux users, ensure both `fontconfig` and `libfontconfig1` are installed. `apt-get install fontconfig libfontconfig1` on Ubuntu.

Some distros, like Linux Alpine, lack fonts for some languages (Arabic, Persian, Thai, etc.) If this is the case for you, install additional fonts families such as [Noto](https://fonts.google.com/noto/specimen/Noto+Sans) or check your distro's wiki page on fonts as it may have an install command for this specific scenario, such as the [Linux Alpine](https://wiki.alpinelinux.org/wiki/Fonts) font page.

The list file for `tsmerge` may contain relative or absolute paths, with one path per line.
Alternatively, the list file may also be an M3U8 playlist file.
8 changes: 8 additions & 0 deletions TwitchDownloaderCore/Options/TsMergeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TwitchDownloaderCore.Options
{
public class TsMergeOptions
{
public string OutputFile { get; set; }
public string InputFile { get; set; }
}
}
142 changes: 142 additions & 0 deletions TwitchDownloaderCore/TsMerger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using TwitchDownloaderCore.Options;
using TwitchDownloaderCore.Tools;

namespace TwitchDownloaderCore
{
public sealed class TsMerger
{
private readonly TsMergeOptions mergeOptions;
private readonly IProgress<ProgressReport> _progress;

public TsMerger(TsMergeOptions tsMergeOptions, IProgress<ProgressReport> progress)
{
mergeOptions = tsMergeOptions;
_progress = progress;
}

public async Task MergeAsync(CancellationToken cancellationToken)
{
if (!File.Exists(mergeOptions.InputFile))
{
throw new FileNotFoundException("Input file does not exist");
}

var isM3U8 = false;
var fileList = new List<string>();
await using (var fs = File.Open(mergeOptions.InputFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using var sr = new StreamReader(fs);
while (await sr.ReadLineAsync() is { } line)
{
if (string.IsNullOrWhiteSpace(line)) continue;

if (isM3U8)
{
if (line.StartsWith('#')) continue;
}
else
{
if (line.StartsWith("#EXTM3U")) isM3U8 = true;
}

fileList.Add(line);
}
}

_progress.Report(new ProgressReport(ReportType.SameLineStatus, "Verifying Parts 0% [1/2]"));

await VerifyVideoParts(fileList, cancellationToken);

_progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Combining Parts 0% [2/2]" });

await CombineVideoParts(fileList, cancellationToken);

_progress.Report(new ProgressReport(100));
}

private async Task VerifyVideoParts(IReadOnlyCollection<string> fileList, CancellationToken cancellationToken)
{
var failedParts = new List<string>();
var partCount = fileList.Count;
var doneCount = 0;

foreach (var part in fileList)
{
var isValidTs = await VerifyVideoPart(part);
if (!isValidTs)
{
failedParts.Add(part);
}

doneCount++;
var percent = (int)(doneCount / (double)partCount * 100);
_progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Verifying Parts {percent}% [1/2]"));
_progress.Report(new ProgressReport(percent));

cancellationToken.ThrowIfCancellationRequested();
}

if (failedParts.Count != 0)
{
if (failedParts.Count == fileList.Count)
{
// Every video part returned corrupted, probably a false positive.
return;
}

_progress.Report(new ProgressReport(ReportType.Log, $"The following TS files are invalid or corrupted: {string.Join(", ", failedParts)}"));
}
}

private static async Task<bool> VerifyVideoPart(string filePath)
{
const int TS_PACKET_LENGTH = 188; // MPEG TS packets are made of a header and a body: [ 4B ][ 184B ] - https://tsduck.io/download/docs/mpegts-introduction.pdf

if (!File.Exists(filePath))
{
return false;
}

await using var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var fileLength = fs.Length;
if (fileLength == 0 || fileLength % TS_PACKET_LENGTH != 0)
{
return false;
}

return true;
}

private async Task CombineVideoParts(IReadOnlyCollection<string> fileList, CancellationToken cancellationToken)
{
DriveInfo outputDrive = DriveHelper.GetOutputDrive(mergeOptions.OutputFile);
string outputFile = mergeOptions.OutputFile;

int partCount = fileList.Count;
int doneCount = 0;

await using var outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write, FileShare.Read);
foreach (var partFile in fileList)
{
await DriveHelper.WaitForDrive(outputDrive, _progress, cancellationToken);

await using (var fs = File.Open(partFile, FileMode.Open, FileAccess.Read, FileShare.Read))
{
await fs.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
}

doneCount++;
int percent = (int)(doneCount / (double)partCount * 100);
_progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Combining Parts {percent}% [2/2]"));
_progress.Report(new ProgressReport(percent));

cancellationToken.ThrowIfCancellationRequested();
}
}
}
}