setlist/docs/beatleader-playlist-api.md

113 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `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):
```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<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`)
```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}");
}
```
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`).