Skip to content

Commit 277f996

Browse files
Starrahclanstycubic-dev-ai[bot]
authored
feat: 导入歌曲时,将图片转换为AssetBundle格式(默认开启,可关闭);以及优化ImageToAbTool、ModifyId的逻辑。 (#61)
* [R] 抽取和分离CreateTextureAssetBundle的逻辑 * [+] 引入新的配置项:ConvertJacketToAssetBundle,默认开启,行为是会在导入歌曲时自动把图片打包为ab * [+] SetMusicJacketApi,根据全局配置项,决定是否在导入时把图片转为ab;ModifyId,在歌曲已为ab格式的封面图的情况下,会重打包ab。 * [+] ImageToAbTool,优化输出目录的计算逻辑,和新增“是否需要删除原有的PNG/JPG图片?”的选项, * [F] 修复各种小问题 * fix: 一些可能的小问题 * fix: 应该是原本存在的问题,没有格式化传入的 id。只是测试的时候都是用的六位数做文件名所以没发现 * chore: JacketPath 改成可空 * fix: 路径大小写 * Update MaiChartManager/Controllers/Tools/ImageToAbToolController.cs Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * fixup --------- Co-authored-by: Clansty <i@gao4.pw> Co-authored-by: 凌莞~(=^▽^=) <opensource@c5y.moe> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
1 parent 6bd2b8e commit 277f996

18 files changed

Lines changed: 288 additions & 94 deletions

‎MaiChartManager/Config.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class Config
2727
public bool IgnoreLevel { get; set; } = false;
2828
public bool DisableBga { get; set; } = false;
2929
public bool UseLegacyMaiLib { get; set; } = false;
30+
public bool ConvertJacketToAssetBundle { get; set; } = true;
3031
public int UiZoom { get; set; } = 0;
3132

3233
public void Save()

‎MaiChartManager/Controllers/App/SettingsController.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class SettingsDto
1111
public bool IgnoreLevel { get; set; }
1212
public bool DisableBga { get; set; }
1313
public bool UseLegacyMaiLib { get; set; }
14+
public bool ConvertJacketToAssetBundle { get; set; }
1415
public int UiZoom { get; set; }
1516
public double TargetDpiScale { get; set; }
1617
}
@@ -30,6 +31,7 @@ public SettingsDto GetSettings()
3031
IgnoreLevel = StaticSettings.Config.IgnoreLevel,
3132
DisableBga = StaticSettings.Config.DisableBga,
3233
UseLegacyMaiLib = StaticSettings.Config.UseLegacyMaiLib,
34+
ConvertJacketToAssetBundle = StaticSettings.Config.ConvertJacketToAssetBundle,
3335
UiZoom = StaticSettings.Config.UiZoom,
3436
TargetDpiScale = Browser.TargetDpiScale,
3537
};
@@ -44,6 +46,7 @@ public void SetSettings([FromBody] SettingsDto dto)
4446
StaticSettings.Config.IgnoreLevel = dto.IgnoreLevel;
4547
StaticSettings.Config.DisableBga = dto.DisableBga;
4648
StaticSettings.Config.UseLegacyMaiLib = dto.UseLegacyMaiLib;
49+
StaticSettings.Config.ConvertJacketToAssetBundle = dto.ConvertJacketToAssetBundle;
4750
StaticSettings.Config.Save();
4851
}
4952
}

