Skip to content

Commit eb085be

Browse files
committed
FEAT: Command line GUI
1 parent 0a9038c commit eb085be

7 files changed

Lines changed: 747 additions & 335 deletions

File tree

BmsToOsu/BmsToOsu.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<PackageReference Include="CommandLineParser" Version="2.9.1" />
1212
<PackageReference Include="log4net" Version="2.0.14" />
1313
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
14+
<PackageReference Include="Terminal.Gui" Version="1.7.2" />
1415
</ItemGroup>
1516

1617
</Project>
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
using System.IO.Compression;
2+
using BmsToOsu.Converter;
3+
using BmsToOsu.Entity;
4+
using BmsToOsu.Utils;
5+
using CommandLine;
6+
using CommandLine.Text;
7+
using log4net;
8+
9+
namespace BmsToOsu.Launcher;
10+
11+
public static class CliArgsLauncher
12+
{
13+
private static readonly ILog Log = LogManager.GetLogger(nameof(CliArgsLauncher));
14+
15+
private static readonly string[] AvailableBmsExt =
16+
{
17+
".bms", ".bml", ".bme", ".bmx"
18+
};
19+
20+
public static void Launch(string[] args)
21+
{
22+
var argsParser = new Parser(with =>
23+
{
24+
with.AutoVersion = false;
25+
with.AutoHelp = true;
26+
with.HelpWriter = null;
27+
});
28+
29+
var result = argsParser.ParseArguments<Option>(args);
30+
31+
result.WithParsed(Convert);
32+
33+
result.WithNotParsed(_ =>
34+
{
35+
var helpText = HelpText.AutoBuild(result, h =>
36+
{
37+
h.AutoHelp = true;
38+
h.AutoVersion = false;
39+
h.AutoVersion = false;
40+
h.AdditionalNewLineAfterOption = false;
41+
h.AddNewLineBetweenHelpSections = false;
42+
h.Heading = "";
43+
h.Copyright = Const.Copyright;
44+
return HelpText.DefaultParsingErrorsHandler(result, h);
45+
}, e => e);
46+
Console.WriteLine(helpText);
47+
});
48+
}
49+
50+
public static void Convert(Option o)
51+
{
52+
o.OutPath = Path.GetFullPath(o.OutPath);
53+
o.InputPath = Path.GetFullPath(o.InputPath);
54+
o.Ffmpeg = string.IsNullOrEmpty(o.Ffmpeg) ? "" : Path.GetFullPath(o.Ffmpeg);
55+
56+
var osz = o.OutPath + ".osz";
57+
58+
#region check options
59+
60+
// avoid removing existing folder
61+
if (Directory.Exists(o.OutPath) && !o.NoRemove)
62+
{
63+
Log.Warn($"{o.OutPath} exists, `--no-remove` will be appended to the parameter");
64+
o.NoRemove = true;
65+
}
66+
67+
if (o.NoCopy && o.GenerateMp3)
68+
{
69+
Log.Error($"`--no-copy` is conflict with `--generate-mp3`");
70+
return;
71+
}
72+
73+
// avoid removing after generation
74+
if (o.NoZip && !o.NoRemove)
75+
{
76+
Log.Warn("`--no-remove` is appended to the parameter");
77+
o.NoRemove = true;
78+
}
79+
80+
// avoid duplication
81+
if (File.Exists(osz))
82+
{
83+
Log.Warn($"{osz} exists, ignoring...");
84+
return;
85+
}
86+
87+
#endregion
88+
89+
#region parse & convert
90+
91+
var converter = new Converter(o);
92+
93+
var bms = Directory
94+
.GetFiles(o.InputPath, "*.*", SearchOption.AllDirectories)
95+
.Where(f => AvailableBmsExt.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase)));
96+
97+
var skippedFileList = new List<string>();
98+
var generationFailedList = new List<string>();
99+
100+
void Proc(params string[] path)
101+
{
102+
try
103+
{
104+
converter.Convert(path);
105+
}
106+
catch (BmsParserException e)
107+
{
108+
lock (skippedFileList) skippedFileList.AddRange(e.FailedList);
109+
}
110+
catch (GenerationFailedException e)
111+
{
112+
lock (generationFailedList) generationFailedList.AddRange(e.FailedList);
113+
}
114+
}
115+
116+
Parallel.ForEach(bms.GroupBy(Path.GetDirectoryName), groupedBms =>
117+
{
118+
if (o.GenerateMp3)
119+
{
120+
Proc(groupedBms.ToArray());
121+
}
122+
else
123+
{
124+
Parallel.ForEach(groupedBms, g => Proc(g));
125+
}
126+
});
127+
128+
#endregion
129+
130+
#region after convertion (e.g. remove temp files)
131+
132+
if (!o.NoCopy)
133+
{
134+
Log.Info("Copying files");
135+
Parallel.ForEach(converter.FilesToCopy, c =>
136+
{
137+
var dest = c.Replace(o.InputPath, o.OutPath);
138+
dest = Path.Join(Path.GetDirectoryName(dest), Path.GetFileName(dest).Escape());
139+
140+
if (!File.Exists(dest))
141+
{
142+
File.Copy(c, dest, true);
143+
}
144+
});
145+
}
146+
147+
if (!o.NoZip && Directory.Exists(o.OutPath))
148+
{
149+
Log.Info($"Creating {osz}");
150+
ZipFile.CreateFromDirectory(o.OutPath, osz, CompressionLevel.Fastest, false);
151+
}
152+
153+
if (!o.NoRemove)
154+
{
155+
Log.Info($"Removing {o.OutPath}");
156+
Directory.Delete(o.OutPath, true);
157+
158+
if (o.OutPath.EndsWith(".osz", StringComparison.OrdinalIgnoreCase))
159+
{
160+
File.Move(osz, o.OutPath);
161+
}
162+
}
163+
164+
if (skippedFileList.Any())
165+
{
166+
Log.Info(new string('-', 60));
167+
Log.Info("Skipped List:");
168+
169+
skippedFileList.ForEach(path => Log.Info(path));
170+
}
171+
172+
if (generationFailedList.Any())
173+
{
174+
Log.Info(new string('-', 60));
175+
Log.Info("Generation Failed List:");
176+
177+
generationFailedList.ForEach(path => Log.Info(path));
178+
}
179+
180+
#endregion
181+
}
182+
}
183+
184+
internal class GenerationFailedException : Exception
185+
{
186+
public readonly List<string> FailedList;
187+
188+
public GenerationFailedException(IEnumerable<string> generationFailedList)
189+
{
190+
FailedList = generationFailedList.ToList();
191+
}
192+
}
193+
194+
internal class BmsParserException : GenerationFailedException
195+
{
196+
public BmsParserException(IEnumerable<string> generationFailedList) : base(generationFailedList)
197+
{
198+
}
199+
}
200+
201+
internal class Converter
202+
{
203+
private readonly Option _option;
204+
private readonly SampleToMp3 _mp3Generator;
205+
private readonly ILog _log;
206+
207+
public readonly List<Task> Tasks = new();
208+
209+
public Converter(Option option)
210+
{
211+
_option = option;
212+
_mp3Generator = new SampleToMp3(_option);
213+
_log = LogManager.GetLogger(nameof(Converter));
214+
}
215+
216+
public readonly HashSet<string> FilesToCopy = new();
217+
218+
private void ConvertOne(BmsFileData data, string mp3Path, HashSet<Sample> excludingSounds)
219+
{
220+
var bmsDir = Path.GetDirectoryName(data.BmsPath) ?? "";
221+
var outputDir = bmsDir.Replace(_option.InputPath, _option.OutPath);
222+
223+
Directory.CreateDirectory(outputDir);
224+
225+
foreach (var includePlate in new[] { true, false })
226+
{
227+
var (osuBeatmap, ftc) = data.ToOsuBeatMap(excludingSounds, _option.NoSv, mp3Path, includePlate);
228+
229+
foreach (var c in ftc)
230+
lock (FilesToCopy)
231+
FilesToCopy.Add(Path.Join(bmsDir, c));
232+
233+
var plate = includePlate ? " (7+1K)" : "";
234+
235+
var osuName = $"{data.Metadata.Title} - {data.Metadata.Artist} - BMS Converted{plate} - {Path.GetFileNameWithoutExtension(data.BmsPath)}.osu";
236+
237+
File.WriteAllText(Path.Join(outputDir, osuName.MakeValidFileName()), osuBeatmap);
238+
}
239+
}
240+
241+
public void Convert(string[] bmsFiles)
242+
{
243+
bmsFiles = bmsFiles.OrderBy(s => s).ToArray();
244+
245+
if (!bmsFiles.Any()) return;
246+
247+
List<Sample>? soundFileList = null;
248+
249+
var dataList = new List<BmsFileData>();
250+
var parseErrorList = new List<string>();
251+
252+
var workPath = Path.GetDirectoryName(bmsFiles[0])!;
253+
254+
foreach (var bmsFilePath in bmsFiles)
255+
{
256+
_log.Info($"Processing {bmsFilePath}");
257+
258+
BmsFileData data;
259+
260+
try
261+
{
262+
data = BmsFileData.FromFile(bmsFilePath);
263+
}
264+
catch (InvalidDataException)
265+
{
266+
parseErrorList.Add(bmsFilePath);
267+
continue;
268+
}
269+
270+
dataList.Add(data);
271+
272+
if (!_option.GenerateMp3) continue;
273+
274+
var soundFiles = data.GetSoundFileList();
275+
276+
soundFileList ??= soundFiles;
277+
soundFileList = soundFileList.Intersect(soundFiles, Sample.Comparer).ToList();
278+
}
279+
280+
if (!dataList.Any()) return;
281+
282+
var filename = _option.GenerateMp3 && soundFileList != null && soundFileList.Any()
283+
? $"{dataList[0].Metadata.Title} - {dataList[0].Metadata.Artist}.mp3".MakeValidFileName()
284+
: "";
285+
286+
foreach (var data in dataList)
287+
{
288+
try
289+
{
290+
ConvertOne(data, filename, new HashSet<Sample>(soundFileList ?? new List<Sample>()));
291+
}
292+
catch (InvalidDataException)
293+
{
294+
parseErrorList.Add(data.BmsPath);
295+
}
296+
}
297+
298+
var mp3 = Path.Join(
299+
Path.GetDirectoryName(dataList[0].BmsPath)!
300+
.Replace(_option.InputPath, _option.OutPath)
301+
, filename
302+
);
303+
304+
if (_option.GenerateMp3)
305+
{
306+
try
307+
{
308+
if (File.Exists(mp3))
309+
{
310+
_log.Warn($"{workPath}: {mp3} exists, skipping...");
311+
}
312+
else
313+
{
314+
if (soundFileList != null && soundFileList.Any())
315+
{
316+
_mp3Generator.GenerateMp3(soundFileList, workPath, mp3);
317+
}
318+
else
319+
{
320+
_log.Warn($"{workPath}: The sampling intersection of the same song is too small, use hit sound instead.");
321+
}
322+
}
323+
}
324+
catch
325+
{
326+
throw new GenerationFailedException(bmsFiles);
327+
}
328+
}
329+
330+
if (parseErrorList.Any())
331+
{
332+
throw new BmsParserException(parseErrorList);
333+
}
334+
}
335+
}

0 commit comments

Comments
 (0)