From 7301c10aa92b75192665e48f75e4d461956d2719 Mon Sep 17 00:00:00 2001 From: pleb Date: Sat, 18 Apr 2026 21:52:18 -0700 Subject: [PATCH] Debug update a playlist --- Setlist/BeatLeaderPlaylistOwnership.cs | 24 ++- Setlist/BeatLeaderPlaylistUpdateTest.cs | 244 ++++++++++++++++++++++++ Setlist/Plugin.cs | 5 +- Setlist/Properties/AssemblyInfo.cs | 4 +- Setlist/Setlist.csproj | 1 + Setlist/manifest.json | 2 +- 6 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 Setlist/BeatLeaderPlaylistUpdateTest.cs diff --git a/Setlist/BeatLeaderPlaylistOwnership.cs b/Setlist/BeatLeaderPlaylistOwnership.cs index ca64b43..f0c014a 100644 --- a/Setlist/BeatLeaderPlaylistOwnership.cs +++ b/Setlist/BeatLeaderPlaylistOwnership.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using BeatSaberPlaylistsLib.Types; using UnityEngine; #if false using BeatLeader.Utils; @@ -71,7 +72,7 @@ namespace Setlist /// ownership from playlist JSON. Must be called from the Unity main thread (BSIPA OnApplicationStart is fine). /// internal static void ScheduleVerifyAndLog( - List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries, + List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries, IPALogger log) { var go = new GameObject("Setlist.OwnershipRunner"); @@ -86,11 +87,11 @@ namespace Setlist /// private sealed class OwnershipRunner : MonoBehaviour { - private List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> _entries; + private List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> _entries; private IPALogger _log; public void Initialize( - List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries, + List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)> entries, IPALogger log) { _entries = entries; @@ -116,6 +117,23 @@ namespace Setlist platformUserId)); } + foreach (var e in _entries) + { + if (!string.Equals(e.Title, BeatLeaderPlaylistUpdateTest.TargetPlaylistTitle, StringComparison.Ordinal)) + { + continue; + } + + yield return BeatLeaderPlaylistUpdateTest.RunIfTargetOwned( + e.Playlist, + e.HasSyncUrl, + e.BeatLeaderGuid, + e.OwnerId, + platformUserId, + _log); + break; + } + Destroy(gameObject); } diff --git a/Setlist/BeatLeaderPlaylistUpdateTest.cs b/Setlist/BeatLeaderPlaylistUpdateTest.cs new file mode 100644 index 0000000..707395f --- /dev/null +++ b/Setlist/BeatLeaderPlaylistUpdateTest.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections; +using System.IO; +using System.Reflection; +using System.Text; +using BeatLeader.Utils; +using BeatSaberPlaylistsLib; +using BeatSaberPlaylistsLib.Types; +using UnityEngine; +using UnityEngine.Networking; +using IPALogger = IPA.Logging.Logger; + +namespace Setlist +{ + /// + /// Dev/test path: POST the on-disk playlist JSON to BeatLeader to prove authenticated updates work. + /// Requires BeatLeader to have finished sign-in (Unity cookie cache). See docs/beatleader-playlist-api.md. + /// + internal static class BeatLeaderPlaylistUpdateTest + { + /// When this playlist title is locally owned (owner matches platform id), run POST test on startup. + internal const string TargetPlaylistTitle = "2025-mapfilter-01"; + + private const string AuthenticationTypeName = "BeatLeader.API.Authentication"; + private const string SignedInFieldName = "_signedIn"; + private const float SignInWaitTimeoutSeconds = 90f; + private const int PostRequestTimeoutSeconds = 120; + + /// POST /user/playlist with the serialized playlist body (same shape as the website sample). + internal static IEnumerator RunIfTargetOwned( + IPlaylist playlist, + bool hasSyncUrl, + string beatLeaderGuid, + string ownerId, + string platformUserId, + IPALogger log) + { + if (!string.Equals(playlist.Title, TargetPlaylistTitle, StringComparison.Ordinal)) + { + yield break; + } + + if (!IsLocallyOwnedBeatLeaderPlaylist(hasSyncUrl, beatLeaderGuid, ownerId, platformUserId)) + { + log.Info( + $"Setlist BeatLeader POST test: skip \"{TargetPlaylistTitle}\" (not locally owned or missing BeatLeader fields)."); + yield break; + } + + if (!TryGetBeatLeaderServerFields(playlist, out var serverPlaylistId, out var shared)) + { + log.Info( + $"Setlist BeatLeader POST test: skip \"{TargetPlaylistTitle}\" (customData missing id or not parseable)."); + yield break; + } + + var signedInField = TryResolveBeatLeaderSignedInField(log); + if (signedInField == null) + { + yield break; + } + + var waited = 0f; + while (!IsSignedIn(signedInField)) + { + if (waited >= SignInWaitTimeoutSeconds) + { + log.Info( + $"Setlist BeatLeader POST test: BeatLeader login did not complete within {SignInWaitTimeoutSeconds:F0}s."); + yield break; + } + + yield return new WaitForSeconds(1f); + waited += 1f; + } + + if (!TrySerializePlaylist(playlist, log, out var bodyJson)) + { + yield break; + } + + var url = + $"{BLConstants.BEATLEADER_API_URL}/user/playlist?id={Uri.EscapeDataString(serverPlaylistId)}&shared=" + + (shared ? "true" : "false"); + + byte[] bodyRaw = Encoding.UTF8.GetBytes(bodyJson); + using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST)) + { + request.timeout = PostRequestTimeoutSeconds; + request.uploadHandler = new UploadHandlerRaw(bodyRaw); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Content-Type", "text/plain;charset=UTF-8"); + request.SetRequestHeader("User-Agent", "Setlist/" + GetAssemblyVersion()); + + yield return request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.ConnectionError) + { + log.Info("Setlist BeatLeader POST test: network error: " + request.error); + yield break; + } + + if (request.result == UnityWebRequest.Result.ProtocolError) + { + log.Info( + $"Setlist BeatLeader POST test: HTTP {request.responseCode} body=" + + TruncateForLog(request.downloadHandler?.text)); + yield break; + } + + log.Info( + "Setlist BeatLeader POST test: success HTTP " + + request.responseCode + + " body=" + + TruncateForLog(request.downloadHandler?.text)); + } + } + + private static bool IsLocallyOwnedBeatLeaderPlaylist( + bool hasSyncUrl, + string beatLeaderGuid, + string ownerId, + string platformUserId) + { + return hasSyncUrl + && !string.IsNullOrEmpty(beatLeaderGuid) + && !string.IsNullOrEmpty(ownerId) + && !string.IsNullOrEmpty(platformUserId) + && string.Equals(ownerId, platformUserId, StringComparison.Ordinal); + } + + private static bool TryGetBeatLeaderServerFields(IPlaylist playlist, out string serverPlaylistId, out bool shared) + { + serverPlaylistId = null; + shared = false; + + if (!playlist.TryGetCustomData("id", out var idObj) || idObj == null) + { + return false; + } + + serverPlaylistId = Convert.ToString(idObj); + if (string.IsNullOrWhiteSpace(serverPlaylistId)) + { + return false; + } + + if (playlist.TryGetCustomData("shared", out var sharedObj) && sharedObj != null) + { + if (sharedObj is bool b) + { + shared = b; + } + else if (bool.TryParse(Convert.ToString(sharedObj), out var parsed)) + { + shared = parsed; + } + } + + return true; + } + + private static bool TrySerializePlaylist(IPlaylist playlist, IPALogger log, out string json) + { + json = null; + var mgr = PlaylistManager.DefaultManager; + IPlaylistHandler handler = null; + if (!string.IsNullOrEmpty(playlist.SuggestedExtension)) + { + handler = mgr.GetHandlerForExtension(playlist.SuggestedExtension); + } + + if (handler == null) + { + handler = mgr.GetHandlerForPlaylistType(playlist.GetType()); + } + + if (handler == null) + { + log.Info("Setlist BeatLeader POST test: no IPlaylistHandler for playlist type."); + return false; + } + + try + { + using (var stream = new MemoryStream()) + { + handler.Serialize(playlist, stream); + json = Encoding.UTF8.GetString(stream.ToArray()); + } + + return !string.IsNullOrEmpty(json); + } + catch (Exception ex) + { + log.Info("Setlist BeatLeader POST test: serialize failed: " + ex.Message); + return false; + } + } + + private static FieldInfo TryResolveBeatLeaderSignedInField(IPALogger log) + { + var beatLeaderAssembly = typeof(BLConstants).Assembly; + var authType = beatLeaderAssembly.GetType(AuthenticationTypeName); + if (authType == null) + { + log.Info($"Setlist BeatLeader POST test: assembly has no {AuthenticationTypeName}."); + return null; + } + + var field = authType.GetField(SignedInFieldName, BindingFlags.NonPublic | BindingFlags.Static); + if (field == null) + { + log.Info( + $"{AuthenticationTypeName} has no static field '{SignedInFieldName}' (BeatLeader API changed?)."); + return null; + } + + return field; + } + + private static bool IsSignedIn(FieldInfo signedInField) + { + var value = signedInField.GetValue(null); + return value is bool b && b; + } + + private static string GetAssemblyVersion() + { + var asm = typeof(BeatLeaderPlaylistUpdateTest).Assembly; + return asm.GetName().Version?.ToString() ?? "0.0.0"; + } + + private static string TruncateForLog(string s, int maxLen = 512) + { + if (string.IsNullOrEmpty(s)) + { + return ""; + } + + return s.Length <= maxLen ? s : s.Substring(0, maxLen) + "…"; + } + } +} diff --git a/Setlist/Plugin.cs b/Setlist/Plugin.cs index e8be6f1..d845bc0 100644 --- a/Setlist/Plugin.cs +++ b/Setlist/Plugin.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using BeatSaberPlaylistsLib.Types; using IPA; using IPALogger = IPA.Logging.Logger; @@ -46,7 +47,7 @@ namespace Setlist return; } - var entries = new List<(string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)>(); + var entries = new List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)>(); foreach (var playlist in playlists) { var hasSyncUrl = false; @@ -69,7 +70,7 @@ namespace Setlist blGuid = g; } - entries.Add((playlist.Title, hasSyncUrl, blGuid, ownerId)); + entries.Add((playlist, playlist.Title, hasSyncUrl, blGuid, ownerId)); } BeatLeaderPlaylistOwnership.ScheduleVerifyAndLog(entries, Log); diff --git a/Setlist/Properties/AssemblyInfo.cs b/Setlist/Properties/AssemblyInfo.cs index de422bc..f18d0d0 100644 --- a/Setlist/Properties/AssemblyInfo.cs +++ b/Setlist/Properties/AssemblyInfo.cs @@ -11,5 +11,5 @@ using System.Runtime.InteropServices; [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] [assembly: Guid("50F53E6E-21D5-4780-8E67-273877DAA28C")] -[assembly: AssemblyVersion("0.0.6.0")] -[assembly: AssemblyFileVersion("0.0.6.0")] +[assembly: AssemblyVersion("0.0.7.0")] +[assembly: AssemblyFileVersion("0.0.7.0")] diff --git a/Setlist/Setlist.csproj b/Setlist/Setlist.csproj index 5628bb7..9dadadf 100644 --- a/Setlist/Setlist.csproj +++ b/Setlist/Setlist.csproj @@ -118,6 +118,7 @@ + diff --git a/Setlist/manifest.json b/Setlist/manifest.json index 28faac5..9afa859 100644 --- a/Setlist/manifest.json +++ b/Setlist/manifest.json @@ -3,7 +3,7 @@ "id": "Setlist", "name": "Setlist", "author": "", - "version": "0.0.6", + "version": "0.0.7", "description": "Syncs playlists with external sources.", "gameVersion": "1.40.8", "dependsOn": {