‎MaiChartManager/Controllers/Music/MusicController.cs‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Diagnostics;
1+
using System.Diagnostics;
22
using AssetStudio;
33
using MaiChartManager.Models;
44
using MaiChartManager.Utils;
@@ -157,27 +157,39 @@ public string AddMusic(int id, string assetDir)
157157
[HttpPut]
158158
public string SetMusicJacket(int id, IFormFile file, string assetDir)
159159
{
160-
var nonDxId = id % 10000;
161160
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
162161
if (!MusicXml.jacketExtensions.Contains(ext[1..]))
163162
{
164163
return Locale.UnsupportedImageFormat;
165164
}
166165

167166
var music = settings.GetMusic(id, assetDir);
168-
while (music?.JacketPath is not null && System.IO.File.Exists(music.JacketPath))
169-
{
170-
FileSystem.DeleteFile(music.JacketPath, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
171-
}
167+
if (music is null) return "Music not found!";
168+
music.DeleteJacket(); // 删除老的jacket
172169

173170
var abiDir = Path.Combine(StaticSettings.StreamingAssets, assetDir, @"AssetBundleImages\jacket");
174171
Directory.CreateDirectory(abiDir);
175-
var path = Path.Combine(abiDir, $"ui_jacket_{nonDxId:000000}{ext}");
176-
using var write = System.IO.File.Open(path, FileMode.Create);
177-
file.CopyTo(write);
178-
write.Close();
179-
if (music is not null)
172+
173+
if (StaticSettings.Config.ConvertJacketToAssetBundle)
174+
{ // 将图片转为AssetBundle
175+
using var buffer = new MemoryStream();
176+
file.CopyTo(buffer);
177+
var imageBytes = buffer.ToArray();
178+
179+
var assetBundleDir = Path.GetDirectoryName(abiDir)!;
180+
var resultAbPath = AssetBundleCreator.CreateMusicJacketAssetBundles(
181+
imageBytes, assetBundleDir, music.NonDxId);
182+
StaticSettings.AssetBundleJacketMap[music.NonDxId] = resultAbPath;
183+
}
184+
else
185+
{
186+
var path = Path.Combine(abiDir, $"ui_jacket_{music.NonDxId:000000}{ext}");
187+
using var write = System.IO.File.Open(path, FileMode.Create);
188+
file.CopyTo(write);
189+
write.Close();
180190
music.JacketPath = path;
191+
}
192+
181193
return "";
182194
}
183195

‎MaiChartManager/Controllers/Music/MusicTransferController.cs‎

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,13 @@ private void DeleteIfExists(params string[] path)
506506
}
507507
}
508508

