8.5 KiB
BeatLeader Playlist API
Here is the practical picture for a second plugin in the same Beat Saber process.
What “authorization state” actually is
For /user/playlist*, the server cares about the ASP.NET cookie auth session: the browser-like cookies your client got after ticket sign-in, which build a principal with NameIdentifier + Issued. There is no separate API key or Bearer token your plugin can copy out of BeatLeader and paste on its own requests unless the server also accepts that (for these routes, you have been told it does not).
Reference BeatLeader and use its WebRequestFactory (real sharing)
In beatleader-mod, session cookies live on a single static HttpClient whose handler uses WebRequestFactory’s CookieContainer. Login is Authentication.Login() (internal), which fills that container; WebRequestFactory.Send / Send<T> are public and, with default waitForLogin: true, call Authentication.WaitLogin() before sending.
So another assembly can:
- Add a project reference (or reference the shipped BeatLeader.dll in
Plugins). - Build
HttpRequestMessageforGET https://api.beatleader.com/user/playlists - Call
WebRequestFactory.Send(...)orWebRequestFactory.Send<T>(..., parser, waitForLogin: true).
That runs on the same HttpClient and CookieContainer as BeatLeader, and waits on the same login gate, so you inherit the session without BeatLeader exposing a special “auth API.”
Requirements / caveats
- BeatLeader must be installed and actually complete login (its menu init calls
Authentication.Login()). If login never succeeds,WaitLogin()never completes happily and you get no cookies. - Hard dependency: no BeatLeader assembly → no compile; disabled/removed mod → your auth path breaks unless you add a fallback.
- API stability: you depend on BeatLeader’s public surface (
WebRequestFactory, parsers, etc.) staying compatible across versions.
You do not need to “obtain” cookies as strings; you only need to route requests through that factory (or duplicate sign-in below).
Playlists API
Here’s a concrete shape that matches how the beatleader mod already does authenticated GETs (UserRequest, ContextsRequest, PlatformEventsRequest): same HttpClient + CookieContainer, default waitForLogin: true, so GET /user/playlists runs after sign-in and sends session cookies automatically.
1. Response model
Align property names with your JSON (camelCase matches typical ASP.NET JSON defaults):
// e.g. Source/2_Core/Models/API/BeatLeader/UserPlaylistSummary.cs
namespace BeatLeader.Models {
public sealed class UserPlaylistSummary {
public int id { get; set; }
public bool isShared { get; set; }
public string? link { get; set; }
public string? ownerId { get; set; }
public string? hash { get; set; }
public string? guid { get; set; }
public bool deleted { get; set; }
}
}
2. Request type (mirror PlatformEventsRequest / PlaylistRequest)
using System.Collections.Generic;
using System.Net.Http;
using BeatLeader.Models;
using BeatLeader.Utils;
using BeatLeader.WebRequests;
namespace BeatLeader.API {
internal sealed class UserPlaylistsRequest
: PersistentWebRequestBase<List<UserPlaylistSummary>, JsonResponseParser<List<UserPlaylistSummary>>> {
private static string Endpoint => BLConstants.BEATLEADER_API_URL + "/user/playlists";
public static IWebRequest<List<UserPlaylistSummary>> Send() {
return SendRet(Endpoint, HttpMethod.Get);
}
}
}
3. Call site (same as PlaylistRequest / PlatformEventsRequest)
var result = await UserPlaylistsRequest.Send().Join();
if (result.RequestState == WebRequests.RequestState.Finished && result.Result != null) {
foreach (var p in result.Result) {
// use p.id, p.guid, p.deleted, etc.
}
} else {
Plugin.Log.Debug($"User playlists failed: {result.FailReason}");
}
That’s all that’s required on the client side for “fetch my playlists”: GET + existing cookie session. No extra headers beyond what WebRequestFactory already applies (User-Agent + cookies).
Troubleshooting: right after implementing, compare behavior to UserRequest (GET /user/modinterface): if one returns Finished with data and the other gets 401, the issue is session/host/HTTPS, not playlist-specific logic.
Field notes (Setlist / BS 1.40.8, 2026)
These observations are from running a second plugin (Setlist) alongside the shipped BeatLeader 0.9.x DLL on a BSManager 1.40.8 install. Treat them as the operational replacement for the WebRequestFactory recipe above when targeting that build.
- Server:
GET ~/user/playlistsreturns 401 with no authenticated user in context. SeePlaylistController.GetAllPlaylistsin beatleader-server (CurrentUserIDthenUnauthorized()). - beatleader-mod source uses
WebRequestFactory(a staticHttpClientover aCookieContainer). That is what the main sections of this file describe. - Shipped
BeatLeader.dll(0.9.x in 1.40.8) is older than that source and the layout is different. Verified withilspycmd -l class:BeatLeader.WebRequests.WebRequestFactorydoes not exist.- The networking stack is
BeatLeader.API.NetworkingUtils+BeatLeader.API.RequestDescriptors.JsonGetRequestDescriptor<T>+BeatLeader.API.RequestHandlers.PersistentSingletonRequestHandler<T,R>+BeatLeader.API.Methods.*(e.g.UserRequest,PlaylistRequest). - Every call funnels through
UnityWebRequest(UnityWebRequest.Get(url)fromJsonGetRequestDescriptor.CreateWebRequest). - Sign-in is
BeatLeader.API.Authentication: a coroutineEnsureLoggedIn(Action onSuccess, Action<string> onFail)that postsBLConstants.SIGNIN_WITH_TICKETviaUnityWebRequest.Post.ResetLogin()clears withUnityWebRequest.ClearCookieCache(...)— confirming the session cookie lives in Unity's cookie cache, not in anyHttpClient/CookieContainer.
- Cookie sharing on this build does work via
UnityWebRequest. Unity'sUnityWebRequestcookie cache is process-wide per host, so a second plugin issuingUnityWebRequest.Get("https://api.beatleader.com/user/playlists")after BeatLeader's sign-in inherits the ASP.NET cookie automatically. (The earlier "UnityWebRequest tends to 401" observation was from racing BeatLeader's login — not from a separate cookie store.) - Detecting "BeatLeader is signed in" without a public hook on this build:
BeatLeader.API.Authenticationisinternaland its successful-login state is the private static field_signedIn. Reflect intotypeof(BLConstants).Assembly.GetType("BeatLeader.API.Authentication").GetField("_signedIn", BindingFlags.NonPublic | BindingFlags.Static)and poll untiltrue, then issue theUnityWebRequest. (TriggeringEnsureLoggedInourselves is brittle: it depends onResources.FindObjectsOfTypeAll<PlatformLeaderboardsModel>()having returned by the time we call it — easier to wait for BeatLeader's own login coroutine to finish.) - Concrete recipe used by Setlist (
Setlist/BeatLeaderPlaylistOwnership.cs):- Spawn a hidden
MonoBehaviourvianew GameObject(...).AddComponent<>()fromOnApplicationStart(BSIPA's[OnStart]runs on the Unity main thread). - In a coroutine, poll
Authentication._signedIn(with a generous timeout, e.g. 90 s; sign-in only happens once the menu scene loads and platform tickets resolve). UnityWebRequest.Get(BLConstants.BEATLEADER_API_URL + "/user/playlists"), set aUser-Agent,yield return SendWebRequest().- Check
request.result == UnityWebRequest.Result.Success, thenJsonConvert.DeserializeObject<List<UserPlaylistSummary>>(request.downloadHandler.text). - Build a
HashSet<string>ofguidvalues; per-playlist ownership is justset.Contains(extractedGuid).
- Spawn a hidden
- JSON shape matches the model in §1: camelCase, only
guidis required for ownership;id,ownerId,isShared,link,hash,deletedare optional metadata. - Reference reading on disk:
~/src/beatleader/beatleader-mod/Source/2_Core/API/{WebRequestFactory,Authentication,PersistentWebRequest}.cs(newer source);ilspycmd -p -o /tmp/bl-dec /home/pleb/.local/share/BSManager/BSInstances/1.40.8/Plugins/BeatLeader.dllto inspect the shipped types (BeatLeader.API.{Authentication,NetworkingUtils}+RequestHandlers.PersistentSingletonRequestHandler).