Created
January 25, 2026 20:40
-
-
Save elbruno/d321b5b12adf99a2b46dc4fbb51bce03 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env dotnet run | |
| #:package [email protected] | |
| using System; | |
| using System.IO; | |
| using System.Linq; | |
| using System.Text.RegularExpressions; | |
| using Spectre.Console; | |
| const int BufferSize = 80 * 1024; // 80KB buffer for chunked I/O | |
| const string InputFolder = "audios"; | |
| const string OutputFile = "joined_output.mp3"; | |
| // Display banner | |
| AnsiConsole.Write(new FigletText("Audio Joiner").Color(Color.Cyan1)); | |
| AnsiConsole.MarkupLine("[grey]Concatenates MP3 files into a single output[/]\n"); | |
| // Check if input folder exists | |
| if (!Directory.Exists(InputFolder)) | |
| { | |
| AnsiConsole.MarkupLine($"[red]Error:[/] Folder '[yellow]{InputFolder}[/]' not found!"); | |
| AnsiConsole.MarkupLine("[grey]Create the folder and add MP3 files to join.[/]"); | |
| return 1; | |
| } | |
| // Get and sort MP3 files numerically | |
| var mp3Files = Directory.GetFiles(InputFolder, "*.mp3") | |
| .OrderBy(f => ExtractNumber(Path.GetFileNameWithoutExtension(f))) | |
| .ThenBy(f => f) | |
| .ToArray(); | |
| if (mp3Files.Length == 0) | |
| { | |
| AnsiConsole.MarkupLine($"[red]Error:[/] No MP3 files found in '[yellow]{InputFolder}[/]'!"); | |
| return 1; | |
| } | |
| AnsiConsole.MarkupLine($"[green]Found {mp3Files.Length} MP3 files to join[/]\n"); | |
| long totalBytesWritten = 0; | |
| long totalInputSize = mp3Files.Sum(f => new FileInfo(f).Length); | |
| // Process files with progress bar | |
| await AnsiConsole.Progress() | |
| .AutoRefresh(true) | |
| .AutoClear(false) | |
| .HideCompleted(false) | |
| .Columns( | |
| new TaskDescriptionColumn(), | |
| new ProgressBarColumn(), | |
| new PercentageColumn(), | |
| new RemainingTimeColumn(), | |
| new SpinnerColumn()) | |
| .StartAsync(async ctx => | |
| { | |
| var task = ctx.AddTask("[cyan]Joining audio files[/]", maxValue: totalInputSize); | |
| using var outputStream = new FileStream(OutputFile, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize, FileOptions.SequentialScan); | |
| var buffer = new byte[BufferSize]; | |
| foreach (var file in mp3Files) | |
| { | |
| task.Description = $"[cyan]Processing:[/] [white]{Path.GetFileName(file)}[/]"; | |
| using var inputStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.SequentialScan); | |
| // Skip ID3v2 tag if present (starts with "ID3") | |
| long skipBytes = GetId3v2TagSize(inputStream); | |
| if (skipBytes > 0) | |
| { | |
| inputStream.Seek(skipBytes, SeekOrigin.Begin); | |
| } | |
| int bytesRead; | |
| while ((bytesRead = await inputStream.ReadAsync(buffer.AsMemory(0, buffer.Length))) > 0) | |
| { | |
| await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead)); | |
| totalBytesWritten += bytesRead; | |
| task.Increment(bytesRead); | |
| } | |
| } | |
| task.Description = "[green]Complete![/]"; | |
| }); | |
| // Display summary table | |
| var outputInfo = new FileInfo(OutputFile); | |
| var table = new Table() | |
| .Border(TableBorder.Rounded) | |
| .AddColumn(new TableColumn("[cyan]Property[/]").LeftAligned()) | |
| .AddColumn(new TableColumn("[cyan]Value[/]").RightAligned()); | |
| table.AddRow("[white]Output File[/]", $"[green]{OutputFile}[/]"); | |
| table.AddRow("[white]Output Size[/]", $"[yellow]{FormatBytes(outputInfo.Length)}[/]"); | |
| table.AddRow("[white]Files Joined[/]", $"[blue]{mp3Files.Length}[/]"); | |
| table.AddRow("[white]Total Input Size[/]", $"[grey]{FormatBytes(totalInputSize)}[/]"); | |
| AnsiConsole.WriteLine(); | |
| AnsiConsole.Write(new Panel(table) | |
| .Header("[bold green]✓ Summary[/]") | |
| .BorderColor(Color.Green)); | |
| AnsiConsole.MarkupLine("\n[green]Done![/] Audio files joined successfully."); | |
| return 0; | |
| // Extract numeric portion from filename for proper sorting | |
| static int ExtractNumber(string filename) | |
| { | |
| var match = Regex.Match(filename, @"(\d+)"); | |
| return match.Success ? int.Parse(match.Groups[1].Value) : int.MaxValue; | |
| } | |
| // Get ID3v2 tag size (returns 0 if no tag present) | |
| static long GetId3v2TagSize(FileStream stream) | |
| { | |
| byte[] header = new byte[10]; | |
| int bytesRead = stream.Read(header, 0, 10); | |
| stream.Seek(0, SeekOrigin.Begin); | |
| if (bytesRead < 10) return 0; | |
| // Check for "ID3" signature | |
| if (header[0] != 'I' || header[1] != 'D' || header[2] != '3') return 0; | |
| // ID3v2 size is stored in bytes 6-9 as syncsafe integer (7 bits per byte) | |
| long size = ((header[6] & 0x7F) << 21) | | |
| ((header[7] & 0x7F) << 14) | | |
| ((header[8] & 0x7F) << 7) | | |
| (header[9] & 0x7F); | |
| // Add 10 bytes for the header itself | |
| return size + 10; | |
| } | |
| // Format bytes to human-readable string | |
| static string FormatBytes(long bytes) | |
| { | |
| string[] suffixes = ["B", "KB", "MB", "GB"]; | |
| int i = 0; | |
| double size = bytes; | |
| while (size >= 1024 && i < suffixes.Length - 1) | |
| { | |
| size /= 1024; | |
| i++; | |
| } | |
| return $"{size:F2} {suffixes[i]}"; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment