using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; using UnityEngine.Networking; public class NasPublisher : MonoBehaviour { [Header("NAS Connection")] [SerializeField] private string nasBaseUrl = "http://192.168.55.3:5000"; [SerializeField] private string nasAccount = "admin"; [SerializeField] private string nasRootPath = "/web/beatsaber"; [Header("Static Server URL (for reading songs.json)")] [SerializeField] private string staticBaseUrl = "http://whdwo798.synology.me/beatsaber"; private string _sid = ""; private string _synoToken = ""; private string _password = ""; private void Awake() => LoadConfig(); private void LoadConfig() { string path = Path.Combine(Application.streamingAssetsPath, "nas_config.json"); if (!File.Exists(path)) { Debug.LogWarning("[NasPublisher] nas_config.json not found: " + path); return; } var cfg = JsonUtility.FromJson(File.ReadAllText(path)); if (cfg == null) return; _password = cfg.password ?? ""; if (!string.IsNullOrEmpty(cfg.host)) nasBaseUrl = cfg.host; if (!string.IsNullOrEmpty(cfg.account)) nasAccount = cfg.account; if (!string.IsNullOrEmpty(cfg.rootPath)) nasRootPath = cfg.rootPath; if (!string.IsNullOrEmpty(cfg.staticUrl)) staticBaseUrl = cfg.staticUrl; } [Serializable] private class NasConfig { public string host = ""; public string account = ""; public string rootPath = ""; public string staticUrl = ""; public string password = ""; } public IEnumerator Publish( SongInfo song, string audioPath, Dictionary> maps, Action onProgress, Action onComplete, Action onError) { bool failed = false; void OnErr(string e) { onError?.Invoke(e); failed = true; } yield return Login(OnErr); if (string.IsNullOrEmpty(_sid)) yield break; onProgress?.Invoke(0.1f); if (!string.IsNullOrEmpty(audioPath)) { yield return UploadFile(audioPath, $"{nasRootPath}/music", $"{song.id}.mp3", OnErr); if (failed) { yield return Logout(); yield break; } } onProgress?.Invoke(0.4f); int total = maps.Count, done = 0; foreach (var kv in maps) { string fileName = $"Map_{song.id}_{kv.Key}.json"; byte[] bytes = Encoding.UTF8.GetBytes(BeatSageConverter.ToMapJson(kv.Value)); AssignMapFile(song, kv.Key, fileName); yield return UploadBytes(bytes, fileName, $"{nasRootPath}/maps", OnErr); if (failed) { yield return Logout(); yield break; } done++; onProgress?.Invoke(0.4f + (float)done / total * 0.3f); } yield return PatchSongsJson(song, OnErr); if (failed) { yield return Logout(); yield break; } onProgress?.Invoke(0.95f); yield return Logout(); onProgress?.Invoke(1f); onComplete?.Invoke(); Debug.Log($"[NasPublisher] Upload complete: '{song.title}'"); } private IEnumerator Login(Action onError) { string url = $"{nasBaseUrl}/webapi/auth.cgi" + $"?api=SYNO.API.Auth&version=6&method=login" + $"&account={UnityWebRequest.EscapeURL(nasAccount)}" + $"&passwd={UnityWebRequest.EscapeURL(_password)}" + $"&session=FileStation&format=sid&enable_syno_token=yes"; using var req = UnityWebRequest.Get(url); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) { onError?.Invoke($"DSM login failed: {req.error}"); yield break; } string resp = req.downloadHandler.text; _sid = ParseJsonString(resp, "sid"); _synoToken = ParseJsonString(resp, "synotoken"); if (string.IsNullOrEmpty(_sid)) onError?.Invoke("DSM sid parse failed — check credentials."); } private IEnumerator Logout() { string url = $"{nasBaseUrl}/webapi/auth.cgi" + $"?api=SYNO.API.Auth&version=1&method=logout&session=FileStation&_sid={_sid}"; using var req = UnityWebRequest.Get(url); yield return req.SendWebRequest(); _sid = ""; } private IEnumerator UploadFile(string localPath, string nasFolder, string fileName, Action onError) { yield return UploadBytes(File.ReadAllBytes(localPath), fileName, nasFolder, onError); } private IEnumerator UploadBytes(byte[] bytes, string fileName, string nasFolder, Action onError) { string uploadUrl = $"{nasBaseUrl}/webapi/entry.cgi" + $"?api=SYNO.FileStation.Upload&version=2&method=upload" + $"&_sid={UnityWebRequest.EscapeURL(_sid)}"; string boundary = Guid.NewGuid().ToString("N"); const string CRLF = "\r\n"; using var body = new MemoryStream(); void WriteText(string s) { var b = Encoding.UTF8.GetBytes(s); body.Write(b, 0, b.Length); } void WriteField(string name, string value) { WriteText($"--{boundary}{CRLF}"); WriteText($"Content-Disposition: form-data; name=\"{name}\"{CRLF}{CRLF}"); WriteText(value + CRLF); } WriteField("path", nasFolder); WriteField("create_parents", "true"); WriteField("overwrite", "true"); WriteText($"--{boundary}{CRLF}"); WriteText($"Content-Disposition: form-data; name=\"file\"; filename=\"{fileName}\"{CRLF}"); WriteText($"Content-Type: application/octet-stream{CRLF}{CRLF}"); body.Write(bytes, 0, bytes.Length); WriteText(CRLF + $"--{boundary}--{CRLF}"); using var req = new UnityWebRequest(uploadUrl, "POST"); req.uploadHandler = new UploadHandlerRaw(body.ToArray()); req.downloadHandler = new DownloadHandlerBuffer(); req.SetRequestHeader("Content-Type", $"multipart/form-data; boundary={boundary}"); if (!string.IsNullOrEmpty(_synoToken)) req.SetRequestHeader("X-SYNO-TOKEN", _synoToken); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) { onError?.Invoke($"Upload failed ({fileName}): {req.error}"); yield break; } if (req.downloadHandler.text.Contains("\"success\":false")) onError?.Invoke($"Upload rejected ({fileName}): {req.downloadHandler.text}"); } private IEnumerator PatchSongsJson(SongInfo newSong, Action onError) { SongsList list = null; using (var req = UnityWebRequest.Get($"{staticBaseUrl}/songs.json")) { yield return req.SendWebRequest(); if (req.result == UnityWebRequest.Result.Success) list = JsonUtility.FromJson(req.downloadHandler.text); } list ??= new SongsList { version = "1.0", songs = new List() }; int idx = list.songs.FindIndex(s => s.id == newSong.id); if (idx >= 0) list.songs[idx] = newSong; else list.songs.Add(newSong); yield return UploadBytes( Encoding.UTF8.GetBytes(JsonUtility.ToJson(list, true)), "songs.json", nasRootPath, onError); } private static string ParseJsonString(string json, string key) { string search = $"\"{key}\":\""; int start = json.IndexOf(search, StringComparison.Ordinal); if (start < 0) return null; start += search.Length; int end = json.IndexOf('"', start); return end > start ? json.Substring(start, end - start) : null; } private static void AssignMapFile(SongInfo song, string diff, string fileName) { var info = song.difficulties.Get(diff); if (info != null) info.mapFile = $"maps/{fileName}"; } }