Add project files.

This commit is contained in:
2025-08-26 09:20:13 -04:00
parent 6c6a149821
commit d2201d6f9b
118 changed files with 1924 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
using JSMR.Application.Circles.Ports;
namespace JSMR.Application.Circles.Commands.UpdateCircleStatus;
public class UpdateCircleStatusHandler(ICircleWriter writer)
{
public Task<UpdateCircleStatusResponse> HandleAsync(UpdateCircleStatusRequest request, CancellationToken cancellationToken = default)
{
return writer.UpdateStatusAsync(request, cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Circles.Contracts;
namespace JSMR.Application.Circles.Commands.UpdateCircleStatus;
public sealed record UpdateCircleStatusRequest(int CircleId, CircleStatus CircleStatus);

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Circles.Contracts;
namespace JSMR.Application.Circles.Commands.UpdateCircleStatus;
public sealed record UpdateCircleStatusResponse(int CircleId, CircleStatus CircleStatus);

View File

@@ -0,0 +1,9 @@
namespace JSMR.Application.Circles.Contracts;
public enum CircleStatus
{
Neutral,
Favorite,
Blacklisted,
Spam
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Circles.Ports;
public interface ICircleReader
{
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Circles.Commands.UpdateCircleStatus;
namespace JSMR.Application.Circles.Ports;
public interface ICircleWriter
{
Task<UpdateCircleStatusResponse> UpdateStatusAsync(UpdateCircleStatusRequest request, CancellationToken cancellationToken = default);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
namespace JSMR.Application.Circles.Queries.GetCreators;
public sealed class GetCircleCreatorsHandler(ICircleCreatorsProvider provider)
{
public Task<GetCircleCreatorsResponse> HandleAsync(GetCircleCreatorsRequest request, CancellationToken cancellationToken = default)
{
return provider.HandleAsync(request, cancellationToken);
}
}

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Circles.Queries.GetCreators;
public sealed record GetCircleCreatorsRequest(int? CircleId, string? NameOrMakerId);

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Circles.Queries.GetCreators;
public sealed record GetCircleCreatorsResponse(IReadOnlyList<CircleCreatorItem> Items);

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Circles.Queries.GetCreators;
public interface ICircleCreatorsProvider
{
Task<GetCircleCreatorsResponse> HandleAsync(GetCircleCreatorsRequest request, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Circles.Queries.GetTags;
public sealed record CircleTagItem(string TagName, string? EnglishTagName, int Count);

View File

@@ -0,0 +1,9 @@
namespace JSMR.Application.Circles.Queries.GetTags;
public sealed class GetCircleTagsHandler(ICircleTagsProvider provider)
{
public Task<GetCircleTagsResponse> HandleAsync(GetCircleTagsRequest request, CancellationToken cancellationToken = default)
{
return provider.HandleAsync(request, cancellationToken);
}
}

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Circles.Queries.GetTags;
public sealed record GetCircleTagsRequest(int? CircleId, string? NameOrMakerId);

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Circles.Queries.GetTags;
public sealed record GetCircleTagsResponse(IReadOnlyList<CircleTagItem> Items);

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Circles.Queries.GetTags;
public interface ICircleTagsProvider
{
Task<GetCircleTagsResponse> HandleAsync(GetCircleTagsRequest request, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Circles.Queries.Search;
public class CircleSearchCriteria
{
public string? Name { get; init; }
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Circles.Queries.Search;
//public record TagSearchOptions : SearchOptions<TagSearchCriteria, TagSortField>
//{
//}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Circles.Queries.Search;
public record CircleSearchResults : SearchResult<CircleSearchItem>
{
}

View File

@@ -0,0 +1,10 @@
namespace JSMR.Application.Circles.Queries.Search;
public enum CircleSortField
{
Name,
Blacklisted,
Favorite,
Spam,
VoiceWorkCount
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Circles.Queries.Search;
public interface ICircleSearchProvider : ISearchProvider<CircleSearchItem, CircleSearchCriteria, CircleSortField>
{
}

View File

@@ -0,0 +1,15 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Circles.Queries.Search;
public sealed class SearchCirclesHandler(ICircleSearchProvider searchProvider)
{
public async Task<SearchCirclesResponse> HandleAsync(SearchCirclesRequest request, CancellationToken cancellationToken)
{
SearchOptions<CircleSearchCriteria, CircleSortField> searchOptions = request.Options;
SearchResult<CircleSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
return new SearchCirclesResponse(results);
}
}

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Circles.Queries.Search;
public sealed record SearchCirclesRequest(SearchOptions<CircleSearchCriteria, CircleSortField> Options);

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Circles.Queries.Search;
public sealed record SearchCirclesResponse(SearchResult<CircleSearchItem> Results);

View File

@@ -0,0 +1,69 @@
namespace JSMR.Application.Common.Caching;
public class CacheEntryOptions
{
private DateTimeOffset? _absoluteExpiration;
private TimeSpan? _absoluteExpirationRelativeToNow;
private TimeSpan? _slidingExpiration;
/// <summary>
/// Gets or sets an absolute expiration date for the cache entry.
/// </summary>
public DateTimeOffset? AbsoluteExpiration
{
get
{
return _absoluteExpiration;
}
set
{
_absoluteExpiration = value;
}
}
/// <summary>
/// Gets or sets an absolute expiration time, relative to now.
/// </summary>
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;
}
}
/// <summary>
/// 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).
/// </summary>
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;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace JSMR.Application.Common.Caching;
public interface ICache
{
ValueTask<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default);
ValueTask SetAsync<T>(string key, T value, CacheEntryOptions options, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,11 @@
namespace JSMR.Application.Common;
public enum Language
{
Unknown,
Japanese,
English,
ChineseSimplified,
ChineseTraditional,
Korean
}

View File

@@ -0,0 +1,8 @@
namespace JSMR.Application.Common.Search;
public interface ISearchProvider<TItem, TCriteria, TSortField>
where TCriteria : new()
where TSortField : struct, Enum
{
Task<SearchResult<TItem>> SearchAsync(SearchOptions<TCriteria, TSortField> options, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,8 @@
namespace JSMR.Application.Common.Search;
public interface ISearchQueryHandler<TItem, TCriteria, TSortField>
where TCriteria : new()
where TSortField : struct, Enum
{
Task<SearchResult<TItem>> HandleAsync(SearchOptions<TCriteria, TSortField> options, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,11 @@
namespace JSMR.Application.Common.Search;
public sealed record SearchOptions<TCriteria, TSortField>
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<TSortField>[] SortOptions { get; set; } = [];
}

View File

@@ -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<TItem, TCriteria, TSortField> : ISearchReader<TItem, TCriteria, TSortField>
// where TSortField : struct, Enum
//{
// public Task<SearchResult<TItem>> SearchAsync(SearchOptions<TCriteria, TSortField> options, CancellationToken cancellationToken = default)
// {
// throw new NotImplementedException();
// }
//}
//public abstract class SearchReader<TSearchResultItem, TCriteria, TSortField> where TSortField : struct, Enum
//{
// public async Task<SearchResult<TSearchResultItem>> SearchAsync(SearchOptions<TCriteria, TSortField> options, CancellationToken cancellationToken = default)
// {
// IQueryable<TSearchResultItem> baseQuery = GetBaseQuery();
// IQueryable<TSearchResultItem> filteredQuery = ApplyFilters(baseQuery, options);
// int total = await filteredQuery.CountAsync(cancellationToken);
// IOrderedQueryable<TSearchResultItem> orderedQuery = ApplySorting(filteredQuery, options);
// TSearchResultItem[] items = await orderedQuery
// .Skip((options.PageNumber - 1) * options.PageSize)
// .Take(options.PageSize)
// .ToArrayAsync(cancellationToken);
// return new SearchResult<TSearchResultItem>()
// {
// Items = items,
// TotalItems = total
// };
// }
// protected abstract IQueryable<TSearchResultItem> GetBaseQuery();
// protected abstract IQueryable<TSearchResultItem> ApplyFilters(IQueryable<TSearchResultItem> query, SearchOptions<TCriteria, TSortField> options);
// private IOrderedQueryable<TSearchResultItem> ApplySorting(IQueryable<TSearchResultItem> query, SearchOptions<TCriteria, TSortField> options)
// {
// IOrderedQueryable<TSearchResultItem>? 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<TSearchResultItem> applyFirst(Expression<Func<TSearchResultItem, object>> selector) => isDescending ? query.OrderByDescending(selector) : query.OrderBy(selector);
// IOrderedQueryable<TSearchResultItem> applyNext(Expression<Func<TSearchResultItem, object>> selector) => isDescending ? ordered!.ThenByDescending(selector) : ordered!.ThenBy(selector);
// Expression<Func<TSearchResultItem, object>> selector = GetSortExpression(field);
// ordered = (i == 0) ? applyFirst(selector) : applyNext(selector);
// }
// return ordered ?? GetDefaultSortExpression(query);
// }
// protected abstract Expression<Func<TSearchResultItem, object>> GetSortExpression(TSortField field);
// protected abstract IOrderedQueryable<TSearchResultItem> GetDefaultSortExpression(IQueryable<TSearchResultItem> query);
//}

View File

@@ -0,0 +1,7 @@
namespace JSMR.Application.Common.Search;
public record SearchResult<T>
{
public T[] Items { get; set; } = [];
public int TotalItems { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace JSMR.Application.Common.Search;
public enum SortDirection
{
Ascending = 0,
Descending = 1
}

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Common.Search;
public record SortOption<TSortField>(TSortField Field, SortDirection Direction) where TSortField : struct, Enum;

View File

@@ -0,0 +1,11 @@
using JSMR.Application.Creators.Ports;
namespace JSMR.Application.Creators.Commands.UpdateCreatorStatus;
public class UpdateCreatorStatusHandler(ICreatorWriter writer)
{
public Task<UpdateCreatorStatusResponse> HandleAsync(UpdateCreatorStatusRequest request, CancellationToken cancellationToken = default)
{
return writer.UpdateStatusAsync(request, cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Creators.Contracts;
namespace JSMR.Application.Creators.Commands.UpdateCreatorStatus;
public sealed record UpdateCreatorStatusRequest(int CreatorId, CreatorStatus CreatorStatus);

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Creators.Contracts;
namespace JSMR.Application.Creators.Commands.UpdateCreatorStatus;
public sealed record UpdateCreatorStatusResponse(int CreatorId, CreatorStatus CreatorStatus);

View File

@@ -0,0 +1,8 @@
namespace JSMR.Application.Creators.Contracts;
public enum CreatorStatus
{
Neutral,
Favorite,
Blacklisted
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Creators.Ports;
public interface ICircleReader
{
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Creators.Commands.UpdateCreatorStatus;
namespace JSMR.Application.Creators.Ports;
public interface ICreatorWriter
{
Task<UpdateCreatorStatusResponse> UpdateStatusAsync(UpdateCreatorStatusRequest request, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Creators.Queries.Search.Contracts;
public class CreatorSearchCriteria
{
public string? Name { get; init; }
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Creators.Queries.Search.Contracts;
//public record TagSearchOptions : SearchOptions<TagSearchCriteria, TagSortField>
//{
//}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Creators.Queries.Search.Contracts;
public record CreatorSearchResults : SearchResult<CreatorSearchItem>
{
}

View File

@@ -0,0 +1,9 @@
namespace JSMR.Application.Creators.Queries.Search.Contracts;
public enum CreatorSortField
{
Name,
Blacklisted,
Favorite,
VoiceWorkCount
}

View File

@@ -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<CreatorSearchItem, CreatorSearchCriteria, CreatorSortField>
{
}

View File

@@ -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<SearchCreatorsResponse> HandleAsync(SearchCreatorsRequest request, CancellationToken cancellationToken)
{
SearchOptions<CreatorSearchCriteria, CreatorSortField> searchOptions = request.Options;
string cacheKey = $"creator:{searchOptions.GetHashCode()}";
CreatorSearchResults? cachedResults = await cache.GetAsync<CreatorSearchResults>(cacheKey, cancellationToken);
if (cachedResults != null)
return new SearchCreatorsResponse(cachedResults);
SearchResult<CreatorSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
CacheEntryOptions cacheEntryOptions = new()
{
SlidingExpiration = TimeSpan.FromMinutes(10)
};
await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
return new SearchCreatorsResponse(results);
}
}

View File

@@ -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<CreatorSearchCriteria, CreatorSortField> Options);

View File

@@ -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<CreatorSearchItem> Results);

View File

@@ -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<SearchVoiceWorksHandler>();
//services.AddScoped<ScanVoiceWorksHandler>();
services.AddScoped<SetTagEnglishNameHandler>();
services.AddScoped<UpdateTagStatusHandler>();
return services;
}
}

View File

@@ -0,0 +1,7 @@
namespace JSMR.Application.Integrations.Chobit.Models;
public class ChobitResult
{
public int Count { get; set; }
public ChobitWork[] Works { get; set; } = [];
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Integrations.Chobit.Models;
public class ChobitWorkResult : Dictionary<string, ChobitResult> { }

View File

@@ -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; }
}

View File

@@ -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; } = [];
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Integrations.DLSite.Models;
public class VoiceWorkDetailCollection : Dictionary<string, VoiceWorkDetails>
{
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Integrations.Chobit.Models;
namespace JSMR.Application.Integrations.Ports;
public interface IChobitClient
{
Task<ChobitWorkResult> GetSampleInfoAsync(string[] productIds, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Integrations.Cien.Models;
namespace JSMR.Application.Integrations.Ports;
public interface ICienClient
{
Task<CienCreator[]> GetCienCreatorAsync(string makerId, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Integrations.DLSite.Models;
namespace JSMR.Application.Integrations.Ports;
public interface IDLSiteClient
{
Task<VoiceWorkDetailCollection> GetVoiceWorkDetailsAsync(string[] productIds, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
</ItemGroup>
<ItemGroup>
<Folder Include="Tags\Queries\Ports\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
using JSMR.Application.Tags.Ports;
namespace JSMR.Application.Tags.Commands.SetEnglishName;
public sealed class SetTagEnglishNameHandler(ITagWriter tagWriter)
{
public async Task<SetTagEnglishNameResponse> 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;
}
}

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Tags.Commands.SetEnglishName;
public sealed record SetTagEnglishNameRequest(int TagId, string EnglishName);

View File

@@ -0,0 +1,3 @@
namespace JSMR.Application.Tags.Commands.SetEnglishName;
public sealed record SetTagEnglishNameResponse(int TagId, string EnglishName);

View File

@@ -0,0 +1,11 @@
using JSMR.Application.Tags.Ports;
namespace JSMR.Application.Tags.Commands.UpdateTagStatus;
public sealed class UpdateTagStatusHandler(ITagWriter writer)
{
public Task<UpdateTagStatusResponse> HandleAsync(UpdateTagStatusRequest request, CancellationToken cancellationToken = default)
{
return writer.UpdateStatusAsync(request, cancellationToken);
}
}

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Tags.Contracts;
namespace JSMR.Application.Tags.Commands.UpdateTagStatus;
public sealed record UpdateTagStatusRequest(int TagId, TagStatus TagStatus);

View File

@@ -0,0 +1,5 @@
using JSMR.Application.Tags.Contracts;
namespace JSMR.Application.Tags.Commands.UpdateTagStatus;
public sealed record UpdateTagStatusResponse(int TagId, TagStatus TagStatus);

View File

@@ -0,0 +1,8 @@
namespace JSMR.Application.Tags.Contracts;
public enum TagStatus
{
Neutral,
Favorite,
Blacklisted
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Tags.Queries.Search.Contracts;
namespace JSMR.Application.Tags.Ports;
public interface ITagReader
{
//Task<TagSearchResults> SearchAsync(TagSearchOptions options, CancellationToken cancellationToken = default);
}

View File

@@ -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<SetTagEnglishNameResponse> SetEnglishNameAsync(SetTagEnglishNameRequest request, CancellationToken cancellationToken = default);
Task<UpdateTagStatusResponse> UpdateStatusAsync(UpdateTagStatusRequest request, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,6 @@
namespace JSMR.Application.Tags.Queries.Search.Contracts;
public class TagSearchCriteria
{
public string? Name { get; init; }
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Tags.Queries.Search.Contracts;
//public record TagSearchOptions : SearchOptions<TagSearchCriteria, TagSortField>
//{
//}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.Tags.Queries.Search.Contracts;
public record TagSearchResults : SearchResult<TagSearchItem>
{
}

View File

@@ -0,0 +1,10 @@
namespace JSMR.Application.Tags.Queries.Search.Contracts;
public enum TagSortField
{
Name,
EnglishName,
Blacklisted,
Favorite,
VoiceWorkCount
}

View File

@@ -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<TagSearchItem, TagSearchCriteria, TagSortField>
{
}

View File

@@ -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<SearchTagsResponse> HandleAsync(SearchTagsRequest request, CancellationToken cancellationToken)
{
SearchOptions<TagSearchCriteria, TagSortField> searchOptions = request.Options;
string cacheKey = $"tag:{searchOptions.GetHashCode()}";
TagSearchResults? cachedResults = await cache.GetAsync<TagSearchResults>(cacheKey, cancellationToken);
if (cachedResults != null)
return new SearchTagsResponse(cachedResults);
SearchResult<TagSearchItem> results = await searchProvider.SearchAsync(searchOptions, cancellationToken);
CacheEntryOptions cacheEntryOptions = new()
{
SlidingExpiration = TimeSpan.FromMinutes(10)
};
await cache.SetAsync(cacheKey, results, cacheEntryOptions, cancellationToken);
return new SearchTagsResponse(results);
}
}

View File

@@ -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<TagSearchCriteria, TagSortField> Options);

View File

@@ -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<TagSearchItem> Results);

View File

@@ -0,0 +1,9 @@
using JSMR.Application.Common.Search;
using JSMR.Application.VoiceWorks.Search.Contracts;
namespace JSMR.Application.VoiceWorks.Ports;
public interface IVoiceWorkReader
{
//Task<VoiceWorkSearchResults> SearchAsync(VoiceWorkSearchOptions options, CancellationToken cancellationToken = default);
}

View File

@@ -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<AgeRating> AgeRatings { get; init; }
public Language[] SupportedLanguages { get; init; } = [];
//public List<AIGeneration> AIGenerationOptions { get; init; }
public bool ShowFavoriteVoiceWorks { get; init; }
public bool ShowInvalidVoiceWorks { get; init; }
public int? MinDownloads { get; init; }
public int? MaxDownloads { get; init; }
}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.VoiceWorks.Search.Contracts;
//public record VoiceWorkSearchOptions : SearchOptions<VoiceWorkSearchCriteria, VoiceWorkSortField>
//{
//}

View File

@@ -0,0 +1,8 @@
using JSMR.Application.Common.Search;
namespace JSMR.Application.VoiceWorks.Search.Contracts;
public record VoiceWorkSearchResults : SearchResult<object>
{
}

View File

@@ -0,0 +1,11 @@
namespace JSMR.Application.VoiceWorks.Search.Contracts;
public enum VoiceWorkSortField
{
ReleaseDateNewToOld,
ReleaseDateOldToNew,
BestSelling,
MostWishedFor,
SalesToWishlistRatio,
StarRating
}

View File

@@ -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<SearchVoiceWorksResponse> HandleAsync(SearchVoiceWorksRequest request, CancellationToken cancellationToken)
// //{
// // VoiceWorkSearchOptions searchOptions = request.Options;
// // string cacheKey = $"vw:{searchOptions.GetHashCode()}";
// // VoiceWorkSearchResults? cachedResults = await cache.GetAsync<VoiceWorkSearchResults>(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);
// //}
//}

View File

@@ -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<VoiceWorkSearchCriteria, VoiceWorkSortField> Options);

View File

@@ -0,0 +1,5 @@
using JSMR.Application.VoiceWorks.Search.Contracts;
namespace JSMR.Application.VoiceWorks.Search;
public sealed record SearchVoiceWorksResponse(VoiceWorkSearchResults Results);

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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<VoiceWorkTag> VoiceWorkTags { get; set; } = [];
public virtual ICollection<VoiceWorkCreator> VoiceWorkCreators { get; set; } = [];
public virtual ICollection<EnglishVoiceWork> EnglishVoiceWorks { get; set; } = [];
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

Some files were not shown because too many files have changed in this diff Show More