Debug update a playlist

This commit is contained in:
pleb 2026-04-18 21:52:18 -07:00
parent 2ad7b50f65
commit 7301c10aa9
6 changed files with 272 additions and 8 deletions

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BeatSaberPlaylistsLib.Types;
using UnityEngine; using UnityEngine;
#if false #if false
using BeatLeader.Utils; using BeatLeader.Utils;
@ -71,7 +72,7 @@ namespace Setlist
/// ownership from playlist JSON. Must be called from the Unity main thread (BSIPA <c>OnApplicationStart</c> is fine). /// ownership from playlist JSON. Must be called from the Unity main thread (BSIPA <c>OnApplicationStart</c> is fine).
/// </summary> /// </summary>
internal static void ScheduleVerifyAndLog( 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) IPALogger log)
{ {
var go = new GameObject("Setlist.OwnershipRunner"); var go = new GameObject("Setlist.OwnershipRunner");
@ -86,11 +87,11 @@ namespace Setlist
/// </summary> /// </summary>
private sealed class OwnershipRunner : MonoBehaviour 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; private IPALogger _log;
public void Initialize( 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) IPALogger log)
{ {
_entries = entries; _entries = entries;
@ -116,6 +117,23 @@ namespace Setlist
platformUserId)); 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); Destroy(gameObject);
} }

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
internal static class BeatLeaderPlaylistUpdateTest
{
/// <summary>When this playlist title is locally owned (owner matches platform id), run POST test on startup.</summary>
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;
/// <summary>POST <c>/user/playlist</c> with the serialized playlist body (same shape as the website sample).</summary>
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) + "…";
}
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using BeatSaberPlaylistsLib.Types;
using IPA; using IPA;
using IPALogger = IPA.Logging.Logger; using IPALogger = IPA.Logging.Logger;
@ -46,7 +47,7 @@ namespace Setlist
return; 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) foreach (var playlist in playlists)
{ {
var hasSyncUrl = false; var hasSyncUrl = false;
@ -69,7 +70,7 @@ namespace Setlist
blGuid = g; blGuid = g;
} }
entries.Add((playlist.Title, hasSyncUrl, blGuid, ownerId)); entries.Add((playlist, playlist.Title, hasSyncUrl, blGuid, ownerId));
} }
BeatLeaderPlaylistOwnership.ScheduleVerifyAndLog(entries, Log); BeatLeaderPlaylistOwnership.ScheduleVerifyAndLog(entries, Log);

View File

@ -11,5 +11,5 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("50F53E6E-21D5-4780-8E67-273877DAA28C")] [assembly: Guid("50F53E6E-21D5-4780-8E67-273877DAA28C")]
[assembly: AssemblyVersion("0.0.6.0")] [assembly: AssemblyVersion("0.0.7.0")]
[assembly: AssemblyFileVersion("0.0.6.0")] [assembly: AssemblyFileVersion("0.0.7.0")]

View File

@ -118,6 +118,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="BeatLeaderPlaylistOwnership.cs" /> <Compile Include="BeatLeaderPlaylistOwnership.cs" />
<Compile Include="BeatLeaderPlaylistUpdateTest.cs" />
<Compile Include="Plugin.cs" /> <Compile Include="Plugin.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>

View File

@ -3,7 +3,7 @@
"id": "Setlist", "id": "Setlist",
"name": "Setlist", "name": "Setlist",
"author": "", "author": "",
"version": "0.0.6", "version": "0.0.7",
"description": "Syncs playlists with external sources.", "description": "Syncs playlists with external sources.",
"gameVersion": "1.40.8", "gameVersion": "1.40.8",
"dependsOn": { "dependsOn": {