diff --git a/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusHandler.cs b/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusHandler.cs new file mode 100644 index 0000000..7c11c5f --- /dev/null +++ b/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusHandler.cs @@ -0,0 +1,11 @@ +using JSMR.Application.Circles.Ports; + +namespace JSMR.Application.Circles.Commands.UpdateCircleStatus; + +public class UpdateCircleStatusHandler(ICircleWriter writer) +{ + public Task HandleAsync(UpdateCircleStatusRequest request, CancellationToken cancellationToken = default) + { + return writer.UpdateStatusAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusRequest.cs b/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusRequest.cs new file mode 100644 index 0000000..4550f4b --- /dev/null +++ b/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusRequest.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Circles.Contracts; + +namespace JSMR.Application.Circles.Commands.UpdateCircleStatus; + +public sealed record UpdateCircleStatusRequest(int CircleId, CircleStatus CircleStatus); \ No newline at end of file diff --git a/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusResponse.cs b/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusResponse.cs new file mode 100644 index 0000000..f8e785f --- /dev/null +++ b/JSMR.Application/Circles/Commands/UpdateCircleStatus/UpdateCircleStatusResponse.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Circles.Contracts; + +namespace JSMR.Application.Circles.Commands.UpdateCircleStatus; + +public sealed record UpdateCircleStatusResponse(int CircleId, CircleStatus CircleStatus); \ No newline at end of file diff --git a/JSMR.Application/Circles/Contracts/CircleStatus.cs b/JSMR.Application/Circles/Contracts/CircleStatus.cs new file mode 100644 index 0000000..a52dd77 --- /dev/null +++ b/JSMR.Application/Circles/Contracts/CircleStatus.cs @@ -0,0 +1,9 @@ +namespace JSMR.Application.Circles.Contracts; + +public enum CircleStatus +{ + Neutral, + Favorite, + Blacklisted, + Spam +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Ports/ICircleReader.cs b/JSMR.Application/Circles/Ports/ICircleReader.cs new file mode 100644 index 0000000..05a5fd6 --- /dev/null +++ b/JSMR.Application/Circles/Ports/ICircleReader.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Circles.Ports; + +public interface ICircleReader +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Ports/ICircleWriter.cs b/JSMR.Application/Circles/Ports/ICircleWriter.cs new file mode 100644 index 0000000..aa2bc81 --- /dev/null +++ b/JSMR.Application/Circles/Ports/ICircleWriter.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Circles.Commands.UpdateCircleStatus; + +namespace JSMR.Application.Circles.Ports; + +public interface ICircleWriter +{ + Task UpdateStatusAsync(UpdateCircleStatusRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetCreators/CircleCreatorItem.cs b/JSMR.Application/Circles/Queries/GetCreators/CircleCreatorItem.cs new file mode 100644 index 0000000..485117d --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetCreators/CircleCreatorItem.cs @@ -0,0 +1,11 @@ +namespace JSMR.Application.Circles.Queries.GetCreators; + +public sealed record CircleCreatorItem( + string CreatorName, + DateTime? FirstCollaborationDate, + DateTime? LastCollaborationDate, + int Downloads, + int Count) +{ + public readonly int AverageDownloads = Downloads / Count; +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsHandler.cs b/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsHandler.cs new file mode 100644 index 0000000..8bf672e --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsHandler.cs @@ -0,0 +1,9 @@ +namespace JSMR.Application.Circles.Queries.GetCreators; + +public sealed class GetCircleCreatorsHandler(ICircleCreatorsProvider provider) +{ + public Task HandleAsync(GetCircleCreatorsRequest request, CancellationToken cancellationToken = default) + { + return provider.HandleAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsRequest.cs b/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsRequest.cs new file mode 100644 index 0000000..f78c383 --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsRequest.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Circles.Queries.GetCreators; + +public sealed record GetCircleCreatorsRequest(int? CircleId, string? NameOrMakerId); \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsResponse.cs b/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsResponse.cs new file mode 100644 index 0000000..c30a577 --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetCreators/GetCircleCreatorsResponse.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Circles.Queries.GetCreators; + +public sealed record GetCircleCreatorsResponse(IReadOnlyList Items); \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetCreators/ICircleCreatorsProvider.cs b/JSMR.Application/Circles/Queries/GetCreators/ICircleCreatorsProvider.cs new file mode 100644 index 0000000..cb22b05 --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetCreators/ICircleCreatorsProvider.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Circles.Queries.GetCreators; + +public interface ICircleCreatorsProvider +{ + Task HandleAsync(GetCircleCreatorsRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetTags/CircleTagItem.cs b/JSMR.Application/Circles/Queries/GetTags/CircleTagItem.cs new file mode 100644 index 0000000..0159ca5 --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetTags/CircleTagItem.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Circles.Queries.GetTags; + +public sealed record CircleTagItem(string TagName, string? EnglishTagName, int Count); \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsHandler.cs b/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsHandler.cs new file mode 100644 index 0000000..bb1bfa5 --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsHandler.cs @@ -0,0 +1,9 @@ +namespace JSMR.Application.Circles.Queries.GetTags; + +public sealed class GetCircleTagsHandler(ICircleTagsProvider provider) +{ + public Task HandleAsync(GetCircleTagsRequest request, CancellationToken cancellationToken = default) + { + return provider.HandleAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsRequest.cs b/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsRequest.cs new file mode 100644 index 0000000..f57c3ad --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsRequest.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Circles.Queries.GetTags; + +public sealed record GetCircleTagsRequest(int? CircleId, string? NameOrMakerId); \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsResponse.cs b/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsResponse.cs new file mode 100644 index 0000000..25926e6 --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetTags/GetCircleTagsResponse.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Circles.Queries.GetTags; + +public sealed record GetCircleTagsResponse(IReadOnlyList Items); \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/GetTags/ICircleTagsProvider.cs b/JSMR.Application/Circles/Queries/GetTags/ICircleTagsProvider.cs new file mode 100644 index 0000000..71138fa --- /dev/null +++ b/JSMR.Application/Circles/Queries/GetTags/ICircleTagsProvider.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Circles.Queries.GetTags; + +public interface ICircleTagsProvider +{ + Task HandleAsync(GetCircleTagsRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/CircleSearchCriteria.cs b/JSMR.Application/Circles/Queries/Search/CircleSearchCriteria.cs new file mode 100644 index 0000000..93c3224 --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/CircleSearchCriteria.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Circles.Queries.Search; + +public class CircleSearchCriteria +{ + public string? Name { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/CircleSearchItem.cs b/JSMR.Application/Circles/Queries/Search/CircleSearchItem.cs new file mode 100644 index 0000000..3fce997 --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/CircleSearchItem.cs @@ -0,0 +1,19 @@ +namespace JSMR.Application.Circles.Queries.Search; + +public record CircleSearchItem +{ + public int CircleId { get; init; } + public string Name { get; init; } = string.Empty; + public string MakerId { get; init; } = string.Empty; + public bool Blacklisted { get; init; } + public bool Favorite { get; init; } + public bool Spam { get; init; } + public int Downloads { get; init; } + public int Releases { get; init; } + public int Pending { get; init; } + public DateTime? FirstReleaseDate { get; init; } + public DateTime? LatestReleaseDate { get; init; } + public string? LatestProductId { get; init; } + public bool? LatestVoiceWorkHasImage { get; init; } + public DateTime? LatestVoiceWorkSalesDate { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/CircleSearchOptions.cs b/JSMR.Application/Circles/Queries/Search/CircleSearchOptions.cs new file mode 100644 index 0000000..f212d02 --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/CircleSearchOptions.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Circles.Queries.Search; + +//public record TagSearchOptions : SearchOptions +//{ + +//} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/CircleSearchResults.cs b/JSMR.Application/Circles/Queries/Search/CircleSearchResults.cs new file mode 100644 index 0000000..dbb8110 --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/CircleSearchResults.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Circles.Queries.Search; + +public record CircleSearchResults : SearchResult +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/CircleSortField.cs b/JSMR.Application/Circles/Queries/Search/CircleSortField.cs new file mode 100644 index 0000000..036648f --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/CircleSortField.cs @@ -0,0 +1,10 @@ +namespace JSMR.Application.Circles.Queries.Search; + +public enum CircleSortField +{ + Name, + Blacklisted, + Favorite, + Spam, + VoiceWorkCount +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/ICircleSearchProvider.cs b/JSMR.Application/Circles/Queries/Search/ICircleSearchProvider.cs new file mode 100644 index 0000000..495e9d5 --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/ICircleSearchProvider.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Circles.Queries.Search; + +public interface ICircleSearchProvider : ISearchProvider +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs b/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs new file mode 100644 index 0000000..d20daaa --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/SearchCirclesHandler.cs @@ -0,0 +1,15 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Circles.Queries.Search; + +public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider) +{ + public async Task HandleAsync(SearchCirclesRequest request, CancellationToken cancellationToken) + { + SearchOptions searchOptions = request.Options; + + SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken); + + return new SearchCirclesResponse(results); + } +} \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/SearchCirclesRequest.cs b/JSMR.Application/Circles/Queries/Search/SearchCirclesRequest.cs new file mode 100644 index 0000000..5c10d0d --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/SearchCirclesRequest.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Circles.Queries.Search; + +public sealed record SearchCirclesRequest(SearchOptions Options); \ No newline at end of file diff --git a/JSMR.Application/Circles/Queries/Search/SearchCirclesResponse.cs b/JSMR.Application/Circles/Queries/Search/SearchCirclesResponse.cs new file mode 100644 index 0000000..890bd08 --- /dev/null +++ b/JSMR.Application/Circles/Queries/Search/SearchCirclesResponse.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Circles.Queries.Search; + +public sealed record SearchCirclesResponse(SearchResult Results); \ No newline at end of file diff --git a/JSMR.Application/Common/Caching/CacheEntryOptions.cs b/JSMR.Application/Common/Caching/CacheEntryOptions.cs new file mode 100644 index 0000000..1491b45 --- /dev/null +++ b/JSMR.Application/Common/Caching/CacheEntryOptions.cs @@ -0,0 +1,69 @@ +namespace JSMR.Application.Common.Caching; + +public class CacheEntryOptions +{ + private DateTimeOffset? _absoluteExpiration; + private TimeSpan? _absoluteExpirationRelativeToNow; + private TimeSpan? _slidingExpiration; + + /// + /// Gets or sets an absolute expiration date for the cache entry. + /// + public DateTimeOffset? AbsoluteExpiration + { + get + { + return _absoluteExpiration; + } + set + { + _absoluteExpiration = value; + } + } + + /// + /// Gets or sets an absolute expiration time, relative to now. + /// + public TimeSpan? AbsoluteExpirationRelativeToNow + { + get + { + return _absoluteExpirationRelativeToNow; + } + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException( + nameof(AbsoluteExpirationRelativeToNow), + value, + "The relative expiration value must be positive."); + } + + _absoluteExpirationRelativeToNow = value; + } + } + + /// + /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. + /// This will not extend the entry lifetime beyond the absolute expiration (if set). + /// + public TimeSpan? SlidingExpiration + { + get + { + return _slidingExpiration; + } + set + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException( + nameof(SlidingExpiration), + value, + "The sliding expiration value must be positive."); + } + _slidingExpiration = value; + } + } +} \ No newline at end of file diff --git a/JSMR.Application/Common/Caching/ICache.cs b/JSMR.Application/Common/Caching/ICache.cs new file mode 100644 index 0000000..1aee3c8 --- /dev/null +++ b/JSMR.Application/Common/Caching/ICache.cs @@ -0,0 +1,7 @@ +namespace JSMR.Application.Common.Caching; + +public interface ICache +{ + ValueTask GetAsync(string key, CancellationToken cancellationToken = default); + ValueTask SetAsync(string key, T value, CacheEntryOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Common/Language.cs b/JSMR.Application/Common/Language.cs new file mode 100644 index 0000000..5f2c384 --- /dev/null +++ b/JSMR.Application/Common/Language.cs @@ -0,0 +1,11 @@ +namespace JSMR.Application.Common; + +public enum Language +{ + Unknown, + Japanese, + English, + ChineseSimplified, + ChineseTraditional, + Korean +} \ No newline at end of file diff --git a/JSMR.Application/Common/Search/ISearchProvider.cs b/JSMR.Application/Common/Search/ISearchProvider.cs new file mode 100644 index 0000000..01e967f --- /dev/null +++ b/JSMR.Application/Common/Search/ISearchProvider.cs @@ -0,0 +1,8 @@ +namespace JSMR.Application.Common.Search; + +public interface ISearchProvider + where TCriteria : new() + where TSortField : struct, Enum +{ + Task> SearchAsync(SearchOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Common/Search/ISearchQueryHandler.cs b/JSMR.Application/Common/Search/ISearchQueryHandler.cs new file mode 100644 index 0000000..7ba5b25 --- /dev/null +++ b/JSMR.Application/Common/Search/ISearchQueryHandler.cs @@ -0,0 +1,8 @@ +namespace JSMR.Application.Common.Search; + +public interface ISearchQueryHandler + where TCriteria : new() + where TSortField : struct, Enum +{ + Task> HandleAsync(SearchOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Common/Search/SearchOptions.cs b/JSMR.Application/Common/Search/SearchOptions.cs new file mode 100644 index 0000000..fd31cc3 --- /dev/null +++ b/JSMR.Application/Common/Search/SearchOptions.cs @@ -0,0 +1,11 @@ +namespace JSMR.Application.Common.Search; + +public sealed record SearchOptions + where TCriteria : new() + where TSortField : struct, Enum +{ + public int PageNumber { get; set; } = 1; + public int PageSize { get; set; } = 100; + public TCriteria Criteria { get; set; } = new(); + public SortOption[] SortOptions { get; set; } = []; +} \ No newline at end of file diff --git a/JSMR.Application/Common/Search/SearchReader.cs b/JSMR.Application/Common/Search/SearchReader.cs new file mode 100644 index 0000000..4db011d --- /dev/null +++ b/JSMR.Application/Common/Search/SearchReader.cs @@ -0,0 +1,71 @@ +using JSMR.Application.Tags.Queries.Search.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace JSMR.Application.Common.Search; + + + + +//public abstract class SearchReader2 : ISearchReader +// where TSortField : struct, Enum +//{ +// public Task> SearchAsync(SearchOptions options, CancellationToken cancellationToken = default) +// { +// throw new NotImplementedException(); +// } +//} + +//public abstract class SearchReader where TSortField : struct, Enum +//{ +// public async Task> SearchAsync(SearchOptions options, CancellationToken cancellationToken = default) +// { +// IQueryable baseQuery = GetBaseQuery(); +// IQueryable filteredQuery = ApplyFilters(baseQuery, options); + +// int total = await filteredQuery.CountAsync(cancellationToken); + +// IOrderedQueryable orderedQuery = ApplySorting(filteredQuery, options); + +// TSearchResultItem[] items = await orderedQuery +// .Skip((options.PageNumber - 1) * options.PageSize) +// .Take(options.PageSize) +// .ToArrayAsync(cancellationToken); + +// return new SearchResult() +// { +// Items = items, +// TotalItems = total +// }; +// } + +// protected abstract IQueryable GetBaseQuery(); +// protected abstract IQueryable ApplyFilters(IQueryable query, SearchOptions options); + +// private IOrderedQueryable ApplySorting(IQueryable query, SearchOptions options) +// { +// IOrderedQueryable? ordered = null; + +// for (int i = 0; i < options.SortOptions.Length; i++) +// { +// var (field, direction) = (options.SortOptions[i].Field, options.SortOptions[i].Direction); +// bool isDescending = direction == SortDirection.Descending; + +// IOrderedQueryable applyFirst(Expression> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector); +// IOrderedQueryable applyNext(Expression> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector); + +// Expression> selector = GetSortExpression(field); + +// ordered = (i == 0) ? applyFirst(selector) : applyNext(selector); +// } + +// return ordered ?? GetDefaultSortExpression(query); +// } + +// protected abstract Expression> GetSortExpression(TSortField field); +// protected abstract IOrderedQueryable GetDefaultSortExpression(IQueryable query); +//} \ No newline at end of file diff --git a/JSMR.Application/Common/Search/SearchResult.cs b/JSMR.Application/Common/Search/SearchResult.cs new file mode 100644 index 0000000..43aa994 --- /dev/null +++ b/JSMR.Application/Common/Search/SearchResult.cs @@ -0,0 +1,7 @@ +namespace JSMR.Application.Common.Search; + +public record SearchResult +{ + public T[] Items { get; set; } = []; + public int TotalItems { get; set; } +} diff --git a/JSMR.Application/Common/Search/SortDirection.cs b/JSMR.Application/Common/Search/SortDirection.cs new file mode 100644 index 0000000..38c811f --- /dev/null +++ b/JSMR.Application/Common/Search/SortDirection.cs @@ -0,0 +1,7 @@ +namespace JSMR.Application.Common.Search; + +public enum SortDirection +{ + Ascending = 0, + Descending = 1 +} \ No newline at end of file diff --git a/JSMR.Application/Common/Search/SortOption.cs b/JSMR.Application/Common/Search/SortOption.cs new file mode 100644 index 0000000..ade2394 --- /dev/null +++ b/JSMR.Application/Common/Search/SortOption.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Common.Search; + +public record SortOption(TSortField Field, SortDirection Direction) where TSortField : struct, Enum; \ No newline at end of file diff --git a/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusHandler.cs b/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusHandler.cs new file mode 100644 index 0000000..05fbf4f --- /dev/null +++ b/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusHandler.cs @@ -0,0 +1,11 @@ +using JSMR.Application.Creators.Ports; + +namespace JSMR.Application.Creators.Commands.UpdateCreatorStatus; + +public class UpdateCreatorStatusHandler(ICreatorWriter writer) +{ + public Task HandleAsync(UpdateCreatorStatusRequest request, CancellationToken cancellationToken = default) + { + return writer.UpdateStatusAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusRequest.cs b/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusRequest.cs new file mode 100644 index 0000000..e9b6841 --- /dev/null +++ b/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusRequest.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Creators.Contracts; + +namespace JSMR.Application.Creators.Commands.UpdateCreatorStatus; + +public sealed record UpdateCreatorStatusRequest(int CreatorId, CreatorStatus CreatorStatus); \ No newline at end of file diff --git a/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusResponse.cs b/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusResponse.cs new file mode 100644 index 0000000..0f57912 --- /dev/null +++ b/JSMR.Application/Creators/Commands/UpdateCreatorStatus/UpdateCreatorStatusResponse.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Creators.Contracts; + +namespace JSMR.Application.Creators.Commands.UpdateCreatorStatus; + +public sealed record UpdateCreatorStatusResponse(int CreatorId, CreatorStatus CreatorStatus); \ No newline at end of file diff --git a/JSMR.Application/Creators/Contracts/CreatorStatus.cs b/JSMR.Application/Creators/Contracts/CreatorStatus.cs new file mode 100644 index 0000000..d19428b --- /dev/null +++ b/JSMR.Application/Creators/Contracts/CreatorStatus.cs @@ -0,0 +1,8 @@ +namespace JSMR.Application.Creators.Contracts; + +public enum CreatorStatus +{ + Neutral, + Favorite, + Blacklisted +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Ports/ICreatorReader.cs b/JSMR.Application/Creators/Ports/ICreatorReader.cs new file mode 100644 index 0000000..2a40291 --- /dev/null +++ b/JSMR.Application/Creators/Ports/ICreatorReader.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Creators.Ports; + +public interface ICircleReader +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Ports/ICreatorWriter.cs b/JSMR.Application/Creators/Ports/ICreatorWriter.cs new file mode 100644 index 0000000..05b5393 --- /dev/null +++ b/JSMR.Application/Creators/Ports/ICreatorWriter.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Creators.Commands.UpdateCreatorStatus; + +namespace JSMR.Application.Creators.Ports; + +public interface ICreatorWriter +{ + Task UpdateStatusAsync(UpdateCreatorStatusRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchCriteria.cs b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchCriteria.cs new file mode 100644 index 0000000..e513ab3 --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchCriteria.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Creators.Queries.Search.Contracts; + +public class CreatorSearchCriteria +{ + public string? Name { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchItem.cs b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchItem.cs new file mode 100644 index 0000000..25bd28e --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchItem.cs @@ -0,0 +1,10 @@ +namespace JSMR.Application.Creators.Queries.Search.Contracts; + +public record CreatorSearchItem +{ + public int CreatorId { get; init; } + public required string Name { get; init; } + public bool Favorite { get; init; } + public bool Blacklisted { get; init; } + public int VoiceWorkCount { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchOptions.cs b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchOptions.cs new file mode 100644 index 0000000..be4cb3c --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchOptions.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Creators.Queries.Search.Contracts; + +//public record TagSearchOptions : SearchOptions +//{ + +//} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchResults.cs b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchResults.cs new file mode 100644 index 0000000..8904ff0 --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSearchResults.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Creators.Queries.Search.Contracts; + +public record CreatorSearchResults : SearchResult +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSortField.cs b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSortField.cs new file mode 100644 index 0000000..a9d2124 --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/Contracts/CreatorSortField.cs @@ -0,0 +1,9 @@ +namespace JSMR.Application.Creators.Queries.Search.Contracts; + +public enum CreatorSortField +{ + Name, + Blacklisted, + Favorite, + VoiceWorkCount +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/Ports/ICreatorSearchProvider.cs b/JSMR.Application/Creators/Queries/Search/Ports/ICreatorSearchProvider.cs new file mode 100644 index 0000000..aef4941 --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/Ports/ICreatorSearchProvider.cs @@ -0,0 +1,9 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; + +namespace JSMR.Application.Creators.Queries.Search.Ports; + +public interface ICreatorSearchProvider : ISearchProvider +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs b/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs new file mode 100644 index 0000000..f9bc5bc --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/SearchCreatorsHandler.cs @@ -0,0 +1,32 @@ +using JSMR.Application.Common.Caching; +using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; +using JSMR.Application.Creators.Queries.Search.Ports; + +namespace JSMR.Application.Creators.Queries.Search; + +public sealed class SearchCreatorsHandler(ICreatorSearchProvider searchProvider, ICache cache) +{ + public async Task HandleAsync(SearchCreatorsRequest request, CancellationToken cancellationToken) + { + SearchOptions searchOptions = request.Options; + + string cacheKey = $"creator:{searchOptions.GetHashCode()}"; + + CreatorSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken); + + if (cachedResults != null) + return new SearchCreatorsResponse(cachedResults); + + SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken); + + CacheEntryOptions cacheEntryOptions = new() + { + SlidingExpiration = TimeSpan.FromMinutes(10) + }; + + await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken); + + return new SearchCreatorsResponse(results); + } +} \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/SearchCreatorsRequest.cs b/JSMR.Application/Creators/Queries/Search/SearchCreatorsRequest.cs new file mode 100644 index 0000000..595c9bb --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/SearchCreatorsRequest.cs @@ -0,0 +1,6 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; + +namespace JSMR.Application.Creators.Queries.Search; + +public sealed record SearchCreatorsRequest(SearchOptions Options); \ No newline at end of file diff --git a/JSMR.Application/Creators/Queries/Search/SearchCreatorsResponse.cs b/JSMR.Application/Creators/Queries/Search/SearchCreatorsResponse.cs new file mode 100644 index 0000000..3704e7e --- /dev/null +++ b/JSMR.Application/Creators/Queries/Search/SearchCreatorsResponse.cs @@ -0,0 +1,6 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; + +namespace JSMR.Application.Creators.Queries.Search; + +public sealed record SearchCreatorsResponse(SearchResult Results); \ No newline at end of file diff --git a/JSMR.Application/DI/ApplicationServiceCollectionExtensions.cs b/JSMR.Application/DI/ApplicationServiceCollectionExtensions.cs new file mode 100644 index 0000000..0ecb076 --- /dev/null +++ b/JSMR.Application/DI/ApplicationServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using JSMR.Application.Tags.Commands.SetEnglishName; +using JSMR.Application.Tags.Commands.UpdateTagStatus; +using JSMR.Application.VoiceWorks.Search; +using Microsoft.Extensions.DependencyInjection; + +namespace JSMR.Application.DI; + +public static class ApplicationServiceCollectionExtensions +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + // Handlers / Use-cases + //services.AddScoped(); + //services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/Chobit/Models/ChobitResult.cs b/JSMR.Application/Integrations/Chobit/Models/ChobitResult.cs new file mode 100644 index 0000000..c4839f3 --- /dev/null +++ b/JSMR.Application/Integrations/Chobit/Models/ChobitResult.cs @@ -0,0 +1,7 @@ +namespace JSMR.Application.Integrations.Chobit.Models; + +public class ChobitResult +{ + public int Count { get; set; } + public ChobitWork[] Works { get; set; } = []; +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/Chobit/Models/ChobitWork.cs b/JSMR.Application/Integrations/Chobit/Models/ChobitWork.cs new file mode 100644 index 0000000..2fe31c6 --- /dev/null +++ b/JSMR.Application/Integrations/Chobit/Models/ChobitWork.cs @@ -0,0 +1,16 @@ +namespace JSMR.Application.Integrations.Chobit.Models; + +public class ChobitWork +{ + public string? WorkId { get; set; } + public string? DLSiteWorkId { get; set; } + public string? WorkName { get; set; } + public string? WorkNameKana { get; set; } + public string? URL { get; set; } + public string? EmbedURL { get; set; } + public string? Thumb { get; set; } + public string? MiniThumb { get; set; } + public string? FileType { get; set; } + public decimal EmbedWidth { get; set; } + public decimal EmbedHeight { get; set; } +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/Chobit/Models/ChobitWorkResult.cs b/JSMR.Application/Integrations/Chobit/Models/ChobitWorkResult.cs new file mode 100644 index 0000000..8cafea8 --- /dev/null +++ b/JSMR.Application/Integrations/Chobit/Models/ChobitWorkResult.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Integrations.Chobit.Models; + +public class ChobitWorkResult : Dictionary { } \ No newline at end of file diff --git a/JSMR.Application/Integrations/Cien/Models/CienArticle.cs b/JSMR.Application/Integrations/Cien/Models/CienArticle.cs new file mode 100644 index 0000000..83fb3e4 --- /dev/null +++ b/JSMR.Application/Integrations/Cien/Models/CienArticle.cs @@ -0,0 +1,8 @@ +namespace JSMR.Application.Integrations.Cien.Models; + +public class CienArticle +{ + public int Id { get; set; } + public required string Title { get; set; } + public required DateTime ReleasedAt { get; set; } +} diff --git a/JSMR.Application/Integrations/Cien/Models/CienCreator.cs b/JSMR.Application/Integrations/Cien/Models/CienCreator.cs new file mode 100644 index 0000000..e0b4ab7 --- /dev/null +++ b/JSMR.Application/Integrations/Cien/Models/CienCreator.cs @@ -0,0 +1,10 @@ +namespace JSMR.Application.Integrations.Cien.Models; + +public class CienCreator +{ + public int Id { get; set; } + public required string Name { get; set; } + public string? Rating { get; set; } + public string? Icon { get; set; } + public CienArticle[] Articles { get; set; } = []; +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/DLSite/Models/VoiceWorkDetailCollection.cs b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkDetailCollection.cs new file mode 100644 index 0000000..38e8afd --- /dev/null +++ b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkDetailCollection.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Integrations.DLSite.Models; + +public class VoiceWorkDetailCollection : Dictionary +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/DLSite/Models/VoiceWorkDetails.cs b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkDetails.cs new file mode 100644 index 0000000..aee063a --- /dev/null +++ b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkDetails.cs @@ -0,0 +1,16 @@ +using JSMR.Application.Common; + +namespace JSMR.Application.Integrations.DLSite.Models; + +public class VoiceWorkDetails +{ + public VoiceWorkSeries? Series { get; init; } + public VoiceWorkTranslation? Translation { get; init; } + public int WishlistCount { get; init; } + public int DownloadCount { get; init; } + public DateTime? RegistrationDate { get; init; } + public Language[] SupportedLanguages { get; init; } = []; + //public AIGeneration AI { get; init; } + public bool HasTrial { get; init; } + public bool HasReviews { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/DLSite/Models/VoiceWorkSeries.cs b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkSeries.cs new file mode 100644 index 0000000..1d1d107 --- /dev/null +++ b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkSeries.cs @@ -0,0 +1,7 @@ +namespace JSMR.Application.Integrations.DLSite.Models; + +public class VoiceWorkSeries +{ + public required string Identifier { get; init; } + public required string Name { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/DLSite/Models/VoiceWorkTranslation.cs b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkTranslation.cs new file mode 100644 index 0000000..6db3e7b --- /dev/null +++ b/JSMR.Application/Integrations/DLSite/Models/VoiceWorkTranslation.cs @@ -0,0 +1,10 @@ +using JSMR.Application.Common; + +namespace JSMR.Application.Integrations.DLSite.Models; + +public class VoiceWorkTranslation +{ + public required string OriginalProductId { get; init; } + public bool IsOfficialTranslation { get; init; } + public required Language Language { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/Ports/IChobitClient.cs b/JSMR.Application/Integrations/Ports/IChobitClient.cs new file mode 100644 index 0000000..07fbaef --- /dev/null +++ b/JSMR.Application/Integrations/Ports/IChobitClient.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Integrations.Chobit.Models; + +namespace JSMR.Application.Integrations.Ports; + +public interface IChobitClient +{ + Task GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/Ports/ICienClient.cs b/JSMR.Application/Integrations/Ports/ICienClient.cs new file mode 100644 index 0000000..fd2a33b --- /dev/null +++ b/JSMR.Application/Integrations/Ports/ICienClient.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Integrations.Cien.Models; + +namespace JSMR.Application.Integrations.Ports; + +public interface ICienClient +{ + Task GetCienCreatorAsync(string makerId, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Integrations/Ports/IDLSiteClient.cs b/JSMR.Application/Integrations/Ports/IDLSiteClient.cs new file mode 100644 index 0000000..85d8516 --- /dev/null +++ b/JSMR.Application/Integrations/Ports/IDLSiteClient.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Integrations.DLSite.Models; + +namespace JSMR.Application.Integrations.Ports; + +public interface IDLSiteClient +{ + Task GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/JSMR.Application.csproj b/JSMR.Application/JSMR.Application.csproj new file mode 100644 index 0000000..44ead7b --- /dev/null +++ b/JSMR.Application/JSMR.Application.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + diff --git a/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameHandler.cs b/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameHandler.cs new file mode 100644 index 0000000..b4c1761 --- /dev/null +++ b/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameHandler.cs @@ -0,0 +1,20 @@ +using JSMR.Application.Tags.Ports; + +namespace JSMR.Application.Tags.Commands.SetEnglishName; + +public sealed class SetTagEnglishNameHandler(ITagWriter tagWriter) +{ + public async Task HandleAsync(SetTagEnglishNameRequest request, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(request.EnglishName)) + throw new ArgumentException("English name is required.", nameof(request)); + + string trimmed = request.EnglishName.Trim(); + SetTagEnglishNameResponse result = await tagWriter.SetEnglishNameAsync(request with { EnglishName = trimmed }, cancellationToken); + + // Optional: refresh search materialization for all voice works that use this tag + //await _searchUpdater.UpdateForTagAsync(request.TagId, cancellationToken); + + return result; + } +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameRequest.cs b/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameRequest.cs new file mode 100644 index 0000000..ab0c4a1 --- /dev/null +++ b/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameRequest.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Tags.Commands.SetEnglishName; + +public sealed record SetTagEnglishNameRequest(int TagId, string EnglishName); \ No newline at end of file diff --git a/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameResponse.cs b/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameResponse.cs new file mode 100644 index 0000000..d10ff15 --- /dev/null +++ b/JSMR.Application/Tags/Commands/SetEnglishName/SetTagEnglishNameResponse.cs @@ -0,0 +1,3 @@ +namespace JSMR.Application.Tags.Commands.SetEnglishName; + +public sealed record SetTagEnglishNameResponse(int TagId, string EnglishName); \ No newline at end of file diff --git a/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusHandler.cs b/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusHandler.cs new file mode 100644 index 0000000..0c2e123 --- /dev/null +++ b/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusHandler.cs @@ -0,0 +1,11 @@ +using JSMR.Application.Tags.Ports; + +namespace JSMR.Application.Tags.Commands.UpdateTagStatus; + +public sealed class UpdateTagStatusHandler(ITagWriter writer) +{ + public Task HandleAsync(UpdateTagStatusRequest request, CancellationToken cancellationToken = default) + { + return writer.UpdateStatusAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusRequest.cs b/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusRequest.cs new file mode 100644 index 0000000..5663ec4 --- /dev/null +++ b/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusRequest.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Tags.Contracts; + +namespace JSMR.Application.Tags.Commands.UpdateTagStatus; + +public sealed record UpdateTagStatusRequest(int TagId, TagStatus TagStatus); \ No newline at end of file diff --git a/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusResponse.cs b/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusResponse.cs new file mode 100644 index 0000000..9c30210 --- /dev/null +++ b/JSMR.Application/Tags/Commands/UpdateTagStatus/UpdateTagStatusResponse.cs @@ -0,0 +1,5 @@ +using JSMR.Application.Tags.Contracts; + +namespace JSMR.Application.Tags.Commands.UpdateTagStatus; + +public sealed record UpdateTagStatusResponse(int TagId, TagStatus TagStatus); \ No newline at end of file diff --git a/JSMR.Application/Tags/Contracts/TagStatus.cs b/JSMR.Application/Tags/Contracts/TagStatus.cs new file mode 100644 index 0000000..23ef5f4 --- /dev/null +++ b/JSMR.Application/Tags/Contracts/TagStatus.cs @@ -0,0 +1,8 @@ +namespace JSMR.Application.Tags.Contracts; + +public enum TagStatus +{ + Neutral, + Favorite, + Blacklisted +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Ports/ITagReader.cs b/JSMR.Application/Tags/Ports/ITagReader.cs new file mode 100644 index 0000000..42ffd98 --- /dev/null +++ b/JSMR.Application/Tags/Ports/ITagReader.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Tags.Queries.Search.Contracts; + +namespace JSMR.Application.Tags.Ports; + +public interface ITagReader +{ + //Task SearchAsync(TagSearchOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Ports/ITagWriter.cs b/JSMR.Application/Tags/Ports/ITagWriter.cs new file mode 100644 index 0000000..e8e194c --- /dev/null +++ b/JSMR.Application/Tags/Ports/ITagWriter.cs @@ -0,0 +1,10 @@ +using JSMR.Application.Tags.Commands.SetEnglishName; +using JSMR.Application.Tags.Commands.UpdateTagStatus; + +namespace JSMR.Application.Tags.Ports; + +public interface ITagWriter +{ + Task SetEnglishNameAsync(SetTagEnglishNameRequest request, CancellationToken cancellationToken = default); + Task UpdateStatusAsync(UpdateTagStatusRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchCriteria.cs b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchCriteria.cs new file mode 100644 index 0000000..854220a --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchCriteria.cs @@ -0,0 +1,6 @@ +namespace JSMR.Application.Tags.Queries.Search.Contracts; + +public class TagSearchCriteria +{ + public string? Name { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchItem.cs b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchItem.cs new file mode 100644 index 0000000..be2e897 --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchItem.cs @@ -0,0 +1,11 @@ +namespace JSMR.Application.Tags.Queries.Search.Contracts; + +public record TagSearchItem +{ + public int TagId { get; init; } + public required string Name { get; init; } + public bool Favorite { get; init; } + public bool Blacklisted { get; init; } + public string? EnglishName { get; init; } + public int VoiceWorkCount { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchOptions.cs b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchOptions.cs new file mode 100644 index 0000000..a7df9a0 --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchOptions.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Tags.Queries.Search.Contracts; + +//public record TagSearchOptions : SearchOptions +//{ + +//} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchResults.cs b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchResults.cs new file mode 100644 index 0000000..3ba8c0c --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/Contracts/TagSearchResults.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.Tags.Queries.Search.Contracts; + +public record TagSearchResults : SearchResult +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/Contracts/TagSortField.cs b/JSMR.Application/Tags/Queries/Search/Contracts/TagSortField.cs new file mode 100644 index 0000000..a815843 --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/Contracts/TagSortField.cs @@ -0,0 +1,10 @@ +namespace JSMR.Application.Tags.Queries.Search.Contracts; + +public enum TagSortField +{ + Name, + EnglishName, + Blacklisted, + Favorite, + VoiceWorkCount +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/Ports/ITagSearchProvider.cs b/JSMR.Application/Tags/Queries/Search/Ports/ITagSearchProvider.cs new file mode 100644 index 0000000..e6402b4 --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/Ports/ITagSearchProvider.cs @@ -0,0 +1,9 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Tags.Queries.Search.Contracts; + +namespace JSMR.Application.Tags.Queries.Search.Ports; + +public interface ITagSearchProvider : ISearchProvider +{ + +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs b/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs new file mode 100644 index 0000000..acad2f0 --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/SearchTagsHandler.cs @@ -0,0 +1,32 @@ +using JSMR.Application.Common.Caching; +using JSMR.Application.Common.Search; +using JSMR.Application.Tags.Queries.Search.Contracts; +using JSMR.Application.Tags.Queries.Search.Ports; + +namespace JSMR.Application.Tags.Queries.Search; + +public sealed class SearchTagsHandler(ITagSearchProvider searchProvider, ICache cache) +{ + public async Task HandleAsync(SearchTagsRequest request, CancellationToken cancellationToken) + { + SearchOptions searchOptions = request.Options; + + string cacheKey = $"tag:{searchOptions.GetHashCode()}"; + + TagSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken); + + if (cachedResults != null) + return new SearchTagsResponse(cachedResults); + + SearchResult results = await searchProvider.SearchAsync(searchOptions, cancellationToken); + + CacheEntryOptions cacheEntryOptions = new() + { + SlidingExpiration = TimeSpan.FromMinutes(10) + }; + + await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken); + + return new SearchTagsResponse(results); + } +} \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/SearchTagsRequest.cs b/JSMR.Application/Tags/Queries/Search/SearchTagsRequest.cs new file mode 100644 index 0000000..cd3d1aa --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/SearchTagsRequest.cs @@ -0,0 +1,6 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Tags.Queries.Search.Contracts; + +namespace JSMR.Application.Tags.Queries.Search; + +public sealed record SearchTagsRequest(SearchOptions Options); \ No newline at end of file diff --git a/JSMR.Application/Tags/Queries/Search/SearchTagsResponse.cs b/JSMR.Application/Tags/Queries/Search/SearchTagsResponse.cs new file mode 100644 index 0000000..516ea55 --- /dev/null +++ b/JSMR.Application/Tags/Queries/Search/SearchTagsResponse.cs @@ -0,0 +1,6 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Tags.Queries.Search.Contracts; + +namespace JSMR.Application.Tags.Queries.Search; + +public sealed record SearchTagsResponse(SearchResult Results); \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Ports/IVoiceWorkReader.cs b/JSMR.Application/VoiceWorks/Ports/IVoiceWorkReader.cs new file mode 100644 index 0000000..2edf40d --- /dev/null +++ b/JSMR.Application/VoiceWorks/Ports/IVoiceWorkReader.cs @@ -0,0 +1,9 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.VoiceWorks.Search.Contracts; + +namespace JSMR.Application.VoiceWorks.Ports; + +public interface IVoiceWorkReader +{ + //Task SearchAsync(VoiceWorkSearchOptions options, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchCriteria.cs b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchCriteria.cs new file mode 100644 index 0000000..be1ceef --- /dev/null +++ b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchCriteria.cs @@ -0,0 +1,29 @@ +using JSMR.Application.Common; + +namespace JSMR.Application.VoiceWorks.Search.Contracts; + +public class VoiceWorkSearchCriteria +{ + public string? Keywords { get; init; } + public string? Title { get; init; } + public string? Circle { get; init; } + //public SaleStatus SaleStatus { get; init; } + //public CircleStatus CircleStatus { get; init; } + //public TagStatus TagStatus { get; init; } + //public CreatorStatus CreatorStatus { get; init; } + public int[] TagIds { get; init; } = []; + public bool IncludeAllTags { get; init; } + public int[] CreatorIds { get; init; } = []; + public bool IncludeAllCreators { get; init; } + //public VoiceWorkSort Sort { get; init; } + //public VoiceWorkLanguage Language { get; init; } + public DateTime? ReleaseDateStart { get; init; } + public DateTime? ReleaseDateEnd { get; init; } + //public List AgeRatings { get; init; } + public Language[] SupportedLanguages { get; init; } = []; + //public List AIGenerationOptions { get; init; } + public bool ShowFavoriteVoiceWorks { get; init; } + public bool ShowInvalidVoiceWorks { get; init; } + public int? MinDownloads { get; init; } + public int? MaxDownloads { get; init; } +} \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchOptions.cs b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchOptions.cs new file mode 100644 index 0000000..88486eb --- /dev/null +++ b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchOptions.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.VoiceWorks.Search.Contracts; + +//public record VoiceWorkSearchOptions : SearchOptions +//{ + +//} \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchResults.cs b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchResults.cs new file mode 100644 index 0000000..59dc2aa --- /dev/null +++ b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSearchResults.cs @@ -0,0 +1,8 @@ +using JSMR.Application.Common.Search; + +namespace JSMR.Application.VoiceWorks.Search.Contracts; + +public record VoiceWorkSearchResults : SearchResult +{ + +} \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSortField.cs b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSortField.cs new file mode 100644 index 0000000..07e6cf0 --- /dev/null +++ b/JSMR.Application/VoiceWorks/Search/Contracts/VoiceWorkSortField.cs @@ -0,0 +1,11 @@ +namespace JSMR.Application.VoiceWorks.Search.Contracts; + +public enum VoiceWorkSortField +{ + ReleaseDateNewToOld, + ReleaseDateOldToNew, + BestSelling, + MostWishedFor, + SalesToWishlistRatio, + StarRating +} \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksHandler.cs b/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksHandler.cs new file mode 100644 index 0000000..f65660f --- /dev/null +++ b/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksHandler.cs @@ -0,0 +1,31 @@ +using JSMR.Application.Common.Caching; +using JSMR.Application.VoiceWorks.Ports; +using JSMR.Application.VoiceWorks.Search.Contracts; + +namespace JSMR.Application.VoiceWorks.Search; + +//public sealed class SearchVoiceWorksHandler(IVoiceWorkReader reader, ICache cache) +//{ +// //public async Task HandleAsync(SearchVoiceWorksRequest request, CancellationToken cancellationToken) +// //{ +// // VoiceWorkSearchOptions searchOptions = request.Options; + +// // string cacheKey = $"vw:{searchOptions.GetHashCode()}"; + +// // VoiceWorkSearchResults? cachedResults = await cache.GetAsync(cacheKey, cancellationToken); + +// // if (cachedResults != null) +// // return new SearchVoiceWorksResponse(cachedResults); + +// // VoiceWorkSearchResults results = await reader.SearchAsync(searchOptions, cancellationToken); + +// // CacheEntryOptions cacheEntryOptions = new() +// // { +// // SlidingExpiration = TimeSpan.FromMinutes(10) +// // }; + +// // await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken); + +// // return new SearchVoiceWorksResponse(results); +// //} +//} \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksRequest.cs b/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksRequest.cs new file mode 100644 index 0000000..15699f1 --- /dev/null +++ b/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksRequest.cs @@ -0,0 +1,6 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.VoiceWorks.Search.Contracts; + +namespace JSMR.Application.VoiceWorks.Search; + +public sealed record SearchVoiceWorksRequest(SearchOptions Options); \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksResponse.cs b/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksResponse.cs new file mode 100644 index 0000000..f554d8e --- /dev/null +++ b/JSMR.Application/VoiceWorks/Search/SearchVoiceWorksResponse.cs @@ -0,0 +1,5 @@ +using JSMR.Application.VoiceWorks.Search.Contracts; + +namespace JSMR.Application.VoiceWorks.Search; + +public sealed record SearchVoiceWorksResponse(VoiceWorkSearchResults Results); \ No newline at end of file diff --git a/JSMR.Domain/Entities/Circle.cs b/JSMR.Domain/Entities/Circle.cs new file mode 100644 index 0000000..fd38b6a --- /dev/null +++ b/JSMR.Domain/Entities/Circle.cs @@ -0,0 +1,11 @@ +namespace JSMR.Domain.Entities; + +public class Circle +{ + public int CircleId { get; set; } + public required string MakerId { get; set; } + public required string Name { get; set; } + public bool Blacklisted { get; set; } + public bool Favorite { get; set; } + public bool Spam { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/Creator.cs b/JSMR.Domain/Entities/Creator.cs new file mode 100644 index 0000000..80348c4 --- /dev/null +++ b/JSMR.Domain/Entities/Creator.cs @@ -0,0 +1,9 @@ +namespace JSMR.Domain.Entities; + +public class Creator +{ + public int CreatorId { get; set; } + public required string Name { get; set; } + public bool Favorite { get; set; } + public bool Blacklisted { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/EnglishTag.cs b/JSMR.Domain/Entities/EnglishTag.cs new file mode 100644 index 0000000..1915e27 --- /dev/null +++ b/JSMR.Domain/Entities/EnglishTag.cs @@ -0,0 +1,11 @@ +namespace JSMR.Domain.Entities; + +public class EnglishTag +{ + public int EnglishTagId { get; set; } + + public int TagId { get; set; } + public Tag? Tag { get; set; } + + public required string Name { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/EnglishVoiceWork.cs b/JSMR.Domain/Entities/EnglishVoiceWork.cs new file mode 100644 index 0000000..2ffb0bf --- /dev/null +++ b/JSMR.Domain/Entities/EnglishVoiceWork.cs @@ -0,0 +1,12 @@ +namespace JSMR.Domain.Entities; + +public class EnglishVoiceWork +{ + public int EnglishVoiceWorkId { get; set; } + public required string ProductName { get; set; } + public required string Description { get; set; } + public bool? IsValid { get; set; } + + public int VoiceWorkId { get; set; } + public VoiceWork? VoiceWork { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/Series.cs b/JSMR.Domain/Entities/Series.cs new file mode 100644 index 0000000..da82bbd --- /dev/null +++ b/JSMR.Domain/Entities/Series.cs @@ -0,0 +1,12 @@ +namespace JSMR.Domain.Entities; + +public class Series +{ + public int SeriesId { get; set; } + + public int CircleId { get; set; } + public Circle? Circle { get; set; } + + public required string Name { get; set; } + public required string Identifier { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/Tag.cs b/JSMR.Domain/Entities/Tag.cs new file mode 100644 index 0000000..5bcb990 --- /dev/null +++ b/JSMR.Domain/Entities/Tag.cs @@ -0,0 +1,9 @@ +namespace JSMR.Domain.Entities; + +public class Tag +{ + public int TagId { get; set; } + public required string Name { get; set; } + public bool Favorite { get; set; } + public bool Blacklisted { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/VoiceWork.cs b/JSMR.Domain/Entities/VoiceWork.cs new file mode 100644 index 0000000..bedec35 --- /dev/null +++ b/JSMR.Domain/Entities/VoiceWork.cs @@ -0,0 +1,42 @@ +namespace JSMR.Domain.Entities; + +public class VoiceWork +{ + public int VoiceWorkId { get; set; } + public required string ProductId { get; set; } + public string? OriginalProductId { get; set; } + public required string ProductName { get; set; } + public string? Description { get; set; } + public int Rating { get; set; } + public bool HasImage { get; set; } + public bool HasTrial { get; set; } + public bool Purchased { get; set; } + public bool Favorite { get; set; } + public DateTime? ExpectedDate { get; set; } + public DateTime? SalesDate { get; set; } + public int? Downloads { get; set; } + public byte? StarRating { get; set; } + public int? Votes { get; set; } + public bool? IsValid { get; set; } + public DateTime? LastScannedDate { get; set; } + public byte Status { get; set; } + public byte SubtitleLanguage { get; set; } + public bool HasChobit { get; set; } + public bool IsPurchased { get; set; } + public int? WishlistCount { get; set; } + public DateTime? PlannedReleaseDate { get; set; } + public byte AIGeneration { get; set; } + + public int CircleId { get; set; } + public Circle? Circle { get; set; } + + public int? SeriesId { get; set; } + public Series? Series { get; set; } + + //public int? VoiceWorkSearchId { get; set; } + //public VoiceWorkSearch? VoiceWorkSearch { get; set; } + + public virtual ICollection VoiceWorkTags { get; set; } = []; + public virtual ICollection VoiceWorkCreators { get; set; } = []; + public virtual ICollection EnglishVoiceWorks { get; set; } = []; +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/VoiceWorkCreator.cs b/JSMR.Domain/Entities/VoiceWorkCreator.cs new file mode 100644 index 0000000..edfabdb --- /dev/null +++ b/JSMR.Domain/Entities/VoiceWorkCreator.cs @@ -0,0 +1,14 @@ +namespace JSMR.Domain.Entities; + +public class VoiceWorkCreator +{ + public int VoiceWorkCreatorId { get; set; } + public int? Position { get; set; } + public bool? IsValid { get; set; } + + public int CreatorId { get; set; } + public Creator? Creator { get; set; } + + public int VoiceWorkId { get; set; } + public VoiceWork? VoiceWork { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/VoiceWorkSearch.cs b/JSMR.Domain/Entities/VoiceWorkSearch.cs new file mode 100644 index 0000000..9ef87df --- /dev/null +++ b/JSMR.Domain/Entities/VoiceWorkSearch.cs @@ -0,0 +1,9 @@ +namespace JSMR.Domain.Entities; + +public class VoiceWorkSearch +{ + public int VoiceWorkId { get; set; } + public VoiceWork? VoiceWork { get; set; } + + public required string SearchText { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/Entities/VoiceWorkTag.cs b/JSMR.Domain/Entities/VoiceWorkTag.cs new file mode 100644 index 0000000..e99031a --- /dev/null +++ b/JSMR.Domain/Entities/VoiceWorkTag.cs @@ -0,0 +1,14 @@ +namespace JSMR.Domain.Entities; + +public class VoiceWorkTag +{ + public int VoiceWorkTagId { get; set; } + public int? Position { get; set; } + public bool? IsValid { get; set; } + + public int TagId { get; set; } + public Tag? Tag { get; set; } + + public int VoiceWorkId { get; set; } + public VoiceWork? VoiceWork { get; set; } +} \ No newline at end of file diff --git a/JSMR.Domain/JSMR.Domain.csproj b/JSMR.Domain/JSMR.Domain.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/JSMR.Domain/JSMR.Domain.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/JSMR.Infrastructure/Caching/DistributedCacheAdapter.cs b/JSMR.Infrastructure/Caching/DistributedCacheAdapter.cs new file mode 100644 index 0000000..94388da --- /dev/null +++ b/JSMR.Infrastructure/Caching/DistributedCacheAdapter.cs @@ -0,0 +1,40 @@ +using JSMR.Application.Common.Caching; +using Microsoft.Extensions.Caching.Distributed; +using System.Text; +using System.Text.Json; + +namespace JSMR.Infrastructure.Caching; + +public class DistributedCacheAdapter(IDistributedCache distributedCache) : ICache +{ + public async ValueTask GetAsync(string key, CancellationToken cancellationToken = default) + { + byte[]? bytes = await distributedCache.GetAsync(key, cancellationToken); + + if (bytes == null) + return default; + + string json = Encoding.UTF8.GetString(bytes); + + return JsonSerializer.Deserialize(json); + } + + public async ValueTask SetAsync(string key, T value, CacheEntryOptions options, CancellationToken cancellationToken = default) + { + string json = JsonSerializer.Serialize(value); + byte[] bytes = Encoding.UTF8.GetBytes(json); + + DistributedCacheEntryOptions distributedCacheOptions = new(); + + if (options.AbsoluteExpirationRelativeToNow != null) + distributedCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpirationRelativeToNow.Value); + + if (options.AbsoluteExpiration != null) + distributedCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpiration.Value); + + if (options.SlidingExpiration != null) + distributedCacheOptions.SetSlidingExpiration(options.SlidingExpiration.Value); + + await distributedCache.SetAsync(key, bytes, distributedCacheOptions, cancellationToken); + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Caching/MemoryCacheAdapter.cs b/JSMR.Infrastructure/Caching/MemoryCacheAdapter.cs new file mode 100644 index 0000000..7bd7bcf --- /dev/null +++ b/JSMR.Infrastructure/Caching/MemoryCacheAdapter.cs @@ -0,0 +1,32 @@ +using JSMR.Application.Common.Caching; +using Microsoft.Extensions.Caching.Memory; + +namespace JSMR.Infrastructure.Caching; + +public class MemoryCacheAdapter(IMemoryCache memoryCache) : ICache +{ + public ValueTask GetAsync(string key, CancellationToken cancellationToken = default) + { + memoryCache.TryGetValue(key, out T? value); + + return ValueTask.FromResult(value); + } + + public ValueTask SetAsync(string key, T value, CacheEntryOptions options, CancellationToken cancellationToken = default) + { + MemoryCacheEntryOptions memoryCacheOptions = new(); + + if (options.AbsoluteExpirationRelativeToNow != null) + memoryCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpirationRelativeToNow.Value); + + if (options.AbsoluteExpiration != null) + memoryCacheOptions.SetAbsoluteExpiration(options.AbsoluteExpiration.Value); + + if (options.SlidingExpiration != null) + memoryCacheOptions.SetSlidingExpiration(options.SlidingExpiration.Value); + + memoryCache.Set(key, value, memoryCacheOptions); + + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Common/Queries/SearchProvider.cs b/JSMR.Infrastructure/Common/Queries/SearchProvider.cs new file mode 100644 index 0000000..645e4ac --- /dev/null +++ b/JSMR.Infrastructure/Common/Queries/SearchProvider.cs @@ -0,0 +1,59 @@ +using JSMR.Application.Common.Search; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace JSMR.Infrastructure.Common.Queries; + +public abstract class SearchProvider : ISearchProvider + where TCriteria : new() + where TSortField : struct, Enum +{ + public async Task> SearchAsync(SearchOptions options, CancellationToken cancellationToken = default) + { + IQueryable baseQuery = GetBaseQuery(); + IQueryable filteredQuery = ApplyFilters(baseQuery, options.Criteria); + + int total = await filteredQuery.CountAsync(cancellationToken); + + IOrderedQueryable orderedQuery = ApplySorting(filteredQuery, options.SortOptions); + IOrderedQueryable selectQuery = GetSelectQuery(orderedQuery); + + TItem[] items = await selectQuery + .Skip((options.PageNumber - 1) * options.PageSize) + .Take(options.PageSize) + .ToArrayAsync(cancellationToken); + + return new SearchResult() + { + Items = items, + TotalItems = total + }; + } + + protected abstract IQueryable GetBaseQuery(); + protected abstract IQueryable ApplyFilters(IQueryable query, TCriteria criteria); + + private IOrderedQueryable ApplySorting(IQueryable query, SortOption[] sortOptions) + { + IOrderedQueryable? ordered = null; + + for (int i = 0; i < sortOptions.Length; i++) + { + var (field, direction) = (sortOptions[i].Field, sortOptions[i].Direction); + bool isDescending = direction == SortDirection.Descending; + + IOrderedQueryable applyFirst(Expression> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector); + IOrderedQueryable applyNext(Expression> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector); + + Expression> selector = GetSortExpression(field); + + ordered = (i == 0) ? applyFirst(selector) : applyNext(selector); + } + + return ordered ?? GetDefaultSortExpression(query); + } + + protected abstract Expression> GetSortExpression(TSortField field); + protected abstract IOrderedQueryable GetDefaultSortExpression(IQueryable query); + protected abstract IOrderedQueryable GetSelectQuery(IOrderedQueryable query); +} \ No newline at end of file diff --git a/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs new file mode 100644 index 0000000..679d23d --- /dev/null +++ b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using JSMR.Application.Circles.Queries.GetCreators; +using JSMR.Application.Circles.Queries.GetTags; +using JSMR.Application.Circles.Queries.Search; +using JSMR.Infrastructure.Data.Repositories.Circles; +using Microsoft.Extensions.DependencyInjection; + +namespace JSMR.Infrastructure.DI; + +public static class InfrastructureServiceCollectionExtensions +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/AppDbContext.cs b/JSMR.Infrastructure/Data/AppDbContext.cs new file mode 100644 index 0000000..717946e --- /dev/null +++ b/JSMR.Infrastructure/Data/AppDbContext.cs @@ -0,0 +1,18 @@ +using JSMR.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Infrastructure.Data; + +public class AppDbContext : DbContext +{ + public DbSet VoiceWorks { get; set; } + public DbSet EnglishVoiceWorks { get; set; } + public DbSet Circles { get; set; } + public DbSet Tags { get; set; } + public DbSet EnglishTags { get; set; } + public DbSet VoiceWorkTags { get; set; } + public DbSet Creators { get; set; } + public DbSet VoiceWorkCreators { get; set; } + public DbSet Series { get; set; } + public DbSet VoiceWorkSearches { get; set; } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Circles/CircleCreatorsProvider.cs b/JSMR.Infrastructure/Data/Repositories/Circles/CircleCreatorsProvider.cs new file mode 100644 index 0000000..bf31eb1 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Circles/CircleCreatorsProvider.cs @@ -0,0 +1,35 @@ +using JSMR.Application.Circles.Queries.GetCreators; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Infrastructure.Data.Repositories.Circles; + +public class CircleCreatorsProvider(AppDbContext context) : ICircleCreatorsProvider +{ + public async Task HandleAsync(GetCircleCreatorsRequest request, CancellationToken cancellationToken = default) + { + int? circleId = await CircleLookup.ResolveCircleIdAsync(context, request.CircleId, request.NameOrMakerId, cancellationToken); + + if (circleId is null) + return new GetCircleCreatorsResponse([]); + + IQueryable query = + from voiceWork in context.VoiceWorks.AsNoTracking() + where voiceWork.CircleId == circleId.Value + join voiceWorkCreator in context.VoiceWorkCreators.AsNoTracking() on voiceWork.VoiceWorkId equals voiceWorkCreator.VoiceWorkId + join creator in context.Creators.AsNoTracking() on voiceWorkCreator.CreatorId equals creator.CreatorId + group new { creator.Name, voiceWork.SalesDate, voiceWork.Downloads } by creator.Name into g + select new CircleCreatorItem( + g.Key, + g.Min(x => x.SalesDate), + g.Max(x => x.SalesDate), + g.Sum(x => x.Downloads ?? 0), + g.Count()); + + CircleCreatorItem[] items = await query + .OrderByDescending(x => x.Count) + .ThenByDescending(x => x.Downloads) + .ToArrayAsync(cancellationToken); + + return new GetCircleCreatorsResponse(items); + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Circles/CircleLookup.cs b/JSMR.Infrastructure/Data/Repositories/Circles/CircleLookup.cs new file mode 100644 index 0000000..e5bd277 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Circles/CircleLookup.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Infrastructure.Data.Repositories.Circles; + +internal static class CircleLookup +{ + public static async Task ResolveCircleIdAsync(AppDbContext context, int? circleId, string? nameOrMakerId, CancellationToken cancellationToken) + { + if (circleId.HasValue) + return circleId.Value; + + if (!string.IsNullOrWhiteSpace(nameOrMakerId)) + { + return await context.Circles + .Where(c => c.Name == nameOrMakerId || c.MakerId == nameOrMakerId) + .Select(c => (int?)c.CircleId) + .FirstOrDefaultAsync(cancellationToken); + } + + return null; + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs new file mode 100644 index 0000000..f731d41 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs @@ -0,0 +1,136 @@ +using JSMR.Application.Circles.Queries.Search; +using JSMR.Infrastructure.Common.Queries; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace JSMR.Infrastructure.Data.Repositories.Circles; + +public class CircleSearchProvider(AppDbContext context) : SearchProvider, ICircleSearchProvider +{ + protected override IQueryable GetBaseQuery() + { + // Precompute LatestProductId per circle (by productId length, then value) + var latestPerCircle = + from vw in context.VoiceWorks.AsNoTracking() + group vw by vw.CircleId into g + let latest = g + .OrderByDescending(x => x.ProductId.Length) + .ThenByDescending(x => x.ProductId) + .Select(x => x.ProductId) + .FirstOrDefault() + select new { CircleId = g.Key, LatestProductId = latest }; + + // Aggregates per circle + var aggregates = + from vw in context.VoiceWorks.AsNoTracking() + group vw by vw.CircleId into g + select new + { + CircleId = g.Key, + Downloads = g.Sum(x => x.Downloads ?? 0), + Releases = g.Count(x => x.SalesDate != null), + Pending = g.Count(x => x.ExpectedDate != null), + FirstReleaseDate = g.Min(x => x.SalesDate), + LatestReleaseDate = g.Max(x => x.SalesDate) + }; + + // Join circles with aggregates and latest product id + var baseQuery = + from c in context.Circles.AsNoTracking() + join agg in aggregates on c.CircleId equals agg.CircleId into aggs + from a in aggs.DefaultIfEmpty() + join lp in latestPerCircle on c.CircleId equals lp.CircleId into lps + from l in lps.DefaultIfEmpty() + select new CircleSearchItem + { + CircleId = c.CircleId, + Name = c.Name, + MakerId = c.MakerId, + Favorite = c.Favorite, + Blacklisted = c.Blacklisted, + Spam = c.Spam, + Downloads = a != null ? a.Downloads : 0, + Releases = a != null ? a.Releases : 0, + Pending = a != null ? a.Pending : 0, + FirstReleaseDate = a != null ? a.FirstReleaseDate : null, + LatestReleaseDate = a != null ? a.LatestReleaseDate : null, + LatestProductId = l != null ? l.LatestProductId : null, + // these two get filled in during Select stage (below) + LatestVoiceWorkHasImage = null, + LatestVoiceWorkSalesDate = null + }; + + return baseQuery; + } + + protected override IQueryable ApplyFilters(IQueryable query, CircleSearchCriteria criteria) + { + if (!string.IsNullOrWhiteSpace(criteria.Name)) + { + var term = $"%{criteria.Name.Trim()}%"; + + query = query.Where(x => + EF.Functions.Like(x.Name, term) || + EF.Functions.Like(x.MakerId, term)); + } + + //if (criteria.Status is CircleStatus.NotBlacklisted) + // query = query.Where(x => !x.Blacklisted); + //else if (criteria.Status is CircleStatus.Favorited) + // query = query.Where(x => x.Favorite); + //else if (criteria.Status is CircleStatus.Blacklisted) + // query = query.Where(x => x.Blacklisted); + //else if (criteria.Status is CircleStatus.Spam) + // query = query.Where(x => x.Spam); + + return query; + } + + protected override Expression> GetSortExpression(CircleSortField field) => field switch + { + //CircleSortField.MakerId => x => x.MakerId, + //CircleSortField.Downloads => x => x.Downloads, + //CircleSortField.Releases => x => x.Releases, + //CircleSortField.Pending => x => x.Pending, + //CircleSortField.FirstReleaseDate => x => x.FirstReleaseDate ?? DateTime.MinValue, + //CircleSortField.LatestReleaseDate => x => x.LatestReleaseDate ?? DateTime.MinValue, + CircleSortField.Favorite => x => x.Favorite, + CircleSortField.Blacklisted => x => x.Blacklisted, + CircleSortField.Spam => x => x.Spam, + _ => x => x.Name + }; + + protected override IOrderedQueryable GetDefaultSortExpression(IQueryable query) + => query.OrderBy(x => x.Name).ThenBy(x => x.CircleId); + + protected override IOrderedQueryable GetSelectQuery(IOrderedQueryable query) + { + // Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate + var selected = + from item in query + join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws + from latest in vws.DefaultIfEmpty() + select new CircleSearchItem + { + CircleId = item.CircleId, + Name = item.Name, + MakerId = item.MakerId, + Favorite = item.Favorite, + Blacklisted = item.Blacklisted, + Spam = item.Spam, + Downloads = item.Downloads, + Releases = item.Releases, + Pending = item.Pending, + FirstReleaseDate = item.FirstReleaseDate, + LatestReleaseDate = item.LatestReleaseDate, + LatestProductId = item.LatestProductId, + LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null, + LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null + }; + + // Preserve existing ordering; add stable tiebreaker + return selected.OrderBy(x => 0).ThenBy(x => x.Name).ThenBy(x => x.CircleId); + // NOTE: If your base class re-applies ordering after Select, you can just: + // return selected.OrderBy(x => x.Name).ThenBy(x => x.CircleId); + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Circles/CircleTagsProvider.cs b/JSMR.Infrastructure/Data/Repositories/Circles/CircleTagsProvider.cs new file mode 100644 index 0000000..a81c4c3 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Circles/CircleTagsProvider.cs @@ -0,0 +1,33 @@ +using JSMR.Application.Circles.Queries.GetTags; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Infrastructure.Data.Repositories.Circles; + +public class CircleTagsProvider(AppDbContext context) : ICircleTagsProvider +{ + public async Task HandleAsync(GetCircleTagsRequest request, CancellationToken cancellationToken = default) + { + int? circleId = await CircleLookup.ResolveCircleIdAsync(context, request.CircleId, request.NameOrMakerId, cancellationToken); + + if (circleId is null) + return new GetCircleTagsResponse([]); + + IQueryable query = + from voiceWork in context.VoiceWorks.AsNoTracking() + where voiceWork.CircleId == circleId.Value + join voiceWorkTag in context.VoiceWorkTags.AsNoTracking() on voiceWork.VoiceWorkId equals voiceWorkTag.VoiceWorkId + join tag in context.Tags.AsNoTracking() on voiceWorkTag.TagId equals tag.TagId + join englishTag in context.EnglishTags.AsNoTracking() on tag.TagId equals englishTag.TagId into eng + from e in eng.DefaultIfEmpty() + group new { tag, e } by new { tag.Name, English = e != null ? e.Name : null } into g + orderby g.Count() descending + select new CircleTagItem( + g.Key.Name, + g.Key.English, + g.Count()); + + CircleTagItem[] items = await query.ToArrayAsync(cancellationToken); + + return new GetCircleTagsResponse(items); + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Circles/CircleWriter.cs b/JSMR.Infrastructure/Data/Repositories/Circles/CircleWriter.cs new file mode 100644 index 0000000..e076462 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Circles/CircleWriter.cs @@ -0,0 +1,49 @@ +using JSMR.Application.Circles.Commands.UpdateCircleStatus; +using JSMR.Application.Circles.Contracts; +using JSMR.Application.Circles.Ports; +using JSMR.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Infrastructure.Data.Repositories.Circles; + +public class CircleWriter(AppDbContext context) : ICircleWriter +{ + public async Task UpdateStatusAsync(UpdateCircleStatusRequest request, CancellationToken cancellationToken = default) + { + Circle circle = await GetCircleAsync(request.CircleId, cancellationToken); + + switch (request.CircleStatus) + { + case CircleStatus.Neutral: + circle.Favorite = false; + circle.Blacklisted = false; + circle.Spam = false; + break; + case CircleStatus.Favorite: + circle.Favorite = true; + circle.Blacklisted = false; + circle.Spam = false; + break; + case CircleStatus.Blacklisted: + circle.Favorite = false; + circle.Blacklisted = true; + circle.Spam = false; + break; + case CircleStatus.Spam: + circle.Favorite = false; + circle.Blacklisted = false; + circle.Spam = true; + break; + } + + await context.SaveChangesAsync(cancellationToken); + + return new UpdateCircleStatusResponse(request.CircleId, request.CircleStatus); + } + + private async Task GetCircleAsync(int circleId, CancellationToken cancellationToken) + { + return await context.Circles.FirstOrDefaultAsync(circle => circle.CircleId == circleId, cancellationToken) + ?? throw new KeyNotFoundException($"Circle {circleId} not found."); + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs new file mode 100644 index 0000000..b46c0a9 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs @@ -0,0 +1,61 @@ +using JSMR.Application.Creators.Queries.Search.Contracts; +using JSMR.Application.Creators.Queries.Search.Ports; +using JSMR.Infrastructure.Common.Queries; +using System.Linq.Expressions; + +namespace JSMR.Infrastructure.Data.Repositories.Creators; + +public class CreatorSearchProvider(AppDbContext context) : SearchProvider, ICreatorSearchProvider +{ + protected override IQueryable GetBaseQuery() + { + return + from creator in context.Creators + join voiceWorkCreators in context.VoiceWorkCreators on creator.CreatorId equals voiceWorkCreators.CreatorId into vwcs + select new CreatorSearchItem + { + CreatorId = creator.CreatorId, + Name = creator.Name, + Favorite = creator.Favorite, + Blacklisted = creator.Blacklisted, + VoiceWorkCount = vwcs.Count() + }; + } + + protected override IQueryable ApplyFilters(IQueryable query, CreatorSearchCriteria criteria) + { + IQueryable filteredQuery = query; + + if (string.IsNullOrEmpty(criteria.Name) == false) + { + string name = criteria.Name.Trim(); + + filteredQuery = filteredQuery.Where(x => x.Name.Contains(name)); + } + + return filteredQuery; + } + + protected override Expression> GetSortExpression(CreatorSortField field) + { + Expression> selector = field switch + { + CreatorSortField.VoiceWorkCount => x => x.VoiceWorkCount, + CreatorSortField.Favorite => x => x.Favorite, + CreatorSortField.Blacklisted => x => x.Blacklisted, + _ => x => x.Name + }; + + return selector; + } + + protected override IOrderedQueryable GetDefaultSortExpression(IQueryable query) + { + return query.OrderBy(x => x.Name); + } + + protected override IOrderedQueryable GetSelectQuery(IOrderedQueryable query) + { + return query; + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Creators/CreatorWriter.cs b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorWriter.cs new file mode 100644 index 0000000..dffaf0a --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorWriter.cs @@ -0,0 +1,41 @@ +using JSMR.Application.Creators.Commands.UpdateCreatorStatus; +using JSMR.Application.Creators.Contracts; +using JSMR.Application.Creators.Ports; +using JSMR.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Infrastructure.Data.Repositories.Creators; + +public class CreatorWriter(AppDbContext context) : ICreatorWriter +{ + public async Task UpdateStatusAsync(UpdateCreatorStatusRequest request, CancellationToken cancellationToken = default) + { + Creator creator = await GetCreatorAsync(request.CreatorId, cancellationToken); + + switch (request.CreatorStatus) + { + case CreatorStatus.Neutral: + creator.Favorite = false; + creator.Blacklisted = false; + break; + case CreatorStatus.Favorite: + creator.Favorite = true; + creator.Blacklisted = false; + break; + case CreatorStatus.Blacklisted: + creator.Favorite = false; + creator.Blacklisted = true; + break; + } + + await context.SaveChangesAsync(cancellationToken); + + return new UpdateCreatorStatusResponse(request.CreatorId, request.CreatorStatus); + } + + private async Task GetCreatorAsync(int creatorId, CancellationToken cancellationToken) + { + return await context.Creators.FirstOrDefaultAsync(creator => creator.CreatorId == creatorId, cancellationToken) + ?? throw new KeyNotFoundException($"Creator {creatorId} not found."); + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Tags/TagReader.cs b/JSMR.Infrastructure/Data/Repositories/Tags/TagReader.cs new file mode 100644 index 0000000..7fdfa99 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Tags/TagReader.cs @@ -0,0 +1,94 @@ +using JSMR.Application.Tags.Queries.Search.Contracts; +using JSMR.Application.Tags.Queries.Search.Ports; +using JSMR.Infrastructure.Common.Queries; +using System.Linq.Expressions; + +namespace JSMR.Infrastructure.Data.Repositories.Tags; + + + +//public class TagReader(AppDbContext context) : ITagReader +//{ +// public async Task SearchAsync(TagSearchOptions options, CancellationToken cancellationToken = default) +// { +// IQueryable baseQuery = GetQuery(); +// IQueryable filteredQuery = ApplyFilters(baseQuery, options); + +// int total = await filteredQuery.CountAsync(cancellationToken); + +// IOrderedQueryable orderedQuery = ApplySorting(filteredQuery, options); + +// TagSearchItem[] items = await orderedQuery +// .Skip((options.PageNumber - 1) * options.PageSize) +// .Take(options.PageSize) +// .ToArrayAsync(cancellationToken); + +// return new TagSearchResults() +// { +// Items = items, +// TotalItems = total +// }; +// } + +// private IQueryable GetQuery() +// { +// return +// from tag in context.Tags +// join englishTag in context.EnglishTags on tag.TagId equals englishTag.TagId into test +// from subEnglishTag in test.DefaultIfEmpty() +// join voiceWorkTags in context.VoiceWorkTags on tag.TagId equals voiceWorkTags.TagId into vwts +// select new TagSearchItem +// { +// TagId = tag.TagId, +// Name = tag.Name, +// EnglishName = subEnglishTag.Name, +// Favorite = tag.Favorite, +// Blacklisted = tag.Blacklisted, +// VoiceWorkCount = context.VoiceWorkTags.Count(x => x.TagId == tag.TagId) +// }; +// } + +// private static IQueryable ApplyFilters(IQueryable query, TagSearchOptions options) +// { +// IQueryable filteredQuery = query; + +// if (string.IsNullOrEmpty(options.Criteria.Name) == false) +// { +// string name = options.Criteria.Name.Trim(); + +// filteredQuery = filteredQuery.Where(x => +// x.Name.Contains(name) || (x.EnglishName != null && x.EnglishName.Contains(name))); +// } + +// return filteredQuery; +// } + +// private static IOrderedQueryable ApplySorting(IQueryable query, TagSearchOptions options) +// { +// IOrderedQueryable? ordered = null; + +// for (int i = 0; i < options.SortOptions.Length; i++) +// { +// var (field, direction) = (options.SortOptions[i].Field, options.SortOptions[i].Direction); +// bool isDescending = direction == SortDirection.Descending; + +// IOrderedQueryable applyFirst(Expression> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector); +// IOrderedQueryable applyNext(Expression> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector); + +// Expression> selector = field switch +// { +// TagSortField.EnglishName => x => x.EnglishName ?? "", +// TagSortField.VoiceWorkCount => x => x.VoiceWorkCount, +// TagSortField.Favorite => x => x.Favorite, +// TagSortField.Blacklisted => x => x.Blacklisted, +// _ => x => x.Name +// }; + +// ordered = (i == 0) ? applyFirst(selector) : applyNext(selector); +// } + +// ordered ??= query.OrderBy(x => x.Name); + +// return ordered; +// } +//} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs new file mode 100644 index 0000000..d1be153 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs @@ -0,0 +1,66 @@ +using JSMR.Application.Tags.Queries.Search.Contracts; +using JSMR.Application.Tags.Queries.Search.Ports; +using JSMR.Infrastructure.Common.Queries; +using System.Linq.Expressions; + +namespace JSMR.Infrastructure.Data.Repositories.Tags; + +public class TagSearchProvider(AppDbContext context) : SearchProvider, ITagSearchProvider +{ + protected override IQueryable GetBaseQuery() + { + return + from tag in context.Tags + join englishTag in context.EnglishTags on tag.TagId equals englishTag.TagId into test + from subEnglishTag in test.DefaultIfEmpty() + join voiceWorkTags in context.VoiceWorkTags on tag.TagId equals voiceWorkTags.TagId into vwts + select new TagSearchItem + { + TagId = tag.TagId, + Name = tag.Name, + EnglishName = subEnglishTag.Name, + Favorite = tag.Favorite, + Blacklisted = tag.Blacklisted, + VoiceWorkCount = vwts.Count() + }; + } + + protected override IQueryable ApplyFilters(IQueryable query, TagSearchCriteria criteria) + { + IQueryable filteredQuery = query; + + if (string.IsNullOrEmpty(criteria.Name) == false) + { + string name = criteria.Name.Trim(); + + filteredQuery = filteredQuery.Where(x => + x.Name.Contains(name) || (x.EnglishName != null && x.EnglishName.Contains(name))); + } + + return filteredQuery; + } + + protected override Expression> GetSortExpression(TagSortField field) + { + Expression> selector = field switch + { + TagSortField.EnglishName => x => x.EnglishName ?? "", + TagSortField.VoiceWorkCount => x => x.VoiceWorkCount, + TagSortField.Favorite => x => x.Favorite, + TagSortField.Blacklisted => x => x.Blacklisted, + _ => x => x.Name + }; + + return selector; + } + + protected override IOrderedQueryable GetDefaultSortExpression(IQueryable query) + { + return query.OrderBy(x => x.Name); + } + + protected override IOrderedQueryable GetSelectQuery(IOrderedQueryable query) + { + return query; + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Tags/TagWriter.cs b/JSMR.Infrastructure/Data/Repositories/Tags/TagWriter.cs new file mode 100644 index 0000000..f9d73c9 --- /dev/null +++ b/JSMR.Infrastructure/Data/Repositories/Tags/TagWriter.cs @@ -0,0 +1,67 @@ +using JSMR.Application.Tags.Commands.SetEnglishName; +using JSMR.Application.Tags.Commands.UpdateTagStatus; +using JSMR.Application.Tags.Contracts; +using JSMR.Application.Tags.Ports; +using JSMR.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Infrastructure.Data.Repositories.Tags; + +public class TagWriter(AppDbContext context) : ITagWriter +{ + public async Task SetEnglishNameAsync(SetTagEnglishNameRequest request, CancellationToken cancellationToken = default) + { + Tag tag = await GetTagAsync(request.TagId, cancellationToken); + EnglishTag? englishTag = await context.EnglishTags.FirstOrDefaultAsync(e => e.TagId == request.TagId, cancellationToken); + + if (englishTag is null) + { + englishTag = new EnglishTag + { + TagId = request.TagId, + Name = request.EnglishName + }; + + context.EnglishTags.Add(englishTag); + } + else + { + englishTag.Name = request.EnglishName; + } + + await context.SaveChangesAsync(cancellationToken); + + return new SetTagEnglishNameResponse(request.TagId, request.EnglishName); + } + + public async Task UpdateStatusAsync(UpdateTagStatusRequest request, CancellationToken cancellationToken = default) + { + Tag tag = await GetTagAsync(request.TagId, cancellationToken); + + switch (request.TagStatus) + { + case TagStatus.Neutral: + tag.Favorite = false; + tag.Blacklisted = false; + break; + case TagStatus.Favorite: + tag.Favorite = true; + tag.Blacklisted = false; + break; + case TagStatus.Blacklisted: + tag.Favorite = false; + tag.Blacklisted = true; + break; + } + + await context.SaveChangesAsync(cancellationToken); + + return new UpdateTagStatusResponse(request.TagId, request.TagStatus); + } + + private async Task GetTagAsync(int tagId, CancellationToken cancellationToken) + { + return await context.Tags.FirstOrDefaultAsync(tag => tag.TagId == tagId, cancellationToken) + ?? throw new KeyNotFoundException($"Tag {tagId} not found."); + } +} \ No newline at end of file diff --git a/JSMR.Infrastructure/JSMR.Infrastructure.csproj b/JSMR.Infrastructure/JSMR.Infrastructure.csproj new file mode 100644 index 0000000..dccbd04 --- /dev/null +++ b/JSMR.Infrastructure/JSMR.Infrastructure.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + diff --git a/JSMR.sln b/JSMR.sln new file mode 100644 index 0000000..73690af --- /dev/null +++ b/JSMR.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36408.4 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.Domain", "JSMR.Domain\JSMR.Domain.csproj", "{BC16F228-63B0-4EE6-9B96-19A38A31C125}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.Application", "JSMR.Application\JSMR.Application.csproj", "{CE5EC2BA-09BB-42E8-B9BB-A8E881078CC8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.Infrastructure", "JSMR.Infrastructure\JSMR.Infrastructure.csproj", "{10099B7E-DB1D-4EED-B12C-70BEB0C1D996}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BC16F228-63B0-4EE6-9B96-19A38A31C125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC16F228-63B0-4EE6-9B96-19A38A31C125}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC16F228-63B0-4EE6-9B96-19A38A31C125}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC16F228-63B0-4EE6-9B96-19A38A31C125}.Release|Any CPU.Build.0 = Release|Any CPU + {CE5EC2BA-09BB-42E8-B9BB-A8E881078CC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE5EC2BA-09BB-42E8-B9BB-A8E881078CC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE5EC2BA-09BB-42E8-B9BB-A8E881078CC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE5EC2BA-09BB-42E8-B9BB-A8E881078CC8}.Release|Any CPU.Build.0 = Release|Any CPU + {10099B7E-DB1D-4EED-B12C-70BEB0C1D996}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10099B7E-DB1D-4EED-B12C-70BEB0C1D996}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10099B7E-DB1D-4EED-B12C-70BEB0C1D996}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10099B7E-DB1D-4EED-B12C-70BEB0C1D996}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B91031A9-0C79-4249-BDEA-6A38905BB6EB} + EndGlobalSection +EndGlobal