using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.UI; public class SongCreatorManager : MonoBehaviour { [Header("Audio Source")] [SerializeField] private TMP_Dropdown audioDropdown; [SerializeField] private Button refreshBtn; [SerializeField] private TMP_Text inputPathHint; [Header("Audio — Local File")] [SerializeField] private Button filePickerBtn; [SerializeField] private TMP_Text addStatusText; [Header("Audio — URL")] [SerializeField] private TMP_InputField urlInput; [SerializeField] private Button urlDownloadBtn; [Header("Metadata")] [SerializeField] private TMP_InputField titleInput; [SerializeField] private TMP_InputField artistInput; [SerializeField] private TMP_InputField bpmInput; [Header("Difficulty")] [SerializeField] private Toggle toggleNormal; [SerializeField] private Toggle toggleHard; [SerializeField] private Toggle toggleExpert; [SerializeField] private Toggle toggleExpertPlus; [Header("Actions")] [SerializeField] private Button generateButton; [SerializeField] private Button manualEditorButton; [SerializeField] private Button backButton; [SerializeField] private string menuSceneName = "Menu"; [Header("Progress")] [SerializeField] private GameObject progressGroup; [SerializeField] private TMP_Text statusText; [SerializeField] private Slider progressSlider; [Header("References")] [SerializeField] private BeatSageUploader beatSageUploader; [SerializeField] private NasPublisher nasPublisher; private static readonly Color NeonBg = new Color(0.05f, 0.82f, 0.95f, 0.42f); private static readonly Color DarkButtonBg = new Color(0.09f, 0.22f, 0.27f, 0.66f); private static readonly Color DisabledBg = new Color(0.06f, 0.12f, 0.15f, 0.48f); private static readonly Color ButtonText = new Color(0.92f, 1.0f, 1.0f, 1.0f); private static readonly Color MutedText = new Color(0.66f, 0.80f, 0.84f, 0.76f); private static readonly Color NeonOutline = new Color(0.25f, 0.96f, 1.0f, 0.42f); private static string InputPath => Path.Combine(Application.persistentDataPath, "input"); private readonly List audioFiles = new(); private string _pendingFilePath; private void OnValidate() { ApplyButtonStyles(); } private void Start() { ApplyButtonStyles(); Directory.CreateDirectory(InputPath); if (inputPathHint != null) inputPathHint.text = $"Path: {InputPath}"; refreshBtn?.onClick.AddListener(RefreshAudioList); generateButton?.onClick.AddListener(OnGenerateClicked); backButton?.onClick.AddListener(() => SceneManager.LoadScene(menuSceneName)); filePickerBtn?.onClick.AddListener(OnFilePickerClicked); urlDownloadBtn?.onClick.AddListener(OnUrlDownloadClicked); if (progressGroup != null) progressGroup.SetActive(false); RefreshAudioList(); } private void Update() { if (_pendingFilePath != null) { CopyToInput(_pendingFilePath); _pendingFilePath = null; } } private void RefreshAudioList() { audioFiles.Clear(); audioDropdown?.ClearOptions(); var options = new List(); foreach (string f in Directory.GetFiles(InputPath, "*.mp3")) { audioFiles.Add(f); options.Add(Path.GetFileNameWithoutExtension(f)); } if (options.Count == 0) options.Add("-- no .mp3 files --"); audioDropdown?.AddOptions(options); } private void OnGenerateClicked() { string directUrl = urlInput != null ? urlInput.text.Trim() : ""; bool hasUrl = !string.IsNullOrEmpty(directUrl); bool hasFile = audioFiles.Count > 0; if (!hasUrl && !hasFile) { SetStatus("No audio source. Add a file or enter a URL."); return; } // BPM input is optional — Beat Sage auto-detects from audio; use as fallback only float.TryParse(bpmInput?.text, out float bpmHint); var diffs = new List { "normal", "hard", "expert", "expertplus" }; if (hasUrl) { if (!Uri.TryCreate(directUrl, UriKind.Absolute, out var uri) || (uri.Scheme != "http" && uri.Scheme != "https")) { SetStatus("Invalid URL."); return; } StartCoroutine(GenerateFlowFromUrl(uri.AbsoluteUri, bpmHint, diffs)); } else { StartCoroutine(GenerateFlow(audioFiles[audioDropdown.value], bpmHint, diffs)); } } private IEnumerator GenerateFlowFromUrl(string audioUrl, float bpm, List diffs) { SetInteractable(false); if (progressGroup != null) progressGroup.SetActive(true); Dictionary> maps = null; bool failed = false; yield return beatSageUploader.UploadFromUrl( audioUrl, diffs, bpm, onProgress: p => { if (progressSlider != null) progressSlider.value = p * 0.8f; SetStatus($"{beatSageUploader.CurrentStatus} ({(int)(p * 80)}%)"); }, onComplete: result => maps = result, onError: err => { SetStatus($"Error: {err}"); failed = true; }); if (failed) { SetInteractable(true); yield break; } SongInfo song = BuildSongInfo(audioUrl, bpm, maps); yield return nasPublisher.Publish( song, null, maps, onProgress: p => { if (progressSlider != null) progressSlider.value = 0.8f + p * 0.2f; SetStatus($"[4/4] Uploading to NAS... ({(int)((0.8f + p * 0.2f) * 100)}%)"); }, onComplete: () => { if (progressSlider != null) progressSlider.value = 1f; SetStatus($"Done! '{song.title}' created successfully."); }, onError: err => { SetStatus($"NAS upload failed: {err}"); failed = true; }); SetInteractable(true); } private IEnumerator GenerateFlow(string audioPath, float bpm, List diffs) { SetInteractable(false); if (progressGroup != null) progressGroup.SetActive(true); Dictionary> maps = null; bool failed = false; yield return beatSageUploader.Upload( audioPath, diffs, bpm, onProgress: p => { if (progressSlider != null) progressSlider.value = p * 0.8f; SetStatus($"{beatSageUploader.CurrentStatus} ({(int)(p * 80)}%)"); }, onComplete: result => maps = result, onError: err => { SetStatus($"Error: {err}"); failed = true; }); if (failed) { SetInteractable(true); yield break; } SongInfo song = BuildSongInfo(audioPath, bpm, maps); yield return nasPublisher.Publish( song, audioPath, maps, onProgress: p => { if (progressSlider != null) progressSlider.value = 0.8f + p * 0.2f; SetStatus($"[4/4] Uploading to NAS... ({(int)((0.8f + p * 0.2f) * 100)}%)"); }, onComplete: () => { if (progressSlider != null) progressSlider.value = 1f; SetStatus($"Done! '{song.title}' created successfully."); }, onError: err => { SetStatus($"NAS upload failed: {err}"); failed = true; }); SetInteractable(true); } private SongInfo BuildSongInfo(string audioPath, float fallbackBpm, Dictionary> maps) { // Prefer values from info.dat (auto-detected by Beat Sage); UI inputs override if non-empty var meta = beatSageUploader != null ? beatSageUploader.LastMetadata : null; string uiTitle = titleInput?.text.Trim() ?? ""; string uiArtist = artistInput?.text.Trim() ?? ""; float.TryParse(bpmInput?.text, out float uiBpm); string title = !string.IsNullOrEmpty(uiTitle) ? uiTitle : (meta?.title ?? ""); string artist = !string.IsNullOrEmpty(uiArtist) ? uiArtist : (meta?.artist ?? ""); float bpm = (meta != null && meta.bpm > 0) ? meta.bpm : (uiBpm > 0 ? uiBpm : fallbackBpm); // Fallback id from filename if title is still empty if (string.IsNullOrEmpty(title) && !string.IsNullOrEmpty(audioPath)) title = Path.GetFileNameWithoutExtension(audioPath); if (string.IsNullOrEmpty(title)) title = $"song_{DateTime.Now:yyyyMMdd_HHmmss}"; string id = title.ToLower().Replace(" ", "_"); var diffMap = new DifficultyMap(); foreach (var kv in maps) { var info = new DifficultyInfo { noteCount = kv.Value.Count }; switch (kv.Key) { case "normal": diffMap.normal = info; break; case "hard": diffMap.hard = info; break; case "expert": diffMap.expert = info; break; case "expertplus": diffMap.expertplus = info; break; } } return new SongInfo { id = id, title = title, artist = artist, bpm = bpm, audioFile = $"music/{id}.mp3", difficulties = diffMap, addedAt = DateTime.Now.ToString("yyyy-MM-dd"), }; } private void SetStatus(string msg) { if (statusText != null) statusText.text = msg; } private void SetInteractable(bool value) { if (generateButton != null) generateButton.interactable = value; if (audioDropdown != null) audioDropdown.interactable = value; if (refreshBtn != null) refreshBtn.interactable = value; if (filePickerBtn != null) filePickerBtn.interactable = value; if (urlDownloadBtn != null) urlDownloadBtn.interactable = value; ApplyButtonStyles(); } private void OnFilePickerClicked() { #if UNITY_EDITOR string path = UnityEditor.EditorUtility.OpenFilePanel("Select audio file", "", "mp3"); if (!string.IsNullOrEmpty(path)) CopyToInput(path); #elif UNITY_STANDALONE_WIN var t = new Thread(() => { var dlg = new System.Windows.Forms.OpenFileDialog { Filter = "MP3|*.mp3" }; if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) _pendingFilePath = dlg.FileName; }); t.SetApartmentState(ApartmentState.STA); t.Start(); #else SetAddStatus($"Copy file via ADB:\n{InputPath}"); #endif } private void CopyToInput(string srcPath) { try { string dest = Path.Combine(InputPath, Path.GetFileName(srcPath)); File.Copy(srcPath, dest, overwrite: true); RefreshAudioList(); string nameNoExt = Path.GetFileNameWithoutExtension(srcPath); int idx = audioFiles.FindIndex(f => Path.GetFileNameWithoutExtension(f) == nameNoExt); if (idx >= 0 && audioDropdown != null) audioDropdown.value = idx; SetAddStatus($"Added: {Path.GetFileName(srcPath)}"); } catch (Exception e) { SetAddStatus($"File copy failed: {e.Message}"); } } private void OnUrlDownloadClicked() { string url = urlInput != null ? urlInput.text.Trim() : ""; if (string.IsNullOrEmpty(url)) { SetAddStatus("Please enter a URL."); return; } if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme != "http" && uri.Scheme != "https")) { SetAddStatus($"Invalid URL: must start with http:// or https://"); return; } // Streaming service URLs cannot be downloaded directly string host = uri.Host.ToLower(); if (host.Contains("youtube.com") || host.Contains("youtu.be") || host.Contains("spotify.com") || host.Contains("soundcloud.com") || host.Contains("music.apple.com")) { SetAddStatus("Streaming URLs not supported.\nUse a direct .mp3 download link or Browse File."); return; } StartCoroutine(DownloadFromUrl(uri.AbsoluteUri)); } private IEnumerator DownloadFromUrl(string url) { SetAddStatus("Downloading..."); if (urlDownloadBtn != null) urlDownloadBtn.interactable = false; ApplyButtonStyles(); string fileName; try { string uriPath = new Uri(url).AbsolutePath; fileName = Path.GetFileName(uriPath); if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase)) fileName = "download.mp3"; } catch { fileName = "download.mp3"; } string savePath = Path.GetFullPath(Path.Combine(InputPath, fileName)); using var req = UnityWebRequest.Get(url); req.downloadHandler = new DownloadHandlerFile(savePath); yield return req.SendWebRequest(); if (urlDownloadBtn != null) urlDownloadBtn.interactable = true; ApplyButtonStyles(); if (req.result == UnityWebRequest.Result.Success) { RefreshAudioList(); int idx = audioFiles.FindIndex( f => Path.GetFileNameWithoutExtension(f) == Path.GetFileNameWithoutExtension(fileName)); if (idx >= 0 && audioDropdown != null) audioDropdown.value = idx; SetAddStatus($"Downloaded: {fileName}"); } else { if (File.Exists(savePath)) File.Delete(savePath); SetAddStatus($"Download failed: {req.error}"); } } private void SetAddStatus(string msg) { if (addStatusText != null) addStatusText.text = msg; } private void ApplyButtonStyles() { ApplyCreatorButtonStyle(generateButton, true); ApplyCreatorButtonStyle(urlDownloadBtn, true); ApplyCreatorButtonStyle(refreshBtn, false); ApplyCreatorButtonStyle(filePickerBtn, false); ApplyCreatorButtonStyle(backButton, false); } private static void ApplyCreatorButtonStyle(Button btn, bool primary) { if (btn == null) return; Color bg = btn.interactable ? (primary ? NeonBg : DarkButtonBg) : DisabledBg; if (btn.targetGraphic is Image img) { img.color = bg; img.raycastTarget = true; } var colors = btn.colors; colors.normalColor = bg; colors.highlightedColor = btn.interactable ? new Color(0.10f, 0.95f, 1.0f, primary ? 0.58f : 0.48f) : DisabledBg; colors.pressedColor = btn.interactable ? new Color(0.02f, 0.58f, 0.72f, 0.80f) : DisabledBg; colors.selectedColor = colors.highlightedColor; colors.disabledColor = DisabledBg; colors.fadeDuration = 0.08f; btn.colors = colors; TMP_Text label = btn.GetComponentInChildren(true); if (label != null) { label.color = btn.interactable ? ButtonText : MutedText; label.raycastTarget = false; } Outline outline = btn.GetComponent() ?? btn.gameObject.AddComponent(); outline.enabled = btn.interactable; outline.effectColor = NeonOutline; outline.effectDistance = new Vector2(0.0f, -0.28f); } }