Skip to content

Commit

Permalink
Merge pull request #98 from BeatLeader/replay-browser-downloader
Browse files Browse the repository at this point in the history
Replay Browser beatmaps downloader
  • Loading branch information
Reezonate authored Jul 19, 2023
2 parents 06487b7 + 93fefae commit a5dce0d
Show file tree
Hide file tree
Showing 18 changed files with 707 additions and 16 deletions.
3 changes: 3 additions & 0 deletions BeatLeader.sln.DotSettings.user
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">INFO</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AA_BB" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Users\Hermanest\Desktop\BSLegacyLauncher\Installed Versions\Beat Saber 1.29.1\Beat Saber_Data\Managed\Main.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Beatmaps/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BEATSAVER/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BSML/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=cdifficulty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=creplay/@EntryIndexedValue">True</s:Boolean>
Expand Down
11 changes: 11 additions & 0 deletions Source/2_Core/Models/BeatSaver/MapDetail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using JetBrains.Annotations;

namespace BeatLeader.Models.BeatSaver {
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
internal class MapDetail {
public string? id;
public MapDetailMetadata? metadata;
public UserDetail? uploader;
public MapVersion[]? versions;
}
}
9 changes: 9 additions & 0 deletions Source/2_Core/Models/BeatSaver/MapDetailMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using JetBrains.Annotations;

namespace BeatLeader.Models.BeatSaver {
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class MapDetailMetadata {
public string? songName;
public string? levelAuthorName;
}
}
10 changes: 10 additions & 0 deletions Source/2_Core/Models/BeatSaver/MapVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JetBrains.Annotations;

namespace BeatLeader.Models.BeatSaver {
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
internal class MapVersion {
public string? hash;
public string? coverURL;
public string? downloadURL;
}
}
8 changes: 8 additions & 0 deletions Source/2_Core/Models/BeatSaver/UserDetail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using JetBrains.Annotations;

namespace BeatLeader.Models.BeatSaver {
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
internal class UserDetail {
public string? name;
}
}
2 changes: 2 additions & 0 deletions Source/2_Core/Replayer/ReplayerLauncher.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using BeatLeader.Models;
using System;
using BeatLeader.Utils;
using JetBrains.Annotations;
using UnityEngine;
using Zenject;

