@@ -55,7 +55,8 @@ public ActionResult<List<MusicListItem>> GetMusicList([FromQuery] string? source
5555 Level = f . Level ,
5656 LevelDecimal = f . LevelDecimal ,
5757 LevelDisplay = f . LevelDisplay ,
58- NotesDesigner = f . NotesDesigner
58+ NotesDesigner = f . NotesDesigner ,
59+ NoteCount = f . NoteCount
5960 } ) . ToArray ( )
6061 } ) . ToList ( ) ;
6162
@@ -334,6 +335,77 @@ public ActionResult ImportJacket([FromQuery] int id, [FromQuery] string assetDir
334335 return Ok ( new { imported = true } ) ;
335336 }
336337
338+ [ HttpPut ]
339+ public ActionResult SetJacket ( [ FromQuery ] int id , [ FromQuery ] string assetDir , IFormFile file )
340+ {
341+ var scanner = scannerService . Scanner ;
342+ if ( scanner == null ) return NotFound ( ) ;
343+
344+ var music = FindMusic ( scanner , id , assetDir ) ;
345+ if ( music == null ) return NotFound ( ) ;
346+
347+ var ddsFileName = $ "CHU_UI_Jacket_{ id : D8} .dds";
348+ var tempPath = Path . Combine ( Path . GetTempPath ( ) , $ "ccm_jacket_{ Guid . NewGuid ( ) } { Path . GetExtension ( file . FileName ) } ") ;
349+ try
350+ {
351+ using ( var fs = System . IO . File . Create ( tempPath ) )
352+ file . CopyTo ( fs ) ;
353+ DdsHelper . ConvertPngToDds ( tempPath , Path . Combine ( music . MusicDirectory , ddsFileName ) ) ;
354+ }
355+ finally
356+ {
357+ try { System . IO . File . Delete ( tempPath ) ; } catch { }
358+ }
359+
360+ var root = music . XmlDoc . SelectSingleNode ( "/MusicData/jaketFile/path" ) ;
361+ if ( root != null ) root . InnerText = ddsFileName ;
362+ music . JacketFileName = ddsFileName ;
363+ music . Save ( ) ;
364+
365+ JacketCache . TryRemove ( music . GetJacketFullPath ( ) ?? "" , out _ ) ;
366+
367+ return Ok ( ) ;
368+ }
369+
370+ [ HttpPut ]
371+ [ DisableRequestSizeLimit ]
372+ public ActionResult SetAudio ( [ FromQuery ] int id , [ FromQuery ] string assetDir , IFormFile file )
373+ {
374+ var scanner = scannerService . Scanner ;
375+ if ( scanner == null ) return NotFound ( ) ;
376+
377+ var music = FindMusic ( scanner , id , assetDir ) ;
378+ if ( music == null ) return NotFound ( ) ;
379+
380+ var tempPath = Path . Combine ( Path . GetTempPath ( ) , $ "ccm_audio_{ Guid . NewGuid ( ) } { Path . GetExtension ( file . FileName ) } ") ;
381+ try
382+ {
383+ using ( var fs = System . IO . File . Create ( tempPath ) )
384+ file . CopyTo ( fs ) ;
385+
386+ var ext = Path . GetExtension ( file . FileName ) . ToLowerInvariant ( ) ;
387+ if ( ext == ".awb" )
388+ {
389+ var sourceRoot = Path . GetDirectoryName ( Path . GetDirectoryName ( music . MusicDirectory ) ) ;
390+ if ( sourceRoot == null ) return BadRequest ( "Cannot determine option root" ) ;
391+ var cueFileDir = Path . Combine ( sourceRoot , "cueFile" , $ "cueFile{ id : D6} ") ;
392+ Directory . CreateDirectory ( cueFileDir ) ;
393+ var awbPath = Path . Combine ( cueFileDir , $ "music{ id : D4} .awb") ;
394+ System . IO . File . Copy ( tempPath , awbPath , true ) ;
395+ }
396+ else
397+ {
398+ AudioHelper . ImportAudioToMusic ( music , tempPath ) ;
399+ }
400+ }
401+ finally
402+ {
403+ try { System . IO . File . Delete ( tempPath ) ; } catch { }
404+ }
405+
406+ return Ok ( ) ;
407+ }
408+
337409 [ HttpPost ]
338410 public ActionResult ImportChart ( [ FromQuery ] int id , [ FromQuery ] string assetDir , [ FromQuery ] int diffIndex )
339411 {
@@ -406,6 +478,63 @@ public ActionResult ImportChart([FromQuery] int id, [FromQuery] string assetDir,
406478 return Ok ( new { imported = true , convertedFrom = ext != "c2s" ? ext : ( string ? ) null , alerts } ) ;
407479 }
408480
481+ [ HttpPut ]
482+ public ActionResult ReplaceChart ( [ FromQuery ] int id , [ FromQuery ] string assetDir , [ FromQuery ] int diffIndex , IFormFile file )
483+ {
484+ var scanner = scannerService . Scanner ;
485+ if ( scanner == null ) return NotFound ( ) ;
486+
487+ var music = FindMusic ( scanner , id , assetDir ) ;
488+ if ( music == null ) return NotFound ( ) ;
489+
490+ var ext = Path . GetExtension ( file . FileName ) . TrimStart ( '.' ) . ToLowerInvariant ( ) ;
491+ var destFileName = $ "{ id : D4} _0{ diffIndex } .c2s";
492+ var destPath = Path . Combine ( music . MusicDirectory , destFileName ) ;
493+ var alerts = new List < string > ( ) ;
494+
495+ using var ms = new MemoryStream ( ) ;
496+ file . CopyTo ( ms ) ;
497+ var sourceContent = Encoding . UTF8 . GetString ( ms . ToArray ( ) ) ;
498+
499+ if ( ext is "ugc" or "sus" )
500+ {
501+ try
502+ {
503+ var ( chart , parseAlerts ) = ext == "ugc"
504+ ? new ChuUgcParser ( ) . Parse ( sourceContent )
505+ : new SusParser ( ) . Parse ( sourceContent ) ;
506+ alerts . AddRange ( parseAlerts . Select ( a => a . ToString ( ) ) ) ;
507+
508+ var ( c2sContent , genAlerts ) = new C2sGenerator ( ) . Generate ( chart ) ;
509+ alerts . AddRange ( genAlerts . Select ( a => a . ToString ( ) ) ) ;
510+
511+ System . IO . File . WriteAllText ( destPath , c2sContent , Encoding . UTF8 ) ;
512+ }
513+ catch ( MuConvert . utils . ConversionException ex )
514+ {
515+ alerts . AddRange ( ex . Alerts . Select ( a => a . ToString ( ) ) ) ;
516+ return BadRequest ( new { error = ex . Message , alerts } ) ;
517+ }
518+ }
519+ else
520+ {
521+ System . IO . File . WriteAllText ( destPath , sourceContent , Encoding . UTF8 ) ;
522+ }
523+
524+ var fumenNodes = music . XmlDoc . SelectNodes ( "/MusicData/fumens/MusicFumenData" ) ;
525+ if ( fumenNodes != null && diffIndex < fumenNodes . Count )
526+ {
527+ var node = fumenNodes [ diffIndex ] ! ;
528+ var enableNode = node . SelectSingleNode ( "enable" ) ;
529+ if ( enableNode != null ) enableNode . InnerText = "true" ;
530+ var fileNode = node . SelectSingleNode ( "file/path" ) ;
531+ if ( fileNode != null ) fileNode . InnerText = destFileName ;
532+ }
533+ music . Save ( ) ;
534+
535+ return Ok ( new { imported = true , convertedFrom = ext != "c2s" ? ext : ( string ? ) null , alerts } ) ;
536+ }
537+
409538 [ HttpGet ]
410539 public ActionResult ExportChart ( [ FromQuery ] int id , [ FromQuery ] string assetDir , [ FromQuery ] int diffIndex , [ FromQuery ] string format = "ugc" )
411540 {
@@ -940,7 +1069,7 @@ public ActionResult ExportOpt([FromQuery] int id, [FromQuery] string assetDir)
9401069 }
9411070
9421071 [ HttpGet ]
943- public ActionResult ExportUgc ( [ FromQuery ] int id , [ FromQuery ] string assetDir )
1072+ public ActionResult ExportCustom ( [ FromQuery ] int id , [ FromQuery ] string assetDir , [ FromQuery ] string format = "ugc" )
9441073 {
9451074 var scanner = scannerService . Scanner ;
9461075 if ( scanner == null ) return NotFound ( ) ;
@@ -962,10 +1091,13 @@ public ActionResult ExportUgc([FromQuery] int id, [FromQuery] string assetDir)
9621091 try
9631092 {
9641093 var ( chart , _) = new C2sParser ( ) . Parse ( c2sContent ) ;
965- var ( ugcContent , _) = new UgcGenerator ( ) . Generate ( chart ) ;
966- var entry = zip . CreateEntry ( $ "{ safeName } .ugc") ;
1094+ var ext = format . ToLowerInvariant ( ) == "sus" ? "sus" : "ugc" ;
1095+ var content = ext == "sus"
1096+ ? new SusGenerator ( ) . Generate ( chart ) . Item1
1097+ : new UgcGenerator ( ) . Generate ( chart ) . Item1 ;
1098+ var entry = zip . CreateEntry ( $ "{ safeName } .{ ext } ") ;
9671099 using var w = new StreamWriter ( entry . Open ( ) , Encoding . UTF8 ) ;
968- w . Write ( ugcContent ) ;
1100+ w . Write ( content ) ;
9691101 }
9701102 catch
9711103 {
@@ -1003,6 +1135,82 @@ public ActionResult ExportUgc([FromQuery] int id, [FromQuery] string assetDir)
10031135 return File ( ms , "application/zip" , $ "{ safeName } .zip") ;
10041136 }
10051137
1138+ [ HttpPost ]
1139+ public ActionResult ChangeId ( [ FromQuery ] int id , [ FromQuery ] string assetDir , [ FromBody ] int newId )
1140+ {
1141+ var scanner = scannerService . Scanner ;
1142+ if ( scanner == null ) return NotFound ( ) ;
1143+
1144+ var music = FindMusic ( scanner , id , assetDir ) ;
1145+ if ( music == null ) return NotFound ( "曲目不存在" ) ;
1146+
1147+ if ( assetDir == "A000" ) return BadRequest ( "不能修改 A000 的曲目 ID" ) ;
1148+
1149+ var optRoot = ResolveOptRoot ( assetDir ) ;
1150+ if ( optRoot == null ) return BadRequest ( "目录无效" ) ;
1151+
1152+ var newMusicDirName = $ "music{ newId : D4} ";
1153+ var newMusicDir = Path . Combine ( optRoot , "music" , newMusicDirName ) ;
1154+ if ( Directory . Exists ( newMusicDir ) && newId != id )
1155+ return BadRequest ( $ "ID { newId } 的曲目目录已存在") ;
1156+
1157+ var oldMusicDir = music . MusicDirectory ;
1158+
1159+ // Music.xml: 更新 ID
1160+ var root = music . XmlDoc . SelectSingleNode ( "/MusicData" ) ;
1161+ var idNode = root ? . SelectSingleNode ( "name/id" ) ;
1162+ if ( idNode != null ) idNode . InnerText = newId . ToString ( ) ;
1163+ var dataNameNode = root ? . SelectSingleNode ( "dataName" ) ;
1164+ if ( dataNameNode != null ) dataNameNode . InnerText = newMusicDirName ;
1165+ music . Save ( ) ;
1166+
1167+ // 重命名 music 目录
1168+ if ( newId != id && oldMusicDir != newMusicDir )
1169+ Directory . Move ( oldMusicDir , newMusicDir ) ;
1170+
1171+ // 重命名 cueFile 目录
1172+ var oldCueDir = Path . Combine ( optRoot , "cueFile" , $ "cueFile{ id : D6} ") ;
1173+ var newCueDir = Path . Combine ( optRoot , "cueFile" , $ "cueFile{ newId : D6} ") ;
1174+ if ( newId != id && Directory . Exists ( oldCueDir ) && ! Directory . Exists ( newCueDir ) )
1175+ Directory . Move ( oldCueDir , newCueDir ) ;
1176+
1177+ // 重新扫描
1178+ var newScanner = new MusicScanner ( StaticSettings . GamePath ) ;
1179+ newScanner . ScanAll ( ) ;
1180+ StaticSettings . Scanner = newScanner ;
1181+
1182+ return Ok ( ) ;
1183+ }
1184+
1185+ [ HttpPost ]
1186+ public ActionResult DeleteMusic ( [ FromQuery ] int id , [ FromQuery ] string assetDir )
1187+ {
1188+ var scanner = scannerService . Scanner ;
1189+ if ( scanner == null ) return NotFound ( ) ;
1190+
1191+ var music = FindMusic ( scanner , id , assetDir ) ;
1192+ if ( music == null ) return NotFound ( "曲目不存在" ) ;
1193+
1194+ if ( assetDir == "A000" ) return BadRequest ( "不能删除 A000 的曲目" ) ;
1195+
1196+ if ( Directory . Exists ( music . MusicDirectory ) )
1197+ Directory . Delete ( music . MusicDirectory , true ) ;
1198+
1199+ var optRoot = ResolveOptRoot ( assetDir ) ;
1200+ if ( optRoot != null )
1201+ {
1202+ var cueDir = Path . Combine ( optRoot , "cueFile" , $ "cueFile{ id : D6} ") ;
1203+ if ( Directory . Exists ( cueDir ) )
1204+ Directory . Delete ( cueDir , true ) ;
1205+ }
1206+
1207+ var newScanner = new MusicScanner ( StaticSettings . GamePath ) ;
1208+ newScanner . ScanAll ( ) ;
1209+ StaticSettings . Scanner = newScanner ;
1210+
1211+ return Ok ( ) ;
1212+ }
1213+
10061214 [ HttpPost ]
10071215 public ActionResult OpenExplorer ( [ FromQuery ] int id , [ FromQuery ] string assetDir )
10081216 {
@@ -1057,6 +1265,7 @@ public class FumenSummary
10571265 public int LevelDecimal { get ; set ; }
10581266 public string LevelDisplay { get ; set ; } = "" ;
10591267 public string NotesDesigner { get ; set ; } = "" ;
1268+ public int NoteCount { get ; set ; }
10601269}
10611270
10621271public class MusicEditDto
0 commit comments