setlist/docs/beatleader-playlist-api.md

8.5 KiB
Raw Blame History

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 WebRequestFactorys 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 HttpRequestMessage for GET https://api.beatleader.com/user/playlists
  • Call WebRequestFactory.Send(...) or WebRequestFactory.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 BeatLeaders 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

Heres 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}");
}

Thats all thats 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/playlists returns 401 with no authenticated user in context. See PlaylistController.GetAllPlaylists in beatleader-server (CurrentUserID then Unauthorized()).
  • beatleader-mod source uses WebRequestFactory (a static HttpClient over a CookieContainer). 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 with ilspycmd -l class:
    • BeatLeader.WebRequests.WebRequestFactory does 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) from JsonGetRequestDescriptor.CreateWebRequest).
    • Sign-in is BeatLeader.API.Authentication: a coroutine EnsureLoggedIn(Action onSuccess, Action<string> onFail) that posts BLConstants.SIGNIN_WITH_TICKET via UnityWebRequest.Post. ResetLogin() clears with UnityWebRequest.ClearCookieCache(...) — confirming the session cookie lives in Unity's cookie cache, not in any HttpClient/CookieContainer.
  • Cookie sharing on this build does work via UnityWebRequest. Unity's UnityWebRequest cookie cache is process-wide per host, so a second plugin issuing UnityWebRequest.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.Authentication is internal and its successful-login state is the private static field _signedIn. Reflect into typeof(BLConstants).Assembly.GetType("BeatLeader.API.Authentication").GetField("_signedIn", BindingFlags.NonPublic | BindingFlags.Static) and poll until true, then issue the UnityWebRequest. (Triggering EnsureLoggedIn ourselves is brittle: it depends on Resources.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):
    1. Spawn a hidden MonoBehaviour via new GameObject(...).AddComponent<>() from OnApplicationStart (BSIPA's [OnStart] runs on the Unity main thread).
    2. 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).
    3. UnityWebRequest.Get(BLConstants.BEATLEADER_API_URL + "/user/playlists"), set a User-Agent, yield return SendWebRequest().
    4. Check request.result == UnityWebRequest.Result.Success, then JsonConvert.DeserializeObject<List<UserPlaylistSummary>>(request.downloadHandler.text).
    5. Build a HashSet<string> of guid values; per-playlist ownership is just set.Contains(extractedGuid).
  • JSON shape matches the model in §1: camelCase, only guid is required for ownership; id, ownerId, isShared, link, hash, deleted are 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.dll to inspect the shipped types (BeatLeader.API.{Authentication,NetworkingUtils} + RequestHandlers.PersistentSingletonRequestHandler).