namespace BeatLeader.Replayer {
[PublicAPI]
public class ReplayerLauncher : MonoBehaviour {
[Inject] private readonly GameScenesManager _gameScenesManager = null!;
[Inject] private readonly PlayerDataModel _playerDataModel = null!;
Expand Down
2 changes: 1 addition & 1 deletion Source/2_Core/Replayer/ReplayerMenuLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public bool LoadEnvironment(ReplayLaunchData launchData, string environmentName)
return true;
}

private async Task<IDifficultyBeatmap?> LoadBeatmapAsync(
public async Task<IDifficultyBeatmap?> LoadBeatmapAsync(
string hash,
string mode,
string difficulty,
Expand Down
10 changes: 10 additions & 0 deletions Source/7_Utils/BeatSaverConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace BeatLeader.BeatSaverAPI {
internal static class BeatSaverConstants {
public const string BEATSAVER_API_URL = "https://api.beatsaver.com";
public const string BEATSAVER_WEBSITE_URL = "https://beatsaver.com";
public const string BEATSAVER_CDN_URL = "https://cdn.beatsaver.com/";

public const string MAPS_HASH_ENDPOINT = "/maps/hash/";
public const string MAPS_ENDPOINT = "/maps/";
}
}
25 changes: 25 additions & 0 deletions Source/7_Utils/BeatSaverUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Threading.Tasks;
using BeatLeader.Models.BeatSaver;
using Newtonsoft.Json;
using static BeatLeader.BeatSaverAPI.BeatSaverConstants;

namespace BeatLeader.Utils {
internal static class BeatSaverUtils {
public static async Task<MapDetail?> GetMapByHashAsync(string hash) {
return await WebUtils.SendAsync(BEATSAVER_API_URL + MAPS_HASH_ENDPOINT + hash) is { IsSuccessStatusCode: true } res ?
JsonConvert.DeserializeObject<MapDetail>(await res.Content.ReadAsStringAsync()) : null;
}

public static string CreateDownloadMapUrl(string mapHash) {
return $"{BEATSAVER_CDN_URL}{mapHash.ToLower()}.zip";
}

public static string CreateMapPageUrl(string bsr) {
return $"{BEATSAVER_WEBSITE_URL}{MAPS_ENDPOINT}{bsr}";
}

public static string FormatBeatmapFolderName(string? id, string? songName, string? authorName, string? hash) {
return $"{id} ({songName} - {authorName}) [{hash}]";
}
}
}
35 changes: 35 additions & 0 deletions Source/7_Utils/FileManager.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BeatLeader.Interop;
using BeatLeader.Models.Activity;
using BeatLeader.Models.BeatSaver;
using BeatLeader.Models.Replay;
using UnityEngine;

namespace BeatLeader.Utils {
internal static class FileManager {
#region Beatmaps

private static string BeatmapsDirectory => Path.Combine(Application.dataPath, "CustomLevels");

public static async Task<bool> InstallBeatmap(byte[] bytes, string folderName) {
try {
var path = Path.Combine(BeatmapsDirectory, folderName);
using var memoryStream = new MemoryStream(bytes);
using var archive = new ZipArchive(memoryStream);
foreach (var entry in archive.Entries) {
using var entryStream = entry.Open();
var streamLength = entry.Length;
var entryBuffer = new byte[streamLength];
var bytesRead = await entryStream.ReadAsync(entryBuffer, 0, (int)streamLength);
if (bytesRead < streamLength) throw new FileLoadException();
var destinationPath = Path.Combine(path, entry.FullName);
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
using var destinationStream = File.OpenWrite(destinationPath);
await destinationStream.WriteAsync(entryBuffer, 0, (int)streamLength);
}
SongCoreInterop.TryRefreshSongs(true);
return true;
} catch (Exception ex) {
Plugin.Log.Error("Failed to install beatmap:\n" + ex);
return false;
}
}

#endregion

#region Replays

public static IEnumerable<string> GetAllReplayPaths() {
Expand Down
66 changes: 66 additions & 0 deletions Source/7_Utils/WebUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using JetBrains.Annotations;

namespace BeatLeader.Utils {
internal static class WebUtils {
public static readonly HttpClient HttpClient = new();

public static async Task<byte[]?> SendRawDataRequestAsync(
string url,
Action<HttpRequestHeaders>? headersCallback = null
) {
return await SendRawDataRequestAsync(new Uri(url), headersCallback);
}

public static async Task<byte[]?> SendRawDataRequestAsync(
Uri uri,
Action<HttpRequestHeaders>? headersCallback = null
) {
return await SendAsync(uri) is { IsSuccessStatusCode: true } res
? await res.Content.ReadAsByteArrayAsync() : null;
}

public static async Task<HttpResponseMessage> SendAsync(
string url,
string method = "GET",
Action<HttpRequestHeaders>? headersCallback = null
) {
return await SendAsync(new Uri(url, UriKind.Absolute), method, headersCallback);
}

public static async Task<HttpResponseMessage> SendAsync(
Uri uri,
string method = "GET",
Action<HttpRequestHeaders>? headersCallback = null
) {
var request = new HttpRequestMessage {
RequestUri = uri,
Method = new(method)
};
headersCallback?.Invoke(request.Headers);
return await HttpClient.SendAsync(request);
}

public static async Task<HttpResponseMessage> SendAsync(
string url,
IDictionary<string, string> headers,
string method = "GET"
) {
return await SendAsync(new Uri(url, UriKind.Absolute), headers, method);
}

public static async Task<HttpResponseMessage> SendAsync(
Uri uri,
IDictionary<string, string> headers,
string method = "GET"
) {
return await SendAsync(uri, method, x => {
foreach (var item in headers) x.Add(item.Key, item.Value);
});
}
}
}
Loading

0 comments on commit a5dce0d

Please sign in to comment.