2025-12-19 15:58:45 -07:00
|
|
|
using Mono.Options;
|
2025-04-13 15:18:03 -06:00
|
|
|
using Repackinator.Core.Helpers;
|
2025-12-19 15:25:06 -07:00
|
|
|
using XboxToolkit;
|
|
|
|
|
using XboxToolkit.Interface;
|
2025-04-13 15:18:03 -06:00
|
|
|
using Repackinator.Core.Models;
|
2023-01-04 21:08:49 -08:00
|
|
|
|
2025-12-19 15:58:45 -07:00
|
|
|
namespace Repackinator.Shell.Console
|
2023-01-04 21:08:49 -08:00
|
|
|
{
|
2025-04-13 15:18:03 -06:00
|
|
|
public static class ConsoleConvert
|
2023-01-04 21:08:49 -08:00
|
|
|
{
|
2023-01-11 20:44:58 +01:00
|
|
|
public const string Action = "Convert";
|
2023-01-04 21:08:49 -08:00
|
|
|
public static string Input { get; set; } = string.Empty;
|
2025-12-19 16:23:18 -07:00
|
|
|
public static string Output { get; set; } = string.Empty;
|
2025-11-22 17:30:18 -08:00
|
|
|
public static bool Scrub { get; set; } = false;
|
|
|
|
|
public static bool TrimScrub { get; set; } = false;
|
|
|
|
|
public static bool Compress { get; set; } = false;
|
2024-10-08 17:21:55 -06:00
|
|
|
public static bool NoSplit { get; set; } = false;
|
2023-01-04 21:08:49 -08:00
|
|
|
public static bool ShowHelp { get; set; } = false;
|
|
|
|
|
public static bool Wait { get; set; } = false;
|
2023-03-12 21:12:45 -05:00
|
|
|
public static bool Quiet { get; set; } = false;
|
2023-01-04 21:08:49 -08:00
|
|
|
|
|
|
|
|
public static OptionSet GetOptions()
|
|
|
|
|
{
|
|
|
|
|
return new OptionSet {
|
|
|
|
|
{ "i|input=", "Input file", i => Input = i },
|
2025-12-19 16:23:18 -07:00
|
|
|
{ "o|output=", "Output file or directory (optional, defaults to 'Converted' subfolder)", o => Output = o },
|
2025-11-22 17:30:18 -08:00
|
|
|
{ "c|compress", "Compress (CCI)", c => Compress = c != null },
|
|
|
|
|
{ "s|scrub", "Scrub", s => Scrub = s != null },
|
|
|
|
|
{ "t|trim", "TrimScrub", t => TrimScrub = t != null },
|
2024-10-08 17:21:55 -06:00
|
|
|
{ "n|nosplit", "No Split of output file", n => NoSplit = n != null },
|
2023-01-04 21:08:49 -08:00
|
|
|
{ "h|help", "show help", h => ShowHelp = h != null },
|
2023-03-12 21:12:45 -05:00
|
|
|
{ "w|wait", "Wait on exit", w => Wait = w != null },
|
|
|
|
|
{ "q|quiet", "Suppress status output", q => Quiet = q != null }
|
2023-01-04 21:08:49 -08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void ShowOptionDescription()
|
|
|
|
|
{
|
2023-01-11 20:44:58 +01:00
|
|
|
System.Console.WriteLine();
|
|
|
|
|
System.Console.WriteLine("Convert Action...");
|
|
|
|
|
System.Console.WriteLine();
|
|
|
|
|
System.Console.WriteLine("This action is used to convert one xbox disk image format to another.");
|
|
|
|
|
System.Console.WriteLine();
|
|
|
|
|
GetOptions().WriteOptionDescriptions(System.Console.Out);
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void Process(string version, string[] args)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var options = GetOptions();
|
|
|
|
|
options.Parse(args);
|
|
|
|
|
if (ShowHelp)
|
|
|
|
|
{
|
2023-01-11 20:44:58 +01:00
|
|
|
ConsoleUtil.ShowHelpHeaderForAction(version, Action, options);
|
2023-01-04 21:08:49 -08:00
|
|
|
ConsoleUtil.ProcessWait(Wait);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 17:31:03 -07:00
|
|
|
if (string.IsNullOrEmpty(Input))
|
|
|
|
|
{
|
|
|
|
|
throw new OptionException("Input file not specified.", "input");
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-04 21:08:49 -08:00
|
|
|
var input = Path.GetFullPath(Input);
|
|
|
|
|
if (!File.Exists(input))
|
|
|
|
|
{
|
2025-12-19 17:31:03 -07:00
|
|
|
throw new OptionException("Input file does not exist.", "input");
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var outputNameWithoutExtension = Path.GetFileNameWithoutExtension(input);
|
|
|
|
|
var subExtension = Path.GetExtension(outputNameWithoutExtension);
|
|
|
|
|
if (subExtension.Equals(".1") || subExtension.Equals(".2"))
|
|
|
|
|
{
|
|
|
|
|
outputNameWithoutExtension = Path.GetFileNameWithoutExtension(outputNameWithoutExtension);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 18:53:39 -07:00
|
|
|
string? outputPath;
|
2025-12-19 16:23:18 -07:00
|
|
|
string outputFile;
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(Output))
|
2023-01-04 21:08:49 -08:00
|
|
|
{
|
2025-12-19 16:23:18 -07:00
|
|
|
var outputFullPath = Path.GetFullPath(Output);
|
|
|
|
|
if (Directory.Exists(outputFullPath) || outputFullPath.EndsWith(Path.DirectorySeparatorChar) || outputFullPath.EndsWith(Path.AltDirectorySeparatorChar))
|
|
|
|
|
{
|
|
|
|
|
// Output is a directory
|
|
|
|
|
outputPath = outputFullPath;
|
|
|
|
|
Directory.CreateDirectory(outputPath);
|
|
|
|
|
var extension = Compress ? ".cci" : ".iso";
|
|
|
|
|
outputFile = Path.Combine(outputPath, $"{outputNameWithoutExtension}{extension}");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Output is a file path
|
|
|
|
|
outputPath = Path.GetDirectoryName(outputFullPath) ?? Path.GetDirectoryName(input) ?? "";
|
|
|
|
|
if (!string.IsNullOrEmpty(outputPath) && !Directory.Exists(outputPath))
|
|
|
|
|
{
|
|
|
|
|
Directory.CreateDirectory(outputPath);
|
|
|
|
|
}
|
|
|
|
|
outputFile = outputFullPath;
|
|
|
|
|
// Ensure correct extension
|
|
|
|
|
if (Compress && !outputFile.EndsWith(".cci", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
outputFile = Path.ChangeExtension(outputFile, ".cci");
|
|
|
|
|
}
|
|
|
|
|
else if (!Compress && !outputFile.EndsWith(".iso", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
outputFile = Path.ChangeExtension(outputFile, ".iso");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Default behavior: use Converted subfolder
|
|
|
|
|
outputPath = Path.GetDirectoryName(input);
|
|
|
|
|
if (outputPath == null)
|
|
|
|
|
{
|
|
|
|
|
throw new OptionException("Unable to get directory from input.", "input");
|
|
|
|
|
}
|
|
|
|
|
outputPath = Path.Combine(outputPath, $"Converted");
|
|
|
|
|
Directory.CreateDirectory(outputPath);
|
|
|
|
|
var extension = Compress ? ".cci" : ".iso";
|
|
|
|
|
outputFile = Path.Combine(outputPath, $"{outputNameWithoutExtension}{extension}");
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
|
|
|
|
|
2025-12-19 16:23:18 -07:00
|
|
|
if (!Quiet)
|
|
|
|
|
{
|
|
|
|
|
System.Console.WriteLine("Converting:");
|
|
|
|
|
var slices = ContainerUtility.GetSlicesFromFile(input);
|
|
|
|
|
foreach (var slice in slices)
|
|
|
|
|
{
|
|
|
|
|
System.Console.WriteLine(Path.GetFileName(slice));
|
|
|
|
|
}
|
|
|
|
|
System.Console.WriteLine($"To: {outputFile}");
|
|
|
|
|
System.Console.WriteLine();
|
|
|
|
|
}
|
2023-01-19 14:35:20 -08:00
|
|
|
|
2023-01-04 21:08:49 -08:00
|
|
|
var previousProgress = -1.0f;
|
|
|
|
|
|
|
|
|
|
if (outputPath != null)
|
|
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
if (!ContainerUtility.TryAutoDetectContainerType(input, out var containerReader) || containerReader == null)
|
2023-01-04 21:08:49 -08:00
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
throw new Exception("Unable to detect container type.");
|
|
|
|
|
}
|
|
|
|
|
using (containerReader)
|
2023-01-04 21:08:49 -08:00
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
if (!containerReader.TryMount())
|
2023-01-04 21:08:49 -08:00
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
throw new Exception("Unable to mount container.");
|
|
|
|
|
}
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var processingOptions = ProcessingOptions.OneToOneCopy;
|
|
|
|
|
if (Scrub || TrimScrub) processingOptions |= ProcessingOptions.ScrubSectors;
|
|
|
|
|
if (TrimScrub) processingOptions |= ProcessingOptions.TrimSectors;
|
|
|
|
|
|
|
|
|
|
if (Compress)
|
2023-01-04 21:08:49 -08:00
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
var progress = new Action<float>((p) =>
|
2025-11-22 17:30:18 -08:00
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
var amount = (float)Math.Round(p * 100);
|
|
|
|
|
if (!Quiet && amount != previousProgress)
|
|
|
|
|
{
|
|
|
|
|
if (amount < 10)
|
|
|
|
|
{
|
|
|
|
|
System.Console.Write($"Progress {amount}%");
|
|
|
|
|
}
|
|
|
|
|
else if (amount < 100)
|
|
|
|
|
{
|
|
|
|
|
System.Console.Write($"Progress {amount}%");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
System.Console.Write($"Progress {amount}%");
|
|
|
|
|
}
|
|
|
|
|
System.Console.CursorLeft = 0;
|
|
|
|
|
previousProgress = amount;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!ContainerUtility.ConvertContainerToCCI(containerReader, processingOptions, outputFile, 0, progress))
|
2025-11-22 17:30:18 -08:00
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
throw new Exception("Unable to convert to CCI.");
|
2025-11-22 17:30:18 -08:00
|
|
|
}
|
2025-12-19 15:25:06 -07:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var splitPoint = NoSplit ? 0L : 4L * 1024 * 1024 * 1024; // 4GB
|
|
|
|
|
var progress = new Action<float>((p) =>
|
2025-11-22 17:30:18 -08:00
|
|
|
{
|
2025-12-19 15:25:06 -07:00
|
|
|
var amount = (float)Math.Round(p * 100);
|
|
|
|
|
if (!Quiet && amount != previousProgress)
|
|
|
|
|
{
|
|
|
|
|
if (amount < 10)
|
|
|
|
|
{
|
|
|
|
|
System.Console.Write($"Progress {amount}%");
|
|
|
|
|
}
|
|
|
|
|
else if (amount < 100)
|
|
|
|
|
{
|
|
|
|
|
System.Console.Write($"Progress {amount}%");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
System.Console.Write($"Progress {amount}%");
|
|
|
|
|
}
|
|
|
|
|
System.Console.CursorLeft = 0;
|
|
|
|
|
previousProgress = amount;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!ContainerUtility.ConvertContainerToISO(containerReader, processingOptions, outputFile, splitPoint, progress))
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Unable to convert to ISO.");
|
2025-11-22 17:30:18 -08:00
|
|
|
}
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
2025-12-19 15:25:06 -07:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
containerReader.Dismount();
|
|
|
|
|
}
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-12 21:12:45 -05:00
|
|
|
if (!Quiet)
|
|
|
|
|
{
|
|
|
|
|
System.Console.WriteLine();
|
2025-11-22 17:30:18 -08:00
|
|
|
System.Console.WriteLine("Conversion completed.");
|
2023-03-12 21:12:45 -05:00
|
|
|
}
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
|
|
|
|
catch (OptionException e)
|
|
|
|
|
{
|
2025-12-19 16:19:12 -07:00
|
|
|
ConsoleUtil.ShowOptionException(e, Action, version);
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
2025-12-20 19:58:42 -07:00
|
|
|
|
|
|
|
|
ConsoleUtil.ProcessWait(Wait);
|
2023-01-04 21:08:49 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-19 15:58:45 -07:00
|
|
|
|