using System; using System.Collections; using System.IO; using UnityEngine; using UnityEngine.Networking; public class DownloadManager : MonoBehaviour { [SerializeField] private string baseUrl = "http://whdwo798.synology.me/beatsaber"; private static string CacheRoot => Path.Combine(Application.persistentDataPath, "beatsaber"); private static string LegacyCacheRoot => Path.Combine(Application.temporaryCachePath, "beatsaber"); // ── Public API ─────────────────────────────────────────── public void FetchSongsList(Action onSuccess, Action onError = null) { StartCoroutine(GetText($"{baseUrl}/songs.json", json => { SongsList list = JsonUtility.FromJson(json); if (list == null) onError?.Invoke("songs.json 파싱 실패"); else onSuccess?.Invoke(list); }, onError)); } public void DownloadSong(SongInfo song, string difficulty, Action onProgress, Action onComplete, Action onError = null) { StartCoroutine(DownloadSongCoroutine(song, difficulty, onProgress, onComplete, onError)); } public void DeleteSong(string songId) { string dir = SongDir(songId); if (Directory.Exists(dir)) { Directory.Delete(dir, recursive: true); Debug.Log($"[DownloadManager] 삭제: {songId}"); } string legacyDir = LegacySongDir(songId); if (Directory.Exists(legacyDir)) Directory.Delete(legacyDir, recursive: true); } public void DeleteDifficulty(SongInfo song, string difficulty) { TryMigrateLegacySong(song.id); string path = MapPath(song, difficulty); if (path != null && File.Exists(path)) File.Delete(path); string songDir = SongDir(song.id); if (Directory.Exists(songDir) && Directory.GetFileSystemEntries(songDir).Length == 0) Directory.Delete(songDir); } public bool IsSongDownloaded(string songId) { TryMigrateLegacySong(songId); return File.Exists(AudioPath(songId)); } public bool IsDifficultyDownloaded(SongInfo song, string difficulty) { TryMigrateLegacySong(song.id); string path = MapPath(song, difficulty); return path != null && File.Exists(path); } public string AudioPath(string songId) => Path.Combine(SongDir(songId), $"{songId}.mp3"); public string MapPath(SongInfo song, string difficulty) { DifficultyInfo info = song.difficulties.Get(difficulty); if (info == null || string.IsNullOrEmpty(info.mapFile)) return null; string fileName = Path.GetFileName(info.mapFile); if (string.IsNullOrEmpty(fileName)) return null; return Path.Combine(SongDir(song.id), fileName); } // ── 내부 구현 ───────────────────────────────────────────── private IEnumerator DownloadSongCoroutine(SongInfo song, string difficulty, Action onProgress, Action onComplete, Action onError) { TryMigrateLegacySong(song.id); string songDir = Path.GetFullPath(SongDir(song.id)); Directory.CreateDirectory(songDir); // 1단계: 오디오 (70%) string audioPath = Path.Combine(songDir, $"{song.id}.mp3"); if (!File.Exists(audioPath)) { bool failed = false; yield return DownloadFile( $"{baseUrl}/{song.audioFile}", audioPath, p => onProgress?.Invoke(p * 0.7f), err => { onError?.Invoke(err); failed = true; }); if (failed) yield break; } // 2단계: 맵 파일 (30%) DifficultyInfo diffInfo = song.difficulties.Get(difficulty); if (diffInfo == null) { onError?.Invoke($"난이도 '{difficulty}' 없음"); yield break; } if (string.IsNullOrEmpty(diffInfo.mapFile)) { onError?.Invoke($"'{difficulty}' 맵 파일 정보 없음 — Creator에서 곡을 다시 생성해주세요"); yield break; } string mapPath = MapPath(song, difficulty); if (mapPath != null) mapPath = Path.GetFullPath(mapPath); if (mapPath == null) { onError?.Invoke($"'{difficulty}' 맵 경로 계산 실패"); yield break; } if (!File.Exists(mapPath)) { bool failed = false; yield return DownloadFile( $"{baseUrl}/{diffInfo.mapFile}", mapPath, p => onProgress?.Invoke(0.7f + p * 0.3f), err => { onError?.Invoke(err); failed = true; }); if (failed) yield break; } onProgress?.Invoke(1f); onComplete?.Invoke(); Debug.Log($"[DownloadManager] 완료: {song.title} ({difficulty})"); } private IEnumerator DownloadFile(string url, string savePath, Action onProgress, Action onError) { using var req = UnityWebRequest.Get(url); req.downloadHandler = new DownloadHandlerFile(savePath); req.SendWebRequest(); while (!req.isDone) { onProgress?.Invoke(req.downloadProgress); yield return null; } if (req.result != UnityWebRequest.Result.Success) { if (File.Exists(savePath)) File.Delete(savePath); onError?.Invoke($"다운로드 실패: {url} — {req.error}"); } } private IEnumerator GetText(string url, Action onSuccess, Action onError) { using var req = UnityWebRequest.Get(url); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) onError?.Invoke($"요청 실패: {url} — {req.error}"); else onSuccess?.Invoke(req.downloadHandler.text); } private static string SongDir(string songId) => Path.Combine(CacheRoot, songId); private static string LegacySongDir(string songId) => Path.Combine(LegacyCacheRoot, songId); private static void TryMigrateLegacySong(string songId) { string sourceDir = LegacySongDir(songId); string targetDir = SongDir(songId); if (Directory.Exists(targetDir) || !Directory.Exists(sourceDir)) return; CopyDirectory(sourceDir, targetDir); Directory.Delete(sourceDir, recursive: true); Debug.Log($"[DownloadManager] 기존 캐시를 영구 저장소로 이동: {songId}"); } private static void CopyDirectory(string sourceDir, string targetDir) { Directory.CreateDirectory(targetDir); foreach (string file in Directory.GetFiles(sourceDir)) { string targetFile = Path.Combine(targetDir, Path.GetFileName(file)); File.Copy(file, targetFile, overwrite: true); } foreach (string dir in Directory.GetDirectories(sourceDir)) { string targetSubDir = Path.Combine(targetDir, Path.GetFileName(dir)); CopyDirectory(dir, targetSubDir); } } }