diff --git a/SharedProject/Models/Responses/SongBestResponse.cs b/SharedProject/Models/Responses/SongBestResponse.cs new file mode 100644 index 0000000..ec23280 --- /dev/null +++ b/SharedProject/Models/Responses/SongBestResponse.cs @@ -0,0 +1,6 @@ +namespace SharedProject.Models.Responses; + +public class SongBestResponse +{ + public List SongBestData { get; set; } = new(); +} \ No newline at end of file diff --git a/SharedProject/Models/SongBestData.cs b/SharedProject/Models/SongBestData.cs new file mode 100644 index 0000000..e674c5c --- /dev/null +++ b/SharedProject/Models/SongBestData.cs @@ -0,0 +1,22 @@ +using SharedProject.Enums; + +namespace SharedProject.Models; + +public class SongBestData +{ + public uint SongId { get; set; } + + public Difficulty Difficulty { get; set; } + + public uint BestScore { get; set; } + + public uint BestRate { get; set; } + + public CrownType BestCrown { get; set; } + + public ScoreRank BestScoreRank { get; set; } + + public DateTime LastPlayTime { get; set; } + + public bool IsFavorite { get; set; } +} \ No newline at end of file diff --git a/TaikoLocalServer/Common/Utils/GZipBytesUtil.cs b/TaikoLocalServer/Common/Utils/GZipBytesUtil.cs index ab031d8..35a30aa 100644 --- a/TaikoLocalServer/Common/Utils/GZipBytesUtil.cs +++ b/TaikoLocalServer/Common/Utils/GZipBytesUtil.cs @@ -6,6 +6,11 @@ namespace TaikoLocalServer.Common.Utils; public static class GZipBytesUtil { + public static MemoryStream GenerateStreamFromString(string value) + { + return new MemoryStream(Encoding.UTF8.GetBytes(value)); + } + public static byte[] GetEmptyJsonGZipBytes() { var outputStream = new MemoryStream(1024); diff --git a/TaikoLocalServer/Controllers/Api/DashboardController.cs b/TaikoLocalServer/Controllers/Api/DashboardController.cs index e3158da..d080697 100644 --- a/TaikoLocalServer/Controllers/Api/DashboardController.cs +++ b/TaikoLocalServer/Controllers/Api/DashboardController.cs @@ -9,13 +9,10 @@ namespace TaikoLocalServer.Controllers.Api; [Route("/api/[controller]")] public class DashboardController : BaseController { - private readonly TaikoDbContext context; - private readonly ICardService cardService; - public DashboardController(TaikoDbContext context, ICardService cardService) + public DashboardController(ICardService cardService) { - this.context = context; this.cardService = cardService; } diff --git a/TaikoLocalServer/Controllers/Api/FavoriteSongsController.cs b/TaikoLocalServer/Controllers/Api/FavoriteSongsController.cs new file mode 100644 index 0000000..7dbda2c --- /dev/null +++ b/TaikoLocalServer/Controllers/Api/FavoriteSongsController.cs @@ -0,0 +1,29 @@ +using TaikoLocalServer.Services.Interfaces; + +namespace TaikoLocalServer.Controllers.Api; + +[ApiController] +[Route("api/[controller]")] +public class FavoriteSongsController : BaseController +{ + private readonly IUserDatumService userDatumService; + + public FavoriteSongsController(IUserDatumService userDatumService) + { + this.userDatumService = userDatumService; + } + + [HttpPost] + public async Task UpdateFavoriteSong(uint baid, uint songId, bool isFavorite) + { + var user = await userDatumService.GetFirstUserDatumOrNull(baid); + + if (user is null) + { + return NotFound(); + } + + await userDatumService.UpdateFavoriteSong(baid, songId, isFavorite); + return NoContent(); + } +} \ No newline at end of file diff --git a/TaikoLocalServer/Controllers/Api/PlayDataController.cs b/TaikoLocalServer/Controllers/Api/PlayDataController.cs new file mode 100644 index 0000000..364bf16 --- /dev/null +++ b/TaikoLocalServer/Controllers/Api/PlayDataController.cs @@ -0,0 +1,42 @@ +using SharedProject.Models.Responses; +using TaikoLocalServer.Services.Interfaces; + +namespace TaikoLocalServer.Controllers.Api; + +[ApiController] +[Route("api/[controller]")] +public class PlayDataController : BaseController +{ + private readonly IUserDatumService userDatumService; + + private readonly ISongBestDatumService songBestDatumService; + + public PlayDataController(IUserDatumService userDatumService, ISongBestDatumService songBestDatumService) + { + this.userDatumService = userDatumService; + this.songBestDatumService = songBestDatumService; + } + + [HttpGet("{baid}")] + public async Task> GetSongBestRecords(uint baid) + { + var user = await userDatumService.GetFirstUserDatumOrNull(baid); + if (user is null) + { + return NotFound(); + } + + var songBestRecords = await songBestDatumService.GetAllSongBestAsModel(baid); + var favoriteSongs = await userDatumService.GetFavoriteSongIds(baid); + var favoriteSet = favoriteSongs.ToHashSet(); + foreach (var songBestRecord in songBestRecords.Where(songBestRecord => favoriteSet.Contains(songBestRecord.SongId))) + { + songBestRecord.IsFavorite = true; + } + + return Ok(new SongBestResponse + { + SongBestData = songBestRecords + }); + } +} \ No newline at end of file diff --git a/TaikoLocalServer/Controllers/Api/UserSettingsController.cs b/TaikoLocalServer/Controllers/Api/UserSettingsController.cs index f4da8a9..43ac82b 100644 --- a/TaikoLocalServer/Controllers/Api/UserSettingsController.cs +++ b/TaikoLocalServer/Controllers/Api/UserSettingsController.cs @@ -11,13 +11,10 @@ namespace TaikoLocalServer.Controllers.Api; [Route("/api/[controller]/{baid}")] public class UserSettingsController : BaseController { - private readonly TaikoDbContext context; - private readonly IUserDatumService userDatumService; - public UserSettingsController(TaikoDbContext context, IUserDatumService userDatumService) + public UserSettingsController(IUserDatumService userDatumService) { - this.context = context; this.userDatumService = userDatumService; } diff --git a/TaikoLocalServer/Services/Interfaces/ISongBestDatumService.cs b/TaikoLocalServer/Services/Interfaces/ISongBestDatumService.cs index d9c4dac..6370099 100644 --- a/TaikoLocalServer/Services/Interfaces/ISongBestDatumService.cs +++ b/TaikoLocalServer/Services/Interfaces/ISongBestDatumService.cs @@ -1,8 +1,12 @@ -namespace TaikoLocalServer.Services.Interfaces; +using SharedProject.Models; + +namespace TaikoLocalServer.Services.Interfaces; public interface ISongBestDatumService { public Task> GetAllSongBestData(uint baid); public Task UpdateOrInsertSongBestDatum(SongBestDatum datum); + + public Task> GetAllSongBestAsModel(uint baid); } \ No newline at end of file diff --git a/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs b/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs index f267905..c78d592 100644 --- a/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs +++ b/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs @@ -13,4 +13,8 @@ public interface IUserDatumService public Task InsertUserDatum(UserDatum userDatum); public Task UpdateUserDatum(UserDatum userDatum); + + public Task> GetFavoriteSongIds(uint baid); + + public Task UpdateFavoriteSong(uint baid, uint songId, bool isFavorite); } \ No newline at end of file diff --git a/TaikoLocalServer/Services/SongBestDatumService.cs b/TaikoLocalServer/Services/SongBestDatumService.cs index 05178d9..33b7a98 100644 --- a/TaikoLocalServer/Services/SongBestDatumService.cs +++ b/TaikoLocalServer/Services/SongBestDatumService.cs @@ -1,4 +1,7 @@ -using TaikoLocalServer.Services.Interfaces; +using SharedProject.Models; +using Swan.Mapping; +using TaikoLocalServer.Services.Interfaces; +using Throw; namespace TaikoLocalServer.Services; @@ -32,4 +35,23 @@ public class SongBestDatumService : ISongBestDatumService await context.SongBestData.AddAsync(datum); await context.SaveChangesAsync(); } + + public async Task> GetAllSongBestAsModel(uint baid) + { + var songbestDbData = await context.SongBestData.Where(datum => datum.Baid == baid).ToListAsync(); + + var result = songbestDbData.Select(datum => datum.CopyPropertiesToNew()).ToList(); + + var playLogs = await context.SongPlayData.Where(datum => datum.Baid == baid).ToListAsync(); + foreach (var bestData in result) + { + var lastPlayLog = playLogs.Where(datum => datum.Difficulty == bestData.Difficulty && + datum.SongId == bestData.SongId) + .MaxBy(datum => datum.PlayTime); + lastPlayLog.ThrowIfNull("Last play log is null! Something is wrong with db!"); + bestData.LastPlayTime = lastPlayLog.PlayTime; + } + + return result; + } } \ No newline at end of file diff --git a/TaikoLocalServer/Services/UserDatumService.cs b/TaikoLocalServer/Services/UserDatumService.cs index 03526ca..39225ed 100644 --- a/TaikoLocalServer/Services/UserDatumService.cs +++ b/TaikoLocalServer/Services/UserDatumService.cs @@ -1,4 +1,6 @@ -using TaikoLocalServer.Services.Interfaces; +using System.Text.Json; +using TaikoLocalServer.Services.Interfaces; +using Throw; namespace TaikoLocalServer.Services; @@ -6,9 +8,12 @@ public class UserDatumService : IUserDatumService { private readonly TaikoDbContext context; - public UserDatumService(TaikoDbContext context) + private readonly ILogger logger; + + public UserDatumService(TaikoDbContext context, ILogger logger) { this.context = context; + this.logger = logger; } public async Task GetFirstUserDatumOrNull(uint baid) @@ -50,4 +55,58 @@ public class UserDatumService : IUserDatumService context.Update(userDatum); await context.SaveChangesAsync(); } + + public async Task> GetFavoriteSongIds(uint baid) + { + var userDatum = await context.UserData.FindAsync(baid); + userDatum.ThrowIfNull(); + + using var stringStream = GZipBytesUtil.GenerateStreamFromString(userDatum.FavoriteSongsArray); + List? result; + try + { + result = await JsonSerializer.DeserializeAsync>(stringStream); + } + catch (JsonException e) + { + logger.LogError(e, "Parse favorite song array json failed! Is the user initialized correctly?"); + result = new List(); + } + result.ThrowIfNull("Song favorite array should never be null!"); + return result; + } + + public async Task UpdateFavoriteSong(uint baid, uint songId, bool isFavorite) + { + var userDatum = await context.UserData.FindAsync(baid); + userDatum.ThrowIfNull(); + + using var stringStream = GZipBytesUtil.GenerateStreamFromString(userDatum.FavoriteSongsArray); + List? favoriteSongIds; + try + { + favoriteSongIds = await JsonSerializer.DeserializeAsync>(stringStream); + } + catch (JsonException e) + { + logger.LogError(e, "Parse favorite song array json failed! Is the user initialized correctly?"); + favoriteSongIds = new List(); + } + favoriteSongIds.ThrowIfNull("Song favorite array should never be null!"); + var favoriteSet = new HashSet(favoriteSongIds); + if (isFavorite) + { + favoriteSet.Add(songId); + } + else + { + favoriteSet.Remove(songId); + } + + await JsonSerializer.SerializeAsync(stringStream, favoriteSet); + + context.Update(userDatum); + await context.SaveChangesAsync(); + } + } \ No newline at end of file