# 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`** 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(..., 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): ```csharp // 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`) ```csharp 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, JsonResponseParser>> { private static string Endpoint => BLConstants.BEATLEADER_API_URL + "/user/playlists"; public static IWebRequest> Send() { return SendRet(Endpoint, HttpMethod.Get); } } } ``` ### 3. Call site (same as `PlaylistRequest` / `PlatformEventsRequest`) ```csharp 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/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` + `BeatLeader.API.RequestHandlers.PersistentSingletonRequestHandler` + `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 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()` 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>(request.downloadHandler.text)`. 5. Build a `HashSet` 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`).