MVP completed: Hook into Playlistmanager to post updates to beatleader maps owned by the user
This commit is contained in:
parent
f9a0e1669f
commit
7879e03cc5
@ -62,6 +62,37 @@ namespace Setlist
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Reads sync URL, BeatLeader guid, and owner from playlist customData (same rules as startup scan).</summary>
|
||||
internal static void TryReadBeatLeaderMetadata(
|
||||
IPlaylist playlist,
|
||||
out bool hasSyncUrl,
|
||||
out string beatLeaderGuid,
|
||||
out string ownerId)
|
||||
{
|
||||
hasSyncUrl = false;
|
||||
beatLeaderGuid = null;
|
||||
ownerId = null;
|
||||
|
||||
if (!playlist.TryGetCustomData("syncURL", out var syncObj) || !(syncObj is string syncUrl)
|
||||
|| string.IsNullOrWhiteSpace(syncUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
hasSyncUrl = true;
|
||||
|
||||
if (playlist.TryGetCustomData("owner", out var ownerObj) && ownerObj is string o
|
||||
&& !string.IsNullOrWhiteSpace(o))
|
||||
{
|
||||
ownerId = o.Trim();
|
||||
}
|
||||
|
||||
if (TryExtractBeatLeaderPlaylistGuid(syncUrl, out var g))
|
||||
{
|
||||
beatLeaderGuid = g;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a hidden coroutine runner that resolves the platform user id, then logs per-playlist
|
||||
/// ownership from playlist JSON. Must be called from the Unity main thread (BSIPA <c>OnApplicationStart</c> is fine).
|
||||
@ -77,6 +108,108 @@ namespace Setlist
|
||||
runner.Initialize(entries, log);
|
||||
}
|
||||
|
||||
internal static IEnumerator FetchPlatformUserIdCoroutine(Action<string> onDone)
|
||||
{
|
||||
var waited = 0f;
|
||||
var lastGetUserInfoAttempt = -PlatformUserGetUserInfoRetrySeconds;
|
||||
while (waited < PlatformUserWaitTimeoutSeconds)
|
||||
{
|
||||
foreach (var plm in EnumerateLeaderboardModels())
|
||||
{
|
||||
var pid = plm.playerId;
|
||||
if (!string.IsNullOrEmpty(pid))
|
||||
{
|
||||
onDone(pid.Trim());
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
if (waited - lastGetUserInfoAttempt >= PlatformUserGetUserInfoRetrySeconds)
|
||||
{
|
||||
lastGetUserInfoAttempt = waited;
|
||||
var model = ResolvePlatformUserModel();
|
||||
if (model != null)
|
||||
{
|
||||
Task<UserInfo> task;
|
||||
try
|
||||
{
|
||||
task = model.GetUserInfo(CancellationToken.None);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
task = null;
|
||||
}
|
||||
|
||||
if (task != null)
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (!task.IsFaulted)
|
||||
{
|
||||
var uid = task.Result?.platformUserId;
|
||||
if (!string.IsNullOrEmpty(uid))
|
||||
{
|
||||
onDone(uid.Trim());
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waited += PlatformUserPollStepSeconds;
|
||||
yield return new WaitForSeconds(PlatformUserPollStepSeconds);
|
||||
}
|
||||
|
||||
onDone(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Resources.FindObjectsOfTypeAll{T}"/> can miss very early; <see cref="Object.FindObjectOfType{T}()"/> sometimes finds the scene instance sooner.
|
||||
/// </summary>
|
||||
private static IEnumerable<PlatformLeaderboardsModel> EnumerateLeaderboardModels()
|
||||
{
|
||||
var seen = new HashSet<PlatformLeaderboardsModel>();
|
||||
foreach (var plm in Resources.FindObjectsOfTypeAll<PlatformLeaderboardsModel>())
|
||||
{
|
||||
if (plm != null && seen.Add(plm))
|
||||
{
|
||||
yield return plm;
|
||||
}
|
||||
}
|
||||
|
||||
var active = UnityEngine.Object.FindObjectOfType<PlatformLeaderboardsModel>();
|
||||
if (active != null && seen.Add(active))
|
||||
{
|
||||
yield return active;
|
||||
}
|
||||
}
|
||||
|
||||
private static IPlatformUserModel ResolvePlatformUserModel()
|
||||
{
|
||||
var field = typeof(PlatformLeaderboardsModel).GetField(
|
||||
"_platformUserModel",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (field == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IPlatformUserModel last = null;
|
||||
foreach (var plm in EnumerateLeaderboardModels())
|
||||
{
|
||||
if (field.GetValue(plm) is IPlatformUserModel m)
|
||||
{
|
||||
last = m;
|
||||
}
|
||||
}
|
||||
|
||||
return last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Component that drives the logging coroutine; self-destroys when finished.
|
||||
/// </summary>
|
||||
@ -97,7 +230,8 @@ namespace Setlist
|
||||
private IEnumerator Run()
|
||||
{
|
||||
string platformUserId = null;
|
||||
yield return StartCoroutine(FetchPlatformUserId(id => { platformUserId = id; }));
|
||||
yield return StartCoroutine(FetchPlatformUserIdCoroutine(id => { platformUserId = id; }));
|
||||
Plugin.CachedPlatformUserId = platformUserId;
|
||||
_log.Info(string.IsNullOrEmpty(platformUserId)
|
||||
? "platformUserId=(unknown)"
|
||||
: $"platformUserId={platformUserId}");
|
||||
@ -112,131 +246,8 @@ namespace Setlist
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BSIPA runs us very early; <see cref="PlatformLeaderboardsModel"/> may not exist yet and its
|
||||
/// async init may not have set <see cref="PlatformLeaderboardsModel.playerId"/>. Poll until ready.
|
||||
/// </summary>
|
||||
private static IEnumerator FetchPlatformUserId(Action<string> onDone)
|
||||
{
|
||||
var waited = 0f;
|
||||
var lastGetUserInfoAttempt = -PlatformUserGetUserInfoRetrySeconds;
|
||||
while (waited < PlatformUserWaitTimeoutSeconds)
|
||||
{
|
||||
foreach (var plm in EnumerateLeaderboardModels())
|
||||
{
|
||||
var pid = plm.playerId;
|
||||
if (!string.IsNullOrEmpty(pid))
|
||||
{
|
||||
onDone(pid.Trim());
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
if (waited - lastGetUserInfoAttempt >= PlatformUserGetUserInfoRetrySeconds)
|
||||
{
|
||||
lastGetUserInfoAttempt = waited;
|
||||
var model = ResolvePlatformUserModel();
|
||||
if (model != null)
|
||||
{
|
||||
Task<UserInfo> task;
|
||||
try
|
||||
{
|
||||
task = model.GetUserInfo(CancellationToken.None);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
task = null;
|
||||
}
|
||||
|
||||
if (task != null)
|
||||
{
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (!task.IsFaulted)
|
||||
{
|
||||
var uid = task.Result?.platformUserId;
|
||||
if (!string.IsNullOrEmpty(uid))
|
||||
{
|
||||
onDone(uid.Trim());
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waited += PlatformUserPollStepSeconds;
|
||||
yield return new WaitForSeconds(PlatformUserPollStepSeconds);
|
||||
}
|
||||
|
||||
onDone(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Resources.FindObjectsOfTypeAll{T}"/> can miss very early; <see cref="Object.FindObjectOfType{T}()"/> sometimes finds the scene instance sooner.
|
||||
/// </summary>
|
||||
private static IEnumerable<PlatformLeaderboardsModel> EnumerateLeaderboardModels()
|
||||
{
|
||||
var seen = new HashSet<PlatformLeaderboardsModel>();
|
||||
foreach (var plm in Resources.FindObjectsOfTypeAll<PlatformLeaderboardsModel>())
|
||||
{
|
||||
if (plm != null && seen.Add(plm))
|
||||
{
|
||||
yield return plm;
|
||||
}
|
||||
}
|
||||
|
||||
var active = UnityEngine.Object.FindObjectOfType<PlatformLeaderboardsModel>();
|
||||
if (active != null && seen.Add(active))
|
||||
{
|
||||
yield return active;
|
||||
}
|
||||
}
|
||||
|
||||
private static IPlatformUserModel ResolvePlatformUserModel()
|
||||
{
|
||||
var field = typeof(PlatformLeaderboardsModel).GetField(
|
||||
"_platformUserModel",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (field == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IPlatformUserModel last = null;
|
||||
foreach (var plm in EnumerateLeaderboardModels())
|
||||
{
|
||||
if (field.GetValue(plm) is IPlatformUserModel m)
|
||||
{
|
||||
last = m;
|
||||
}
|
||||
}
|
||||
|
||||
return last;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,44 +13,42 @@ 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.
|
||||
/// POSTs an updated playlist to BeatLeader when the local copy is owned by the player (playlist JSON
|
||||
/// <c>owner</c> vs platform user id). Uses Unity cookie session after BeatLeader sign-in.
|
||||
/// </summary>
|
||||
internal static class BeatLeaderPlaylistUpdateTest
|
||||
internal static class BeatLeaderPlaylistSync
|
||||
{
|
||||
/// <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)
|
||||
internal static IEnumerator CoPostOwnedPlaylistToBeatLeader(IPlaylist playlist, IPALogger log)
|
||||
{
|
||||
if (!string.Equals(playlist.Title, TargetPlaylistTitle, StringComparison.Ordinal))
|
||||
BeatLeaderPlaylistOwnership.TryReadBeatLeaderMetadata(playlist, out var hasSyncUrl, out var beatLeaderGuid, out var ownerId);
|
||||
|
||||
var platformUserId = Plugin.CachedPlatformUserId;
|
||||
if (string.IsNullOrEmpty(platformUserId))
|
||||
{
|
||||
yield break;
|
||||
string resolved = null;
|
||||
yield return BeatLeaderPlaylistOwnership.FetchPlatformUserIdCoroutine(id => { resolved = id; });
|
||||
platformUserId = resolved;
|
||||
if (!string.IsNullOrEmpty(platformUserId))
|
||||
{
|
||||
Plugin.CachedPlatformUserId = platformUserId;
|
||||
}
|
||||
}
|
||||
|
||||
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).");
|
||||
$"Setlist BeatLeader sync: skip \"{playlist.Title}\" (customData id missing; cannot POST).");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@ -66,7 +64,7 @@ namespace Setlist
|
||||
if (waited >= SignInWaitTimeoutSeconds)
|
||||
{
|
||||
log.Info(
|
||||
$"Setlist BeatLeader POST test: BeatLeader login did not complete within {SignInWaitTimeoutSeconds:F0}s.");
|
||||
$"Setlist BeatLeader sync: login did not complete within {SignInWaitTimeoutSeconds:F0}s (playlist \"{playlist.Title}\").");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@ -96,22 +94,20 @@ namespace Setlist
|
||||
|
||||
if (request.result == UnityWebRequest.Result.ConnectionError)
|
||||
{
|
||||
log.Info("Setlist BeatLeader POST test: network error: " + request.error);
|
||||
log.Info("Setlist BeatLeader sync: network error: " + request.error);
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (request.result == UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
log.Info(
|
||||
$"Setlist BeatLeader POST test: HTTP {request.responseCode} body="
|
||||
$"Setlist BeatLeader sync: HTTP {request.responseCode} playlist=\"{playlist.Title}\" body="
|
||||
+ TruncateForLog(request.downloadHandler?.text));
|
||||
yield break;
|
||||
}
|
||||
|
||||
log.Info(
|
||||
"Setlist BeatLeader POST test: success HTTP "
|
||||
+ request.responseCode
|
||||
+ " body="
|
||||
$"Setlist BeatLeader sync: OK HTTP {request.responseCode} playlist=\"{playlist.Title}\" body="
|
||||
+ TruncateForLog(request.downloadHandler?.text));
|
||||
}
|
||||
}
|
||||
@ -163,7 +159,7 @@ namespace Setlist
|
||||
private static bool TrySerializePlaylist(IPlaylist playlist, IPALogger log, out string json)
|
||||
{
|
||||
json = null;
|
||||
var mgr = PlaylistManager.DefaultManager;
|
||||
var mgr = BeatSaberPlaylistsLib.PlaylistManager.DefaultManager;
|
||||
IPlaylistHandler handler = null;
|
||||
if (!string.IsNullOrEmpty(playlist.SuggestedExtension))
|
||||
{
|
||||
@ -177,7 +173,7 @@ namespace Setlist
|
||||
|
||||
if (handler == null)
|
||||
{
|
||||
log.Info("Setlist BeatLeader POST test: no IPlaylistHandler for playlist type.");
|
||||
log.Info($"Setlist BeatLeader sync: no IPlaylistHandler for \"{playlist.Title}\".");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -193,7 +189,7 @@ namespace Setlist
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Info("Setlist BeatLeader POST test: serialize failed: " + ex.Message);
|
||||
log.Info($"Setlist BeatLeader sync: serialize failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -204,7 +200,7 @@ namespace Setlist
|
||||
var authType = beatLeaderAssembly.GetType(AuthenticationTypeName);
|
||||
if (authType == null)
|
||||
{
|
||||
log.Info($"Setlist BeatLeader POST test: assembly has no {AuthenticationTypeName}.");
|
||||
log.Info($"Setlist BeatLeader sync: assembly has no {AuthenticationTypeName}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -227,7 +223,7 @@ namespace Setlist
|
||||
|
||||
private static string GetAssemblyVersion()
|
||||
{
|
||||
var asm = typeof(BeatLeaderPlaylistUpdateTest).Assembly;
|
||||
var asm = typeof(BeatLeaderPlaylistSync).Assembly;
|
||||
return asm.GetName().Version?.ToString() ?? "0.0.0";
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using BeatSaberPlaylistsLib.Types;
|
||||
using IPA;
|
||||
using PlaylistManager.Utilities;
|
||||
using IPALogger = IPA.Logging.Logger;
|
||||
|
||||
namespace Setlist
|
||||
@ -16,6 +17,9 @@ namespace Setlist
|
||||
/// </summary>
|
||||
internal static IPALogger Log { get; private set; }
|
||||
|
||||
/// <summary>Set after the ownership scan resolves the platform user id; used when syncing after PlaylistManager adds a song.</summary>
|
||||
internal static string CachedPlatformUserId { get; set; }
|
||||
|
||||
[Init]
|
||||
public Plugin(IPALogger logger)
|
||||
{
|
||||
@ -28,6 +32,10 @@ namespace Setlist
|
||||
{
|
||||
try
|
||||
{
|
||||
SetlistSyncHost.Ensure();
|
||||
|
||||
Events.playlistSongAdded += OnPlaylistSongAdded;
|
||||
|
||||
var playlists = BeatSaberPlaylistsLib.PlaylistManager.DefaultManager.GetAllPlaylists(
|
||||
includeChildren: true,
|
||||
out AggregateException loadErrors);
|
||||
@ -50,25 +58,11 @@ namespace Setlist
|
||||
var entries = new List<(IPlaylist Playlist, string Title, bool HasSyncUrl, string BeatLeaderGuid, string OwnerId)>();
|
||||
foreach (var playlist in playlists)
|
||||
{
|
||||
var hasSyncUrl = false;
|
||||
string syncUrl = null;
|
||||
if (playlist.TryGetCustomData("syncURL", out var syncObj) && syncObj is string url)
|
||||
{
|
||||
syncUrl = url;
|
||||
hasSyncUrl = !string.IsNullOrWhiteSpace(url);
|
||||
}
|
||||
|
||||
string ownerId = null;
|
||||
if (playlist.TryGetCustomData("owner", out var ownerObj) && ownerObj is string o)
|
||||
{
|
||||
ownerId = string.IsNullOrWhiteSpace(o) ? null : o.Trim();
|
||||
}
|
||||
|
||||
string blGuid = null;
|
||||
if (hasSyncUrl && BeatLeaderPlaylistOwnership.TryExtractBeatLeaderPlaylistGuid(syncUrl, out var g))
|
||||
{
|
||||
blGuid = g;
|
||||
}
|
||||
BeatLeaderPlaylistOwnership.TryReadBeatLeaderMetadata(
|
||||
playlist,
|
||||
out var hasSyncUrl,
|
||||
out var blGuid,
|
||||
out var ownerId);
|
||||
|
||||
entries.Add((playlist, playlist.Title, hasSyncUrl, blGuid, ownerId));
|
||||
}
|
||||
@ -84,6 +78,12 @@ namespace Setlist
|
||||
[OnExit]
|
||||
public void OnApplicationQuit()
|
||||
{
|
||||
Events.playlistSongAdded -= OnPlaylistSongAdded;
|
||||
}
|
||||
|
||||
private void OnPlaylistSongAdded(IPlaylistSong song, IPlaylist playlist)
|
||||
{
|
||||
SetlistSyncHost.Instance?.StartPostOwnedBeatLeaderPlaylist(playlist, Log);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -11,5 +11,5 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: Guid("50F53E6E-21D5-4780-8E67-273877DAA28C")]
|
||||
[assembly: AssemblyVersion("0.0.8.0")]
|
||||
[assembly: AssemblyFileVersion("0.0.8.0")]
|
||||
[assembly: AssemblyVersion("0.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("0.1.0.0")]
|
||||
|
||||
@ -115,11 +115,16 @@
|
||||
<HintPath>$(BeatSaberDir)\Plugins\BeatLeader.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="PlaylistManager">
|
||||
<HintPath>$(BeatSaberDir)\Plugins\PlaylistManager.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="BeatLeaderPlaylistOwnership.cs" />
|
||||
<Compile Include="BeatLeaderPlaylistUpdateTest.cs" />
|
||||
<Compile Include="BeatLeaderPlaylistSync.cs" />
|
||||
<Compile Include="Plugin.cs" />
|
||||
<Compile Include="SetlistSyncHost.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
32
Setlist/SetlistSyncHost.cs
Normal file
32
Setlist/SetlistSyncHost.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using BeatSaberPlaylistsLib.Types;
|
||||
using UnityEngine;
|
||||
using IPALogger = IPA.Logging.Logger;
|
||||
|
||||
namespace Setlist
|
||||
{
|
||||
/// <summary>
|
||||
/// Persists for <see cref="UnityEngine.MonoBehaviour.StartCoroutine"/> (e.g. BeatLeader POST after UI adds a song).
|
||||
/// </summary>
|
||||
internal sealed class SetlistSyncHost : MonoBehaviour
|
||||
{
|
||||
internal static SetlistSyncHost Instance { get; private set; }
|
||||
|
||||
internal static void Ensure()
|
||||
{
|
||||
if (Instance != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var go = new GameObject("Setlist.SyncHost");
|
||||
UnityEngine.Object.DontDestroyOnLoad(go);
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
Instance = go.AddComponent<SetlistSyncHost>();
|
||||
}
|
||||
|
||||
internal void StartPostOwnedBeatLeaderPlaylist(IPlaylist playlist, IPALogger log)
|
||||
{
|
||||
StartCoroutine(BeatLeaderPlaylistSync.CoPostOwnedPlaylistToBeatLeader(playlist, log));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,13 @@
|
||||
"id": "Setlist",
|
||||
"name": "Setlist",
|
||||
"author": "",
|
||||
"version": "0.0.8",
|
||||
"version": "0.1.0",
|
||||
"description": "Syncs playlists with external sources.",
|
||||
"gameVersion": "1.40.8",
|
||||
"dependsOn": {
|
||||
"BSIPA": "^4.3.0",
|
||||
"BeatSaberPlaylistsLib": "^1.7.0",
|
||||
"BeatLeader": "^0.9.0"
|
||||
"BeatLeader": "^0.9.0",
|
||||
"PlaylistManager": "^1.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user