509+
private void DeleteAb(string abPath)
510+
{
511+
FileSystem.DeleteFile(abPath, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
512+
if (System.IO.File.Exists(abPath + ".manifest"))
513+
FileSystem.DeleteFile(abPath + ".manifest", UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
514+
}
515+
509516
[HttpPost]
510517
public async Task ModifyId(int id, [FromBody] int newId, string assetDir)
511518
{
@@ -521,16 +528,18 @@ public async Task ModifyId(int id, [FromBody] int newId, string assetDir)
521528
}
522529
var newNonDxId = newId % 10000;
523530

524-
var abJacketTarget = Path.Combine(StaticSettings.StreamingAssets, assetDir, "AssetBundleImages", "jacket", $"ui_jacket_{newNonDxId:000000}.ab");
525-
var abJacketSTarget = Path.Combine(StaticSettings.StreamingAssets, assetDir, "AssetBundleImages", "jacket_s", $"ui_jacket_{newNonDxId:000000}_s.ab");
531+
var abiDir = Path.Combine(StaticSettings.StreamingAssets, assetDir, @"AssetBundleImages\jacket");
532+
var abiSDir = Path.Combine(StaticSettings.StreamingAssets, assetDir, @"AssetBundleImages\jacket_s");
533+
Directory.CreateDirectory(abiDir);
534+
Directory.CreateDirectory(abiSDir);
535+
var abJacketTarget = Path.Combine(abiDir, $"ui_jacket_{newNonDxId:000000}.ab");
536+
var abJacketSTarget = Path.Combine(abiSDir, $"ui_jacket_{newNonDxId:000000}_s.ab");
526537
var acbawbTarget = Path.Combine(StaticSettings.StreamingAssets, assetDir, "SoundData", $"music{newNonDxId:000000}");
527538
var movieTarget = Path.Combine(StaticSettings.StreamingAssets, assetDir, "MovieData", $"{newNonDxId:000000}");
528539
var newMusicDir = Path.Combine(StaticSettings.StreamingAssets, assetDir, "music", $"music{newId:000000}");
529540
DeleteIfExists(abJacketTarget, abJacketTarget + ".manifest", abJacketSTarget, abJacketSTarget + ".manifest", acbawbTarget + ".acb", acbawbTarget + ".awb", movieTarget + ".dat", movieTarget + ".mp4", newMusicDir);
530-
var abiDir = Path.Combine(StaticSettings.StreamingAssets, assetDir, @"AssetBundleImages\jacket");
531-
Directory.CreateDirectory(abiDir);
532541

533-
// jacket
542+
#region 移动或重打包封面图
534543
var jacketSourcePath = music.JacketPath is not null ? music.JacketPath : music.PseudoAssetBundleJacket;
535544
if (jacketSourcePath is not null)
536545
{
@@ -542,29 +551,51 @@ public async Task ModifyId(int id, [FromBody] int newId, string assetDir)
542551
}
543552
else if (music.AssetBundleJacket is not null)
544553
{
545-
// 否则需要执行转换逻辑
546-
var localJacketTarget = Path.Combine(abiDir, $"ui_jacket_{newNonDxId:000000}.png");
547-
logger.LogInformation("Convert jacket: {music.AssetBundleJacket} -> {abJacketTarget}", music.AssetBundleJacket, abJacketTarget);
548-
System.IO.File.WriteAllBytes(localJacketTarget, music.GetMusicJacketPngData()!);
549-
FileSystem.DeleteFile(music.AssetBundleJacket, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
550-
// AB→PNG: the old .ab.manifest no longer has a matching .ab at the new ID, so just delete it instead of moving
551-
if (System.IO.File.Exists(music.AssetBundleJacket + ".manifest"))
554+
var oldAb = music.AssetBundleJacket!;
555+
var oldSmallAb = GetAssetBundleJacketSmallPath(oldAb);
556+
var idPad = $"{newNonDxId:000000}";
557+
logger.LogInformation("Repack jacket AB: {oldMainAb} -> {abJacketTarget}", oldAb, abJacketTarget);
558+
559+
// 重打包大jacket
560+
AssetBundleCreator.RepackTextureAssetBundle(
561+
oldAb,
562+
abJacketTarget,
563+
$"UI_Jacket_{idPad}",
564+
$"assets/assetbundle/jacket/ui_jacket_{idPad}.png",
565+
$"jacket/ui_jacket_{idPad}.ab");
566+
567+
// 对小jacket:如果存在,重打包;如果不存在,则从png重新缩放,重新CreateTextureAssetBundle
568+
if (oldSmallAb is not null && System.IO.File.Exists(oldSmallAb))
552569
{
553-
FileSystem.DeleteFile(music.AssetBundleJacket + ".manifest", UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
570+
AssetBundleCreator.RepackTextureAssetBundle(
571+
oldSmallAb,
572+
abJacketSTarget,
573+
$"UI_Jacket_{idPad}_s",
574+
$"assets/assetbundle/jacket_s/ui_jacket_{idPad}_s.png",
575+
$"jacket_s/ui_jacket_{idPad}_s.ab");
554576
}
555-
556-
// Issue #42: also clean up the companion jacket_s AB so it doesn't stay orphaned under the old ID
557-
var oldJacketSPath = GetAssetBundleJacketSmallPath(music.AssetBundleJacket);
558-
if (oldJacketSPath is not null && System.IO.File.Exists(oldJacketSPath))
577+
else
559578
{
560-
FileSystem.DeleteFile(oldJacketSPath, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
561-
if (System.IO.File.Exists(oldJacketSPath + ".manifest"))
562-
{
563-
FileSystem.DeleteFile(oldJacketSPath + ".manifest", UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
564-
}
579+
var pngBytes = music.GetMusicJacketPngData();
580+
AssetBundleCreator.CreateTextureAssetBundle(
581+
pngBytes,
582+
abJacketSTarget,
583+
$"UI_Jacket_{idPad}_s",
584+
$"assets/assetbundle/jacket_s/ui_jacket_{idPad}_s.png",
585+
$"jacket_s/ui_jacket_{idPad}_s.ab",
586+
resizeWidth: 200,
587+
resizeHeight: 200);
565588
}
589+
590+
DeleteAb(oldAb);
591+
if (oldSmallAb is not null) DeleteAb(oldSmallAb);
592+
593+
StaticSettings.AssetBundleJacketMap.Remove(music.NonDxId);
594+
StaticSettings.AssetBundleJacketMap[newNonDxId] = abJacketTarget;
566595
}
596+
#endregion
567597

598+
#region 移动音频和视频
568599
// 我也不知道它需不需要重新保存,先直接移动试试
569600
// 是可以的
570601
if (StaticSettings.AcbAwb.TryGetValue($"music{music.NonDxId:000000}.acb", out var acb))
@@ -585,6 +616,7 @@ public async Task ModifyId(int id, [FromBody] int newId, string assetDir)
585616
logger.LogInformation("Move movie: {movie} -> {movieTarget}", movie, movieTarget);
586617
FileSystem.MoveFile(movie, movieTarget + Path.GetExtension(movie), UIOption.OnlyErrorDialogs);
587618
}
619+
#endregion
588620

589621
// 谱面
590622
var oldMusicDir = Path.GetDirectoryName(music.FilePath)!;

‎MaiChartManager/Controllers/Tools/ImageToAbToolController.cs‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace MaiChartManager.Controllers.Tools;
66

77
[ApiController]
88
[Route("MaiChartManagerServlet/[action]Api")]
9-
public partial class ImageToAbToolController(ILogger<ImageToAbToolController> logger) : ControllerBase
9+
public partial class ImageToAbToolController(StaticSettings settings, ILogger<ImageToAbToolController> logger) : ControllerBase
1010
{
1111
[GeneratedRegex(@"^(?<id>\d+)\.(png|jpg|jpeg)$", RegexOptions.IgnoreCase)]
1212
private static partial Regex NumericFileRegex();
@@ -44,6 +44,17 @@ public async Task ImageToAbTool()
4444
await WriteEvent(ImageToAbEventType.Error, Locale.FileNotSelected);
4545
return;
4646
}
47+
48+
// 所选择的路径是否是正规的OPT内jacket路径。方法是判断路径结尾是否是AssetBundleImages\jacket
49+
var isIngameJacketPath = selectedPath.TrimEnd('\\').EndsWith(@"AssetBundleImages\jacket", StringComparison.OrdinalIgnoreCase);
50+
51+
var deleteOriginalPngAfterSuccess = false;
52+
if (isIngameJacketPath)
53+
{
54+
deleteOriginalPngAfterSuccess = MessageBox.Show(
55+
Locale.ImageToAbDeleteOriginalPngQuestion, Locale.ImageToAb, MessageBoxButtons.YesNo,
56+
MessageBoxIcon.Question) == DialogResult.Yes;
57+
}
4758

4859
var candidates = Directory.EnumerateFiles(selectedPath)
4960
.Select(path => new
@@ -56,13 +67,13 @@ public async Task ImageToAbTool()
5667
var numericMatch = NumericFileRegex().Match(x.Name);
5768
if (numericMatch.Success)
5869
{
59-
return new ImageTaskItem(x.Path, numericMatch.Groups["id"].Value);
70+
return new ImageTaskItem(x.Path, numericMatch.Groups["id"].Value.PadLeft(6, '0'));
6071
}
6172

6273
var uiJacketMatch = UiJacketFileRegex().Match(x.Name);
6374
if (uiJacketMatch.Success)
6475
{
65-
return new ImageTaskItem(x.Path, uiJacketMatch.Groups["id"].Value);
76+
return new ImageTaskItem(x.Path, uiJacketMatch.Groups["id"].Value.PadLeft(6, '0'));
6677
}
6778

6879
return null;
@@ -79,8 +90,9 @@ await WriteEvent(
7990
return;
8091
}
8192

82-
var jacketDir = Path.Combine(selectedPath, "jacket");
83-
var jacketSmallDir = Path.Combine(selectedPath, "jacket_s");
93+
var outputRootDir = isIngameJacketPath ? Path.GetDirectoryName(selectedPath)! : selectedPath;
94+
var jacketDir = Path.Combine(outputRootDir, "jacket");
95+
var jacketSmallDir = Path.Combine(outputRootDir, "jacket_s");
8496
Directory.CreateDirectory(jacketDir);
8597
Directory.CreateDirectory(jacketSmallDir);
8698

@@ -119,8 +131,22 @@ await WriteEvent(
119131
{
120132
logger.LogError(ex, "Failed to create AB for image {ImagePath}", item.FilePath);
121133
failures.Add($"{Path.GetFileName(item.FilePath)}: {ex.Message}");
134+
continue;
135+
}
136+
if (deleteOriginalPngAfterSuccess)
137+
{
138+
try
139+
{ // 删除原始文件。如果删除失败,也不要抛异常,只是打个警告
140+
System.IO.File.Delete(item.FilePath);
141+
}
142+
catch (Exception ex)
143+
{
144+
logger.LogWarning(ex, "Failed to delete source PNG after ImageToAb: {Path}", item.FilePath);
145+
}
122146
}
123147
}
148+
149+
await settings.RescanAll(); // rescan all
124150

125151
if (failures.Count > 0)
126152
{

‎MaiChartManager/Front/src/client/apiGen.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ export interface SettingsDto {
403403
ignoreLevel?: boolean;
404404
disableBga?: boolean;
405405
useLegacyMaiLib?: boolean;
406+
convertJacketToAssetBundle?: boolean;
406407
/** @format int32 */
407408
uiZoom?: number;
408409
/** @format double */

‎MaiChartManager/Front/src/locales/en.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ settings:
618618
yuv420p: Use YUV420P color space when converting USM
619619
noScale: Don't scale video to 1080 width
620620
useLegacyMaiLib: Use legacy chart converter for import/export (compatibility mode)
621+
convertJacketToAssetBundle: Use native AssetBundle format for music jackets
621622
updateChannel: Update Channel
622623
updateChannelSlow: Stable
623624
updateChannelCi: Fast

‎MaiChartManager/Front/src/locales/zh-TW.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ settings:
579579
yuv420p: 轉換 USM 時使用 YUV420P 顏色空間
580580
noScale: 不要縮放影片到 1080 寬度
581581
useLegacyMaiLib: 匯入/匯出時,使用舊版轉譜器進行轉譜(相容模式)
582+
convertJacketToAssetBundle: 歌曲封面圖使用原生AssetBundle格式
582583
updateChannel: 更新通道
583584
updateChannelSlow: 穩定
584585
updateChannelCi: 快速

‎MaiChartManager/Front/src/locales/zh.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ settings:
573573
yuv420p: 转换 USM 时使用 YUV420P 颜色空间
574574
noScale: 不要缩放视频到 1080 宽度
575575
useLegacyMaiLib: 导入/导出时,使用旧版转谱器进行转谱(兼容模式)
576+
convertJacketToAssetBundle: 歌曲封面图使用原生AssetBundle格式
576577
updateChannel: 更新通道
577578
updateChannelSlow: 稳定
578579
updateChannelCi: 快速

‎MaiChartManager/Front/src/views/Settings/ImportOptionsSection.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default defineComponent({
2929
</div>
3030
<CheckBox v-model:value={appSettings.value.yuv420p} onChange={onSettingChange}>{t('settings.yuv420p')}</CheckBox>
3131
<CheckBox v-model:value={appSettings.value.noScale} onChange={onSettingChange}>{t('settings.noScale')}</CheckBox>
32+
<CheckBox v-model:value={appSettings.value.convertJacketToAssetBundle} onChange={onSettingChange}>{t('settings.convertJacketToAssetBundle')}</CheckBox>
3233
<CheckBox v-model:value={appSettings.value.useLegacyMaiLib} onChange={onSettingChange}>{t('settings.useLegacyMaiLib')}</CheckBox>
3334
</div>
3435
</div>

0 commit comments

Comments
 (0)