diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/Common.Domain/Abstractions/Entity.cs b/Common.Domain/Abstractions/Entity.cs new file mode 100644 index 0000000..aa12cd2 --- /dev/null +++ b/Common.Domain/Abstractions/Entity.cs @@ -0,0 +1,57 @@ +using MediatR; +namespace Common.Domain.Abstractions; + +public abstract class Entity +{ + private int? _requestedHashCode; + + public virtual Guid Id { get; protected set; } + + private List _domainEvents = []; + public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); + + public void AddDomainEvent(INotification eventItem) => _domainEvents.Add(eventItem); + public void RemoveDomainEvent(INotification eventItem) => _domainEvents.Remove(eventItem); + public void ClearDomainEvents() => _domainEvents.Clear(); + + public bool IsTransient() => Id == default; + + public override bool Equals(object? obj) + { + if (obj == null || !(obj is Entity)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (GetType() != obj.GetType()) + return false; + + Entity item = (Entity)obj; + + if (item.IsTransient() || IsTransient()) + return false; + else + return item.Id == Id; + } + + public override int GetHashCode() + { + if (!IsTransient()) + { + if (!_requestedHashCode.HasValue) + _requestedHashCode = Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) + return _requestedHashCode.Value; + } + else + return base.GetHashCode(); + } + + public static bool operator ==(Entity left, Entity right) => + Equals(left, null) ? Equals(right, null) : left.Equals(right); + + public static bool operator !=(Entity left, Entity right) + { + return !(left == right); + } +} diff --git a/Common.Domain/Abstractions/IAggregateRoot.cs b/Common.Domain/Abstractions/IAggregateRoot.cs new file mode 100644 index 0000000..962d29f --- /dev/null +++ b/Common.Domain/Abstractions/IAggregateRoot.cs @@ -0,0 +1,3 @@ +namespace Common.Domain.Abstractions; + +public interface IAggregateRoot { } diff --git a/Common.Domain/Class1.cs b/Common.Domain/Class1.cs new file mode 100644 index 0000000..8d97887 --- /dev/null +++ b/Common.Domain/Class1.cs @@ -0,0 +1,7 @@ +namespace Common.Domain +{ + public class Class1 + { + + } +} diff --git a/Common.Domain/Common.Domain.csproj b/Common.Domain/Common.Domain.csproj new file mode 100644 index 0000000..0b88cc6 --- /dev/null +++ b/Common.Domain/Common.Domain.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Modules.Library.Application/Gateways/IAnimeUserGateway.cs b/Modules.Library.Application/Gateways/IAnimeUserGateway.cs new file mode 100644 index 0000000..284cdb4 --- /dev/null +++ b/Modules.Library.Application/Gateways/IAnimeUserGateway.cs @@ -0,0 +1,8 @@ +using Modules.Library.Application.Interfaces; + +namespace Modules.Library.Application.Gateways; + +public interface IAnimeUserGateway +{ + public IAnimeUser GetUserById(Guid Id); +} \ No newline at end of file diff --git a/Modules.Library.Application/Gateways/IRepository.cs b/Modules.Library.Application/Gateways/IRepository.cs new file mode 100644 index 0000000..fed5342 --- /dev/null +++ b/Modules.Library.Application/Gateways/IRepository.cs @@ -0,0 +1,36 @@ +using Modules.Library.Domain.Entities; +using System.Linq.Expressions; + +namespace Modules.Library.Application.Gateways; + +public interface IRepository +{ + Task> GetAllAsync(); + + Task GetByIdAsync(TKey id); + Task GetByIdOrDefaultAsync(TKey id); + + Task> GetRangeByIdsAsync(List ids); + + Task GetFirstWhere(Expression> predicate); + Task GetFirstOrDefaultWhere(Expression> predicate); + + Task> GetWhere(Expression> predicate); + + Task AnyWhere(Expression> predicate); + /* + Task AddAsync(T entity, IUser user); + + Task UpdateAsync(T entity, IUser user); + + Task DeleteAsync(T entity, IUser user); + */ + Task AddAsync(T entity); + + Task UpdateAsync(T entity); + + Task DeleteAsync(T entity); +} + +public interface IRepository : IRepository where T : Entity { } +//public interface IRepository : IRepository { } \ No newline at end of file diff --git a/Modules.Library.Application/Gateways/IUserGateway.cs b/Modules.Library.Application/Gateways/IUserGateway.cs new file mode 100644 index 0000000..c6d2261 --- /dev/null +++ b/Modules.Library.Application/Gateways/IUserGateway.cs @@ -0,0 +1,8 @@ +using Modules.Library.Application.Interfaces; + +namespace Modules.Library.Application.Gateways; + +public interface IUserGateway +{ + public IUser GetUserById(Guid Id); +} \ No newline at end of file diff --git a/Modules.Library.Application/Interfaces/IAnimeUser.cs b/Modules.Library.Application/Interfaces/IAnimeUser.cs new file mode 100644 index 0000000..6fe201e --- /dev/null +++ b/Modules.Library.Application/Interfaces/IAnimeUser.cs @@ -0,0 +1,16 @@ +namespace Modules.Library.Application.Interfaces; + +public interface IAnimeUser : IUser +{ + public bool CanCreateTitle(); + public bool CanCreateSeason(); + public bool CanCreateEpisode(); + + public bool CanEditTitle(); + public bool CanEditSeason(); + public bool CanEditEpisode(); + + public bool CanRemoveTitle(); + public bool CanRemoveSeason(); + public bool CanRemoveEpisode(); +} \ No newline at end of file diff --git a/Modules.Library.Application/Interfaces/IUser.cs b/Modules.Library.Application/Interfaces/IUser.cs new file mode 100644 index 0000000..a721372 --- /dev/null +++ b/Modules.Library.Application/Interfaces/IUser.cs @@ -0,0 +1,13 @@ +namespace Modules.Library.Application.Interfaces; + +public interface IUser +{ + public Guid Id { get; set; } + public bool CanAddMediaContent(); + public bool CanEditMediaContent(); + public bool CanRemoveMediaContent(); + + public bool CanAddMediaInfo(); + public bool CanEditMediaInfo(); + public bool CanRemoveMediaInfo(); +} \ No newline at end of file diff --git a/Modules.Library.Application/Modules.Library.Application.csproj b/Modules.Library.Application/Modules.Library.Application.csproj new file mode 100644 index 0000000..9e75c39 --- /dev/null +++ b/Modules.Library.Application/Modules.Library.Application.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/Modules.Library.Application/ServiceCollectionExtensions.cs b/Modules.Library.Application/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..5353075 --- /dev/null +++ b/Modules.Library.Application/ServiceCollectionExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Modules.Library.Application.Services.MediaContent; + +namespace Modules.Library.Application; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + return services; + } +} \ No newline at end of file diff --git a/Modules.Library.Application/Services/GenreService.cs b/Modules.Library.Application/Services/GenreService.cs new file mode 100644 index 0000000..af3a9dc --- /dev/null +++ b/Modules.Library.Application/Services/GenreService.cs @@ -0,0 +1,15 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Domain.Interactors; + +namespace Modules.Library.Application.Services; + +public class GenreService(IGenreGateway genreGateway) +{ + private readonly GenreInteractor _genreInteractor = new(genreGateway); + //public async Task AddGenre(string name) + public async Task Add(string name) => await _genreInteractor.Create(name); + + public async Task Edit(Guid genreId, string name) => await _genreInteractor.Edit(genreId, name); + + public async Task Remove(Guid genreId) => await _genreInteractor.Delete(genreId); +} \ No newline at end of file diff --git a/Modules.Library.Application/Services/LanguageService.cs b/Modules.Library.Application/Services/LanguageService.cs new file mode 100644 index 0000000..d7b77ad --- /dev/null +++ b/Modules.Library.Application/Services/LanguageService.cs @@ -0,0 +1,21 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Domain.Interactors; + +namespace Modules.Library.Application.Services; + +//public class LanguageService(ILanguageGateway languageGateway, IUserGateway userGateway) +public class LanguageService(ILanguageGateway languageGateway) +{ + private readonly LanguageInteractor _languageInteractor = new(languageGateway); + //public async Task AddGenre(string name) + //public async Task Add(IUser user, string codeIso2, string name, Guid? iconId) + public async Task Add(string codeIso2, string name, Guid? iconId) => + await _languageInteractor.Create(codeIso2, name, iconId); + + //public async Task Edit(IUser user, Guid id, string name, Guid? iconId) + public async Task Edit(Guid id, string name, Guid? iconId) => + await _languageInteractor.Edit(id, name, iconId); + + //public async Task Remove(IUser user, Guid id) + public async Task Remove(Guid id) => await _languageInteractor.Delete(id); +} \ No newline at end of file diff --git a/Modules.Library.Application/Services/MediaContent/AnimeEpisodeService.cs b/Modules.Library.Application/Services/MediaContent/AnimeEpisodeService.cs new file mode 100644 index 0000000..70a65ac --- /dev/null +++ b/Modules.Library.Application/Services/MediaContent/AnimeEpisodeService.cs @@ -0,0 +1,18 @@ +using Modules.Library.Application.Gateways; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +namespace Modules.Library.Application.Services.MediaContent; + +public class AnimeEpisodeService(IAnimeTitleGateway titleRepository) +{ +// public async Task Edit(string titleId, string? seasonId, string episodeId, int? number, TimeSpan? duration) +// { +// var title = await titleRepository.GetByIdAsync(episodeId); +// var episode = (string.IsNullOrWhiteSpace(seasonId) ? title.Items.OfType() : +// title.Items.OfType().FirstOrDefault(q => q.Id == seasonId)?.Episodes)?.FirstOrDefault(q => q.Id == episodeId); +// //if (episode == null) throw new EpisodeNotFoundException; +// if (episode == null) throw new Exception("EpisodeNotFound"); +// episode.SetNumber(number); +// if (duration.HasValue) episode.SetDuration(duration.Value); +// } +} \ No newline at end of file diff --git a/Modules.Library.Application/Services/MediaContent/AnimeSeasonService.cs b/Modules.Library.Application/Services/MediaContent/AnimeSeasonService.cs new file mode 100644 index 0000000..ac04541 --- /dev/null +++ b/Modules.Library.Application/Services/MediaContent/AnimeSeasonService.cs @@ -0,0 +1,64 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +namespace Modules.Library.Application.Services.MediaContent; + +public class AnimeSeasonService(IAnimeTitleGateway titleGateway) +{ +// public async Task AddEpisode(Guid titleId, Guid seasonId, AnimeEpisodeType episodeType) +// { +// await StructureAction(titleId, seasonId, (title, season) => +// { +// var lastOrder = season.Items.OrderByDescending(q => q.Order).FirstOrDefault()?.Order; +// var episode = AnimeEpisode.New(episodeType); +// season.AddItem(episode); +// episode.SetOrder(lastOrder.HasValue ? lastOrder.Value + 1 : 0); +// episode.SetNumber(season.Items.OfType().Select(q => q.Number).DefaultIfEmpty(0).Max() + 1); +// }); +// } + +// public async Task AddEpisodeAsVariant(Guid titleId, Guid seasonId, AnimeEpisodeType episodeType, uint order) +// { +// await StructureAction(titleId, seasonId, (title, season) => +// { +// var lastVariant = season.Items.OrderByDescending(q => q.Variant).FirstOrDefault(q => q.Order == order)?.Variant ?? +// throw new Exception($"Could not find episode with order [{order}]"); +// var episode = AnimeEpisode.New(episodeType); +// season.AddItem(episode); +// episode.SetOrder(order); +// episode.SetVariant(lastVariant + 1); +// episode.SetNumber(season.Items.Where(q => q.Order == order).OfType().Select(q => q.Number).FirstOrDefault() ?? 0); +// }); +// } + +// public async Task RemoveEpisode(Guid titleId, Guid seasonId, Guid episodeId) +// { +// await StructureAction(titleId, seasonId, (title, season) => +// { +// var episode = season.Items.OrderByDescending(q => q.Variant).FirstOrDefault(q => q.Id == episodeId) ?? +// throw new Exception($"Could not find episode with Id [{episodeId}]"); +// //season.RemoveItem(episode); +// episode.SetDeleted(); +// }); +// } + +// private async Task StructureAction(Guid titleId, Guid seasonId, Action action) +// { +// var title = await titleGateway.GetById(titleId); +// var season = await seasonRepository.GetById(seasonId); +// action.Invoke(title, season); +// season.CheckIfCompleted(); +// title.CheckIfCompleted(); +// } + + + +// public async Task Edit(Guid seasonId, int? number, string? director, string? originCountry, TimeSpan? expirationTime) +// { +// var season = await seasonRepository.GetById(seasonId); +// season.SetNumber(number); +// season.SetDirector(director); +// season.SetOriginCountry(originCountry); +// season.SetExpirationTime(expirationTime ?? TimeSpan.Zero); +// } +} \ No newline at end of file diff --git a/Modules.Library.Application/Services/MediaContent/AnimeTitleService.cs b/Modules.Library.Application/Services/MediaContent/AnimeTitleService.cs new file mode 100644 index 0000000..2716962 --- /dev/null +++ b/Modules.Library.Application/Services/MediaContent/AnimeTitleService.cs @@ -0,0 +1,48 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.EntityBuilders; +using Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +namespace Modules.Library.Application.Services.MediaContent; + +public class AnimeTitleService(IAnimeTitleGateway titleGateway, ILanguageGateway languageGateway, IGenreGateway genreGateway) +{ + private readonly CreateInteractor createInteractor = new (titleGateway, languageGateway, genreGateway); + public async Task New(string nameOriginal, Guid nameOriginalLanguageId) => + await createInteractor.Create(nameOriginal, nameOriginalLanguageId); + + public async Task AddSeason(Guid titleId, string nameOriginal, Guid nameOriginalLanguageId) => + await createInteractor.CreateAnimeSeason(titleId, nameOriginal, nameOriginalLanguageId); + + public async Task AddEpisode(Guid titleId, Guid? seasonId, AnimeEpisodeType episodeType, string nameOriginal, Guid nameOriginalLanguageId) + { + if (seasonId.HasValue) + return await createInteractor.CreateAnimeEpisode(titleId, seasonId.Value, episodeType, nameOriginal, nameOriginalLanguageId); + else + return await createInteractor.CreateAnimeEpisode(titleId, episodeType, nameOriginal, nameOriginalLanguageId); + } + /* + public async Task AddEpisodeAsVariant(string titleId, string seasonId, AnimeEpisodeType episodeType, ushort order) + { + var title = await titleGateway.GetByIdAsync(titleId); + var season = title.Items.OfType().FirstOrDefault(q => q.Id == seasonId) ?? + throw new Exception("NotFound"); + season.AddEpisodeAsVariant(episodeType, order); + } + */ + + //public async Task RemoveItem(string titleId, string itemId) + //{ + // var title = await titleGateway.GetByIdAsync(titleId); + // var item = title.Items.FirstOrDefault(q => q.Id == itemId) ?? + // throw new Exception("NotFound"); + // if (item is AnimeSeason season) title.RemoveSeason(season); + // else if (item is AnimeEpisode episode) title.RemoveEpisode(episode); + //} + + //public async Task Edit(string seasonId, TimeSpan? expirationTime) + //{ + // var title = await titleGateway.GetByIdAsync(seasonId); + // title.SetExpirationTime(expirationTime ?? TimeSpan.Zero); + //} +} \ No newline at end of file diff --git a/Modules.Library.Application/Services/MediaContent/CommonProperties/CommonPropertiesService.cs b/Modules.Library.Application/Services/MediaContent/CommonProperties/CommonPropertiesService.cs new file mode 100644 index 0000000..7f84c82 --- /dev/null +++ b/Modules.Library.Application/Services/MediaContent/CommonProperties/CommonPropertiesService.cs @@ -0,0 +1,111 @@ +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Application.Services.MediaContent.CommonProperties; + +public class CommonPropertiesService(ICommonPropertiesGateway commonPropertiesGateway, IGenreGateway genreGateway) +{ + /* + + public async Task SetNameValue(Guid commonPropertiesId, Guid nameId, string value) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.SetNameValue(nameId, value); + } + public async Task RemoveName(Guid commonPropertiesId, Guid nameId) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.RemoveName(nameId); + } + + + public async Task AddPreview(Guid commonPropertiesId, string url, MediaInfoType type) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.SetPreview(url, type); + } + public async Task EditPreview(Guid commonPropertiesId, string url, MediaInfoType type) => + await AddPreview(commonPropertiesId, url, type); + public async Task DeletePreview(Guid commonPropertiesId) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.DeletePreview(); + } + + public async Task AddDescription(Guid commonPropertiesId, string value, bool isOriginal = false) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + switch (isOriginal) + { + case true: + commonProperties.AddOriginalDescription(value); + break; + default: + commonProperties.AddNotOriginalDescription(value); + break; + } + } + public async Task SetDescriptionValue(Guid commonPropertiesId, Guid descriptionId, string value) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.SetDescriptionValue(descriptionId, value); + } + public async Task RemoveDescription(Guid commonPropertiesId, Guid descriptionId) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.RemoveDescription(descriptionId); + } + + public async Task AddGenre(Guid commonPropertiesId, Guid genreId, decimal? value = null) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.AddGenre(genreId, value); + } + public async Task SetGenreProportion(Guid commonPropertiesId, Guid genreProportionItemId, decimal? value = null) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.SetGenreProportion(genreProportionItemId, value); + } + public async Task RemoveGenre(Guid commonPropertiesId, Guid genreId) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.RemoveGenre(genreId); + } + + public async Task AddRelatedContent(Guid commonPropertiesId, string url, MediaInfoType type) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.AddRelatedContent(url, type); + } + public async Task EditRelatedContent(Guid commonPropertiesId, Guid relatedContentId, string url, MediaInfoType type) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.EditRelatedContent(relatedContentId, url, type); + } + public async Task RemoveRelatedContent(Guid commonPropertiesId, Guid relatedContentId) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.RemoveRelatedContent(relatedContentId); + } + + public async Task SetAnnouncementDate(Guid commonPropertiesId, DateTimeOffset value) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.SetAnnouncementDate(value); + } + public async Task SetEstimatedReleaseDate(Guid commonPropertiesId, DateTimeOffset value) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.SetEstimatedReleaseDate(value); + } + public async Task SetReleaseDate(Guid commonPropertiesId, DateTimeOffset value) + { + var commonProperties = await GetCommonProperties(commonPropertiesId); + commonProperties.SetReleaseDate(value); + } + + private async Task GetCommonProperties(string commonPropertiesId) + { + return await commonPropertiesGateway.GetByMediaContentItemId(commonPropertiesId); + } + */ +} \ No newline at end of file diff --git a/Modules.Library.Application/Services/MediaInfoService.cs b/Modules.Library.Application/Services/MediaInfoService.cs new file mode 100644 index 0000000..9779ca3 --- /dev/null +++ b/Modules.Library.Application/Services/MediaInfoService.cs @@ -0,0 +1,26 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Application.Interfaces; +using Modules.Library.Domain.Entities; + +namespace Modules.Library.Application.Services; + +public class MediaInfoService(IMediaInfoGateway mediaInfoGateway) +{ + public async Task Add(IUser user, string url, MediaInfoType type) + { + if (!user.CanAddMediaInfo()) throw new Exception("AccessDenied"); + //await mediaInfoGateway.AddAsync(url, type); + } + + public async Task Edit(IUser user, Guid id, string url, MediaInfoType type) + { + if (!user.CanEditMediaInfo()) throw new Exception("AccessDenied"); + //await domainMediaInfoService.Edit(id, url, type); + } + + public async Task Remove(IUser user, Guid id) + { + if (!user.CanRemoveMediaInfo()) throw new Exception("AccessDenied"); + //await domainMediaInfoService.Remove(id); + } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/Common/IOrderableItem.cs b/Modules.Library.Core/Domain/Common/IOrderableItem.cs new file mode 100644 index 0000000..80efea6 --- /dev/null +++ b/Modules.Library.Core/Domain/Common/IOrderableItem.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Core.Domain.Common; + +public interface IOrderableItem +{ + public uint Order { get; } + + public void SetOrder(uint order); +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/Common/MediaInfo.cs b/Modules.Library.Core/Domain/Common/MediaInfo.cs new file mode 100644 index 0000000..d2131f6 --- /dev/null +++ b/Modules.Library.Core/Domain/Common/MediaInfo.cs @@ -0,0 +1,19 @@ +namespace Modules.Library.Core.Domain.Common; +public class MediaInfo : Entity +{ + public MediaInfoType Type { get; init; } = MediaInfoType.OtherFile; + //public string ContentType { get; set; } = default!; + public string Url { get; set; } = default!; + public void SetUrl(string value) + { + Url = value; + } +} + +public enum MediaInfoType +{ + Image, + Video, + Link, + OtherFile +} diff --git a/Modules.Library.Core/Domain/Genre/Genre.cs b/Modules.Library.Core/Domain/Genre/Genre.cs new file mode 100644 index 0000000..ce4edc1 --- /dev/null +++ b/Modules.Library.Core/Domain/Genre/Genre.cs @@ -0,0 +1,7 @@ +namespace Modules.Library.Core.Domain.Genre; + +public class Genre : Entity +{ + [Required] + public string Name { get; set; } = default!; +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/Language/Language.cs b/Modules.Library.Core/Domain/Language/Language.cs new file mode 100644 index 0000000..a000a21 --- /dev/null +++ b/Modules.Library.Core/Domain/Language/Language.cs @@ -0,0 +1,10 @@ +namespace Modules.Library.Core.Domain.Language; + +public class Language : Entity +{ + [Required] + public string CodeIso2 { get; set; } = default!; + [Required] + public string Name { get; set; } = default!; + public MediaInfo? Icon { get; set; } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/CommonProperties/CommonProperties.cs b/Modules.Library.Core/Domain/MediaContent/CommonProperties/CommonProperties.cs new file mode 100644 index 0000000..4eca8a8 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/CommonProperties/CommonProperties.cs @@ -0,0 +1,164 @@ +using System.Transactions; + +namespace Modules.Library.Core.Domain.MediaContent.CommonProperties; + +public class CommonProperties : Entity +{ + private List _names = []; + public IReadOnlyCollection Names => _names.AsReadOnly(); + public MediaInfo? Preview { get; private set; } + + private List _descriptions = []; + public IReadOnlyCollection Descriptions => _descriptions.AsReadOnly(); + + public List _genres = []; + public IReadOnlyCollection Genres => _genres.AsReadOnly(); + + public List _relatedContent = []; + public IReadOnlyCollection RelatedContent => _relatedContent.AsReadOnly(); + + public DateTimeOffset? AnnouncementDate { get; private set; } + public DateTimeOffset? EstimatedReleaseDate { get; private set; } + public DateTimeOffset? ReleaseDate { get; private set; } + + #region Names + public void AddNameOriginal(Guid language) + { + if (_names.Any(q => q.Type == NameType.Original)) throw new Exception("Original name is already exist."); + _names.Add(new NameItem { LanguageId = language, Type = NameType.Original }); + } + public void AddNameOriginalInAnotherLanguage(Guid language, string value) + { + if (language == _names.Single(q => q.Type == NameType.Original).LanguageId) + throw new Exception("Language must not match original name language"); + + if (_names.Any(q => q.Type == NameType.OriginalInAnotherLanguage && q.LanguageId == language)) + throw new Exception("Name in following language is already exist."); + + _names.Add(new NameItem { LanguageId = language, Type = NameType.OriginalInAnotherLanguage }); + } + public void AddNameTranslation(Guid language, string value) + { + _names.Add(new NameItem { LanguageId = language, Type = NameType.Translation }); + } + public void AddNameAbbreviation(Guid language, string value) + { + _names.Add(new NameItem { LanguageId = language, Type = NameType.Abbreviation }); + } + + public void RemoveName(Guid nameId) + { + var name = GetName(nameId); + if (name.Type == NameType.Original) throw new Exception($"Unable to remove original name"); + _names.Remove(name); + } + + public void SetNameValue(Guid nameId, string value) + { + var name = GetName(nameId); + name.SetValue(value); + } + + private NameItem GetName(Guid nameId) + { + return _names.FirstOrDefault(q => q.Id == nameId) ?? throw new Exception($"Name with id [{nameId}] is not found"); + } + #endregion + + public void SetPreview(string url) + { + Preview ??= new MediaInfo { Type = MediaInfoType.Image }; + Preview.SetUrl(url); + } + public void DeletePreview() + { + Preview = null; + } + + #region Descriptions + public void AddDescription(Guid language, bool isOriginal) + { + if (_descriptions.Any(q => q.IsOriginal) && isOriginal) throw new Exception("Could not add one more original description"); + _descriptions.Add(new DescriptionItem { LanguageId = language, IsOriginal = isOriginal}); + } + + public void RemoveDescription(Guid descriptionId) + { + var description = GetDescription(descriptionId); + //if (description.IsOriginal) throw new Exception($"Unable to remove original description"); + _descriptions.Remove(description); + } + + public void SetDescriptionValue(Guid descriptionId, string value) + { + var description = GetDescription(descriptionId); + description.SetValue(value); + } + + private DescriptionItem GetDescription(Guid descriptionId) + { + return _descriptions.FirstOrDefault(q => q.Id == descriptionId) ?? throw new Exception($"Description with id [{descriptionId}] is not found"); + } + #endregion + + #region Genres + public void AddGenre(Guid genreId) + { + _genres.Add(new Genre_Percentage { GenreId = genreId }); + } + + public void RemoveGenre(Guid genreItemId) + { + var genre = GetGenreItem(genreItemId); + _genres.Remove(genre); + } + + public void AddGenreValue(Guid genreItemId, decimal value) + { + var genre = GetGenreItem(genreItemId); + genre.SetValue(value); + } + private Genre_Percentage GetGenreItem(Guid genreItemId) + { + return _genres.FirstOrDefault(q => q.Id == genreItemId) ?? throw new Exception($"Genre item with id [{genreItemId}] is not found"); + } + #endregion + + #region RelatedContents + public void AddRelatedContent(MediaInfoType type) + { + _relatedContent.Add(new MediaInfo { Type = type }); + } + + public void RemoveRelatedContent(Guid relatedContentId) + { + var relatedContent = GetRelatedContent(relatedContentId); + _relatedContent.Remove(relatedContent); + + } + + public void AddRelatedContent(Guid relatedContentId, string url) + { + var relatedContent = GetRelatedContent(relatedContentId); + relatedContent.SetUrl(url); + } + + public MediaInfo GetRelatedContent(Guid relatedContentId) + { + return _relatedContent.FirstOrDefault(q => q.Id == relatedContentId) ?? throw new Exception($"Related content with id [{relatedContentId}] is not found"); + } + #endregion + + public void SetAnnouncementDate(DateTimeOffset value) + { + AnnouncementDate = value; + } + public void SetEstimatedReleaseDate(DateTimeOffset value) + { + EstimatedReleaseDate = value; + } + public void SetReleaseDate(DateTimeOffset value) + { + ReleaseDate = value; + } +} diff --git a/Modules.Library.Core/Domain/MediaContent/CommonProperties/Description.cs b/Modules.Library.Core/Domain/MediaContent/CommonProperties/Description.cs new file mode 100644 index 0000000..031b6c8 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/CommonProperties/Description.cs @@ -0,0 +1,14 @@ +namespace Modules.Library.Core.Domain.MediaContent.CommonProperties; +public class DescriptionItem : Entity +{ + [Required] + public string Value { get; private set; } = string.Empty; + public bool IsOriginal { get; init; } + [Required] + public Guid LanguageId { get; init; } = default!; + + public void SetValue(string value) + { + Value = value; + } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/CommonProperties/Genre_Percentage.cs b/Modules.Library.Core/Domain/MediaContent/CommonProperties/Genre_Percentage.cs new file mode 100644 index 0000000..2815ab4 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/CommonProperties/Genre_Percentage.cs @@ -0,0 +1,13 @@ +namespace Modules.Library.Core.Domain.MediaContent.CommonProperties; + +public class Genre_Percentage : Entity +{ + public decimal Value { get; private set; } + [Required] + public Guid GenreId { get; init; } = default!; + + public void SetValue(decimal value) + { + Value = value; + } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/CommonProperties/NameItem.cs b/Modules.Library.Core/Domain/MediaContent/CommonProperties/NameItem.cs new file mode 100644 index 0000000..259a8ea --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/CommonProperties/NameItem.cs @@ -0,0 +1,23 @@ +namespace Modules.Library.Core.Domain.MediaContent.CommonProperties; + +public class NameItem : Entity +{ + [Required] + public string Value { get; private set; } = string.Empty; + public NameType Type { get; init; } + [Required] + public Guid LanguageId { get; init; } = default!; + + public void SetValue(string value) + { + Value = value; + } +} + +public enum NameType +{ + Original, + OriginalInAnotherLanguage, + Translation, + Abbreviation, +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/ICompletable.cs b/Modules.Library.Core/Domain/MediaContent/ICompletable.cs new file mode 100644 index 0000000..c7e6c3f --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/ICompletable.cs @@ -0,0 +1,10 @@ +namespace Modules.Library.Core.Domain.MediaContent; + +public interface ICompletable +{ + public bool Completed { get; } + public TimeSpan ExpirationTime { get; } + public void SetCompleted(); + public void SetNotCompleted(); + public void SetExpirationTime(TimeSpan value); +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeEpisode.cs b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeEpisode.cs new file mode 100644 index 0000000..dba76a1 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeEpisode.cs @@ -0,0 +1,14 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items.Anime; + +public class AnimeEpisode : AnimeItemSingle +{ + public AnimeEpisodeType Type { get; set; } = AnimeEpisodeType.Regilar; + public int? Number { get; set; } +} + +public enum AnimeEpisodeType +{ + Regilar, + FullLength, + Ova, +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeItem.cs b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeItem.cs new file mode 100644 index 0000000..d9e7b72 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeItem.cs @@ -0,0 +1,6 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items.Anime; + +public abstract class AnimeItem : MediaContentItem +{ + +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeItemSingle.cs b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeItemSingle.cs new file mode 100644 index 0000000..1082a28 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeItemSingle.cs @@ -0,0 +1,6 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items.Anime; + +public abstract class AnimeItemSingle: AnimeItem, INotContainer +{ + public TimeSpan? Duration { get; set; } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeSeason.cs b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeSeason.cs new file mode 100644 index 0000000..1ea1162 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeSeason.cs @@ -0,0 +1,44 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items.Anime; + +public class AnimeSeason : AnimeItem, IContainer +{ + public int? Number { get; set; } + public string Director { get; protected set; } = default!; + public string OriginCountry { get; protected set; } = default!; + + protected List _items = []; + public IReadOnlyCollection Items => _items.AsReadOnly(); + + public void AddEpisode(AnimeEpisode episode) + { + var lastOrder = _items.OrderByDescending(q => q.Order).FirstOrDefault()?.Order; + _items.Add(episode); + episode.SetOrder(lastOrder.HasValue ? lastOrder.Value + 1 : 0); + CheckIfCompleted(); + } + + public void AddEpisodeAsVariant(AnimeEpisode episode, uint order) + { + var lastVariant = _items.OrderByDescending(q => q.Variant).FirstOrDefault(q => q.Order == order)?.Variant ?? + throw new Exception($"Could not find episode with order [{order}]"); + + _items.Add(episode); + episode.SetOrder(order); + //episode.SetVariant(lastVariant.HasValue ? lastVariant.Value + 1 : 0); + episode.SetVariant(lastVariant + 1); + CheckIfCompleted(); + } + + private void CheckIfCompleted() + { + var itemsQuery = Items.AsQueryable(); + var unreleasedEpisodesAreExists = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + //return unreleasedEpisodesAreExists || lastEpisodeReleaseDate - DateTime.UtcNow > expirePeriod; + if (unreleasedEpisodesAreExists || lastEpisodeReleaseDate >= (DateTime.UtcNow - ExpirationTime)) SetNotCompleted(); + else SetCompleted(); + } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeTitle.cs b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeTitle.cs new file mode 100644 index 0000000..a5945d1 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/Anime/AnimeTitle.cs @@ -0,0 +1,120 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items.Anime; + +public class AnimeTitle : TopLevelItem +{ + public void AddSeason() + { + var season = new AnimeSeason(); + _items.Add(season); + CheckIfCompleted(); + } + + + public void AddEpisode(AnimeEpisodeType type, Guid? seasonId = null) + { + var episode = new AnimeEpisode + { + Type = type + }; + if (!seasonId.HasValue) + { + _items.Add(episode); + episode.SetOrder(GetItemsLastOrder() + 1); + } + else + { + var season = GetSeason(seasonId.Value); + season.AddEpisode(episode); + } + CheckIfCompleted(); + } + + public void AddEpisodeVariant(AnimeEpisodeType type, uint episodeOrder, Guid? seasonId = null) + { + var episode = new AnimeEpisode + { + Type = type + }; + if (!seasonId.HasValue) + { + var lastVariant = _items.OrderByDescending(q => q.Variant).FirstOrDefault(q => q.Order == episodeOrder)?.Variant ?? + throw new Exception($"Could not find episode with order [{episodeOrder}]"); + _items.Add(episode); + episode.SetOrder(episodeOrder); + episode.SetOrder(lastVariant + 1); + } + else + { + var season = GetSeason(seasonId.Value); + season.AddEpisodeAsVariant(episode, episodeOrder); + } + CheckIfCompleted(); + } + + private uint GetItemsLastOrder() => + (uint)(_items.OfType() + .OrderByDescending(q => q.Order).LastOrDefault()?.Order ?? -1u); + private void CheckIfCompleted() + { + var itemsQuery = Items.AsQueryable(); + var ucompletedSeasons = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + if (ucompletedSeasons || lastEpisodeReleaseDate >= (DateTime.UtcNow - ExpirationTime)) SetNotCompleted(); + else SetCompleted(); + } + + #region Episode Properties + public void SetEpisodeDuration(Guid episodeId, TimeSpan? value, Guid ? seasonId = null) + { + var episode = GetEpisode(episodeId, seasonId); + episode.Duration = value; + } + + #region Name + public void AddEpisodeName(Guid episodeId, string? value, Guid? seasonId = null) + { + var episode = GetEpisode(episodeId, seasonId); + episode.Duration = value; + } + + public void AddName(CommonProperties.CommonProperties commonProperties, Language.Language language, string? value) + { + var episode = GetEpisode(episodeId, seasonId); + episode.Duration = value; + } + #endregion + + #region Description + + #endregion + + #region Genre + + #endregion + + #endregion + + private AnimeEpisode GetEpisode(Guid episodeId, Guid? seasonId = null) + { + AnimeEpisode? result = null; + if (seasonId.HasValue) + { + var season = GetSeason(seasonId.Value); + result = season.Items.OfType() + .FirstOrDefault(q => q.Id == episodeId); + } + else + { + result = _items.OfType() + .FirstOrDefault(q => q.Id == episodeId); + } + return result ?? throw new Exception($"Could not find episode with id [{episodeId}]"); + } + + private AnimeSeason GetSeason(Guid seasonId) => + _items.OfType().FirstOrDefault(q => q.Id == seasonId) ?? + throw new Exception($"Could not find season with id [{seasonId}]"); +} diff --git a/Modules.Library.Core/Domain/MediaContent/Items/IContainer.cs b/Modules.Library.Core/Domain/MediaContent/Items/IContainer.cs new file mode 100644 index 0000000..bb3225e --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/IContainer.cs @@ -0,0 +1,6 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items; + +public interface IContainer +{ + public IReadOnlyCollection Items { get; } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/Items/INotContainer.cs b/Modules.Library.Core/Domain/MediaContent/Items/INotContainer.cs new file mode 100644 index 0000000..9b15a41 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/INotContainer.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items; + +public interface INotContainer { } + +public interface INotContainer +{ + public IContainer Container { get; } +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/Items/IVariantItem.cs b/Modules.Library.Core/Domain/MediaContent/Items/IVariantItem.cs new file mode 100644 index 0000000..03d2fc1 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/Items/IVariantItem.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Core.Domain.MediaContent.Items; + +public interface IVariantItem +{ + public uint Variant { get; } + + public void SetVariant(uint variant); +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/MediaContentItem.cs b/Modules.Library.Core/Domain/MediaContent/MediaContentItem.cs new file mode 100644 index 0000000..6296e52 --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/MediaContentItem.cs @@ -0,0 +1,29 @@ +using Modules.Library.Core.Domain.MediaContent.Items; + +namespace Modules.Library.Core.Domain.MediaContent; + +public abstract class MediaContentItem : Entity, IOrderableItem, IVariantItem, ICompletable +{ + [Required] + public CommonProperties.CommonProperties CommonProperties { get; set; } = default!; + + public uint Order { get; private set; } + + public uint Variant { get; private set; } + + + + public bool Completed { get; private set; } + + public TimeSpan ExpirationTime { get; private set; } = TimeSpan.Zero; + + + public void SetOrder(uint order) => Order = order; + + public void SetVariant(uint variant) => Variant = variant; + + + public void SetCompleted() => Completed = true; + public void SetNotCompleted() => Completed = false; + public void SetExpirationTime(TimeSpan value) => ExpirationTime = value; +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/MediaContent/TopLevelItem.cs b/Modules.Library.Core/Domain/MediaContent/TopLevelItem.cs new file mode 100644 index 0000000..8c40cad --- /dev/null +++ b/Modules.Library.Core/Domain/MediaContent/TopLevelItem.cs @@ -0,0 +1,22 @@ + +namespace Modules.Library.Core.Domain.MediaContent; + +public class TopLevelItem : Entity, IAggregateRoot, ICompletable +{ + [Required] + public CommonProperties.CommonProperties CommonProperties { get; set; } = default!; + + protected List _items = []; + public IReadOnlyCollection Items => _items.AsReadOnly(); + + [Required] + public User.User Creator { get; private set; } = default!; + + public bool Completed { get; private set; } + + public TimeSpan ExpirationTime { get; private set; } = TimeSpan.Zero; + + public void SetCompleted() => Completed = true; + public void SetNotCompleted() => Completed = false; + public void SetExpirationTime(TimeSpan value) => ExpirationTime = value; +} \ No newline at end of file diff --git a/Modules.Library.Core/Domain/Models/ITopLevelItem.cs b/Modules.Library.Core/Domain/Models/ITopLevelItem.cs new file mode 100644 index 0000000..07b556f --- /dev/null +++ b/Modules.Library.Core/Domain/Models/ITopLevelItem.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Modules.Library.Core.Domain.Entities +{ + internal interface ITopLevelItem + { + } +} diff --git a/Modules.Library.Core/Domain/User/User.cs b/Modules.Library.Core/Domain/User/User.cs new file mode 100644 index 0000000..79942e7 --- /dev/null +++ b/Modules.Library.Core/Domain/User/User.cs @@ -0,0 +1,8 @@ +using Common.Domain.Abstractions; + +namespace Modules.Library.Core.Domain.User; + +public class User : Entity +{ + +} \ No newline at end of file diff --git a/Modules.Library.Core/GlobalUsings.cs b/Modules.Library.Core/GlobalUsings.cs new file mode 100644 index 0000000..38afdce --- /dev/null +++ b/Modules.Library.Core/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using Common.Domain.Abstractions; +global using Modules.Library.Core.Domain.Common; + +global using System.ComponentModel.DataAnnotations; \ No newline at end of file diff --git a/Modules.Library.Core/Modules.Library.Core.csproj b/Modules.Library.Core/Modules.Library.Core.csproj new file mode 100644 index 0000000..852e17b --- /dev/null +++ b/Modules.Library.Core/Modules.Library.Core.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Modules.Library.Database/Database/Models/Anime/AnimeEpisode1.cs b/Modules.Library.Database/Database/Models/Anime/AnimeEpisode1.cs new file mode 100644 index 0000000..561634c --- /dev/null +++ b/Modules.Library.Database/Database/Models/Anime/AnimeEpisode1.cs @@ -0,0 +1,14 @@ +namespace Modules.Library.Database.Database.Models.Anime; + +public class AnimeEpisode1 : AnimeItem1 +{ + public AnimeEpisodeType1 Type { get; set; } + public int? Number { get; set; } +} + +public enum AnimeEpisodeType1 +{ + Regilar, + FullLength, + Ova, +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/Anime/AnimeItem1.cs b/Modules.Library.Database/Database/Models/Anime/AnimeItem1.cs new file mode 100644 index 0000000..e3dd4ee --- /dev/null +++ b/Modules.Library.Database/Database/Models/Anime/AnimeItem1.cs @@ -0,0 +1,10 @@ +namespace Modules.Library.Database.Database.Models.Anime; + +public class AnimeItem1 : Entity1 +{ + public CommonProperties.CommonProperties1 CommonProperties { get; set; } = default!; + public ushort Order { get; set; } + public ushort Variant { get; set; } + public bool Completed { get; set; } + public TimeSpan ExpirationTime { get; set; } +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/Anime/AnimeSeasonq.cs b/Modules.Library.Database/Database/Models/Anime/AnimeSeasonq.cs new file mode 100644 index 0000000..695a141 --- /dev/null +++ b/Modules.Library.Database/Database/Models/Anime/AnimeSeasonq.cs @@ -0,0 +1,10 @@ +namespace Modules.Library.Database.Database.Models.Anime; + +public class AnimeSeasonq : AnimeItem1 +{ + public List Episodes { get; set; } = []; + + public int? Number { get; set; } + public string? Director { get; set; } + public string? OriginCountry { get; set; } +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/Anime/AnimeTitle1.cs b/Modules.Library.Database/Database/Models/Anime/AnimeTitle1.cs new file mode 100644 index 0000000..83132a1 --- /dev/null +++ b/Modules.Library.Database/Database/Models/Anime/AnimeTitle1.cs @@ -0,0 +1,9 @@ +namespace Modules.Library.Database.Database.Models.Anime; + +public class AnimeTitle1 : Entity1 +{ + public CommonProperties.CommonProperties1 CommonProperties { get; set; } = default!; + public List Items { get; set; } = []; + public bool Completed { get; set; } + public TimeSpan ExpirationTime { get; set; } +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/CommonProperties/CommonProperties1.cs b/Modules.Library.Database/Database/Models/CommonProperties/CommonProperties1.cs new file mode 100644 index 0000000..ff1d94a --- /dev/null +++ b/Modules.Library.Database/Database/Models/CommonProperties/CommonProperties1.cs @@ -0,0 +1,15 @@ +namespace Modules.Library.Database.Database.Models.CommonProperties; + +public class CommonProperties1 : Entity1 +{ + public List Names { get; set; } = []; + public MediaInfo1? Preview { get; set; } + + public List Descriptions { get; set; } = []; + public List Genres { get; set; } = []; + public List RelatedContent { get; set; } = []; + + public DateTimeOffset? AnnouncementDate { get; set; } + public DateTimeOffset? EstimatedReleaseDate { get; set; } + public DateTimeOffset? ReleaseDate { get; set; } +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/CommonProperties/DescriptionItem1.cs b/Modules.Library.Database/Database/Models/CommonProperties/DescriptionItem1.cs new file mode 100644 index 0000000..fb54508 --- /dev/null +++ b/Modules.Library.Database/Database/Models/CommonProperties/DescriptionItem1.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Database.Database.Models.CommonProperties; + +public class DescriptionItem1 : Entity1 +{ + public string Value { get; set; } = string.Empty; + public bool IsOriginal { get; set; } + public string LanguageId { get; set; } = default!; +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/CommonProperties/GenreProportionItem1.cs b/Modules.Library.Database/Database/Models/CommonProperties/GenreProportionItem1.cs new file mode 100644 index 0000000..8c72123 --- /dev/null +++ b/Modules.Library.Database/Database/Models/CommonProperties/GenreProportionItem1.cs @@ -0,0 +1,7 @@ +namespace Modules.Library.Database.Database.Models.CommonProperties; + +public class GenreProportionItem1 : Entity1 +{ + public decimal? Proportion { get; set; } + public string GenreId { get; set; } = default!; +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/CommonProperties/NameItem1.cs b/Modules.Library.Database/Database/Models/CommonProperties/NameItem1.cs new file mode 100644 index 0000000..3aeab6f --- /dev/null +++ b/Modules.Library.Database/Database/Models/CommonProperties/NameItem1.cs @@ -0,0 +1,16 @@ +namespace Modules.Library.Database.Database.Models.CommonProperties; + +public class NameItem1 : Entity1 +{ + public string Value { get; set; } = string.Empty; + public NameType1 Type { get; set; } + public string LanguageId { get; set; } = default!; +} + +public enum NameType1 +{ + Original, + OriginalInAnotherLanguage, + Translation, + Abbreviation, +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/Entity1.cs b/Modules.Library.Database/Database/Models/Entity1.cs new file mode 100644 index 0000000..44ce4f5 --- /dev/null +++ b/Modules.Library.Database/Database/Models/Entity1.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace Modules.Library.Database.Database.Models; + +public class Entity1 : Domain.Entities.Entity +{ + public ObjectId ObjectId { get; set; } + public override string Id => ObjectId.ToString(); + + public void SetDeleted(bool value) => Deleted = value; +} diff --git a/Modules.Library.Database/Database/Models/Genre/Genre1.cs b/Modules.Library.Database/Database/Models/Genre/Genre1.cs new file mode 100644 index 0000000..b74fbce --- /dev/null +++ b/Modules.Library.Database/Database/Models/Genre/Genre1.cs @@ -0,0 +1,6 @@ +namespace Modules.Library.Database.Database.Models.Genre; + +public class Genre1 : Entity1 +{ + public string Name { get; set; } = default!; +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/Language/Language1.cs b/Modules.Library.Database/Database/Models/Language/Language1.cs new file mode 100644 index 0000000..704f32e --- /dev/null +++ b/Modules.Library.Database/Database/Models/Language/Language1.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Database.Database.Models.Language; + +public class Language1 : Entity1 +{ + public string CodeIso2 { get; set; } = default!; + public string Name { get; set; } = default!; + public Guid? IconId { get; set; } +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/Models/MediaInfo1.cs b/Modules.Library.Database/Database/Models/MediaInfo1.cs new file mode 100644 index 0000000..95f9154 --- /dev/null +++ b/Modules.Library.Database/Database/Models/MediaInfo1.cs @@ -0,0 +1,15 @@ +namespace Modules.Library.Database.Database.Models; + +public class MediaInfo1 : Entity1 +{ + public MediaInfoType1 Type { get; set; } = MediaInfoType1.OtherFile; + public string Url { get; set; } = default!; +} + +public enum MediaInfoType1 +{ + Image, + Video, + Link, + OtherFile +} \ No newline at end of file diff --git a/Modules.Library.Database/Database/MongoDbContext.cs b/Modules.Library.Database/Database/MongoDbContext.cs new file mode 100644 index 0000000..222cc94 --- /dev/null +++ b/Modules.Library.Database/Database/MongoDbContext.cs @@ -0,0 +1,46 @@ +using Modules.Library.Domain.Entities; +using Modules.Library.Domain.Entities.Genre; +using Modules.Library.Domain.Entities.Language; +using Modules.Library.Domain.Entities.MediaContent.CommonProperties; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.EntityBuilders; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Bson.Serialization.IdGenerators; +using MongoDB.Driver; +using System.Linq.Expressions; +using System.Reflection; + +namespace Modules.Library.Database.Database; + +public class MongoDbContext(IMongoDatabase database) +{ + private bool _initialized; + public IMongoCollection GetCollection(string collectionName) + { + if (!_initialized) throw new Exception(string.Concat(nameof(MongoDbContext), " has not initialized yet")); + return database.GetCollection(collectionName); + } + + public IMongoCollection Genres => GetCollection(nameof(Genre)); + public IMongoCollection Languages => GetCollection(nameof(Language)); + public IMongoCollection MediaInfos => GetCollection(nameof(MediaInfo)); + + private const string _mediaContentItemsCollectionName = "MediaContentItems"; + public IMongoCollection AnimeTitles => GetCollection(_mediaContentItemsCollectionName); + + public void Initialize() + { + BsonClassMap.RegisterClassMap(q => + { + //q.AutoMap(); + q.MapCreator(q => AnimeTitleBuilder.FromAnimeTitle(q).Build()); + //q.MapIdMember(c => c.Id).SetIdGenerator(StringObjectIdGenerator.Instance); + }); + _initialized = true; + } + + //private IntSequence _paymentProviderIdSequence = new("PaymentProviderId"); + + //public Task GetNextPaymentProviderId() => _paymentProviderIdSequence.GetNextSequenceValue(context); +} diff --git a/Modules.Library.Database/Modules.Library.Database.csproj b/Modules.Library.Database/Modules.Library.Database.csproj new file mode 100644 index 0000000..f6ad13d --- /dev/null +++ b/Modules.Library.Database/Modules.Library.Database.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Modules.Library.Database/Repositories/AnimeTitleRepository.cs b/Modules.Library.Database/Repositories/AnimeTitleRepository.cs new file mode 100644 index 0000000..bb0a870 --- /dev/null +++ b/Modules.Library.Database/Repositories/AnimeTitleRepository.cs @@ -0,0 +1,73 @@ +using Modules.Library.Application.Gateways; +using Modules.Library.Database.Database; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.Gateways; +using MongoDB.Driver; + +namespace Modules.Library.Database.Repositories; + +public class AnimeTitleRepository(MongoDbContext context) : RepositoryBase(context), IAnimeTitleGateway +{ + protected override IMongoCollection GetCollections(MongoDbContext context) => context.AnimeTitles; + + protected override async Task SoftDeleteAsync(AnimeTitle entity) + { + entity.Delete(); + return await UpdateAsync(entity); + } + + /* + public async Task AddAsync(Domain.Entities.MediaContent.Items.Anime.AnimeTitle entity, IUser user) => + await AddAsync(ToDbConverter.Title(entity)); + + public Task AnyWhere(Expression> predicate) + { + var p = predicate. + } + + public Task DeleteAsync(Domain.Entities.MediaContent.Items.Anime.AnimeTitle entity, IUser user) + { + throw new NotImplementedException(); + } + + public Task GetByIdAsync(string id) + { + throw new NotImplementedException(); + } + + public Task GetByIdOrDefaultAsync(string id) + { + throw new NotImplementedException(); + } + + public Task GetFirstOrDefaultWhere(Expression> predicate) + { + throw new NotImplementedException(); + } + + public Task GetFirstWhere(Expression> predicate) + { + throw new NotImplementedException(); + } + + public Task> GetRangeByIdsAsync(List ids) + { + throw new NotImplementedException(); + } + + public Task> GetWhere(Expression> predicate) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(Domain.Entities.MediaContent.Items.Anime.AnimeTitle entity, IUser user) + { + throw new NotImplementedException(); + } + + Task> Application.Gateways.IRepository.GetAllAsync() + { + throw new NotImplementedException(); + } + */ +} \ No newline at end of file diff --git a/Modules.Library.Database/Repositories/Converters/ToDbConverter.cs b/Modules.Library.Database/Repositories/Converters/ToDbConverter.cs new file mode 100644 index 0000000..9df3ca8 --- /dev/null +++ b/Modules.Library.Database/Repositories/Converters/ToDbConverter.cs @@ -0,0 +1,143 @@ +//using Modules.Library.Database.Database.Models; +//using Modules.Library.Database.Database.Models.Anime; +//using Modules.Library.Database.Database.Models.CommonProperties; +//using MongoDB.Bson; + +//namespace Modules.Library.Database.Repositories.Converters; + +//internal static class ToDbConverter +//{ +// internal static AnimeTitle1 Title(Domain.Entities.MediaContent.Items.Anime.AnimeTitle entity) +// { +// var result = new AnimeTitle1 +// { +// CommonProperties = ToDbConverter.CommonProperties(entity.CommonProperties), +// Items = entity.Items.Select(q => +// { +// AnimeItem1 result; +// if (q is Domain.Entities.MediaContent.Items.Anime.AnimeSeason season) result = Season(season); +// else if (q is Domain.Entities.MediaContent.Items.Anime.AnimeEpisode episode) result = Episode(episode); +// else throw new NotImplementedException(); +// return result; +// }).ToList(), +// Completed = entity.Completed, +// ExpirationTime = entity.ExpirationTime, +// }; +// ToDbConverterBase.SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// internal static AnimeSeasonq Season(Domain.Entities.MediaContent.Items.Anime.AnimeSeason entity) +// { +// var result = new AnimeSeasonq +// { +// CommonProperties = ToDbConverter.CommonProperties(entity.CommonProperties), +// Episodes = entity.Episodes.Select(Episode).ToList(), +// Number = entity.Number, +// Order = entity.Order, +// Variant = entity.Variant, +// OriginCountry = entity.OriginCountry, +// Completed = entity.Completed, +// Director = entity.Director, +// ExpirationTime = entity.ExpirationTime, +// }; +// ToDbConverterBase.SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// internal static AnimeEpisode1 Episode(Domain.Entities.MediaContent.Items.Anime.AnimeEpisode entity) +// { +// var result = new AnimeEpisode1 +// { +// CommonProperties = ToDbConverter.CommonProperties(entity.CommonProperties), +// Type = (AnimeEpisodeType1)entity.Type, +// Order = entity.Order, +// Variant = entity.Variant, +// Completed = entity.Completed, +// Number = entity.Number, +// ExpirationTime = entity.ExpirationTime, +// }; +// ToDbConverterBase.SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// internal static CommonProperties1 CommonProperties(Domain.Entities.MediaContent.CommonProperties.CommonProperties entity) +// { +// var result = new CommonProperties1 +// { +// Names = entity.Names.Select(NameItem).ToList(), +// Preview = entity.Preview != null ? MediaInfo(entity.Preview) : null, +// Descriptions = entity.Descriptions.Select(Description).ToList(), +// Genres = entity.Genres.Select(Genre).ToList(), +// RelatedContent = entity.RelatedContent.Select(MediaInfo).ToList(), +// AnnouncementDate = entity.AnnouncementDate, +// EstimatedReleaseDate = entity.EstimatedReleaseDate, +// ReleaseDate = entity.ReleaseDate, +// }; +// SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// //internal static NameItem NameItem(Domain.Entities.MediaContent.CommonProperties.NameItem entity, bool createNew) +// internal static NameItem1 NameItem(Domain.Entities.MediaContent.CommonProperties.NameItem entity) +// { +// var result = new NameItem1 +// { +// LanguageId = entity.LanguageId, +// Type = (NameType1)entity.Type, +// Value = entity.Value, +// }; +// SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// //internal static MediaInfo NameItem(Domain.Entities.MediaInfo entity, bool createNew) +// internal static MediaInfo1 MediaInfo(Domain.Entities.MediaInfo entity) +// { +// var result = new MediaInfo1 +// { +// Type = (MediaInfoType1)entity.Type, +// Url = entity.Url, +// }; +// SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// internal static DescriptionItem1 Description(Domain.Entities.MediaContent.CommonProperties.DescriptionItem entity) +// { +// var result = new DescriptionItem1 +// { +// LanguageId = entity.LanguageId, +// IsOriginal = entity.IsOriginal, +// Value = entity.Value, +// }; +// SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// internal static GenreProportionItem1 Genre(Domain.Entities.MediaContent.CommonProperties.GenreProportionItem entity) +// { +// var result = new GenreProportionItem1 +// { +// GenreId = entity.GenreId, +// Proportion = entity.Proportion, +// }; +// SetId(result, entity.Id); +// result.SetDeleted(entity.Deleted); +// return result; +// } + +// internal static void SetId(Entity1 entity, string? domainEntityId = null) +// { +// if (string.IsNullOrWhiteSpace(domainEntityId)) entity.ObjectId = ObjectId.GenerateNewId(); +// else entity.ObjectId = ObjectId.Parse(domainEntityId); +// } + +//} \ No newline at end of file diff --git a/Modules.Library.Database/Repositories/Converters/ToDbConverterBase.cs b/Modules.Library.Database/Repositories/Converters/ToDbConverterBase.cs new file mode 100644 index 0000000..17aac26 --- /dev/null +++ b/Modules.Library.Database/Repositories/Converters/ToDbConverterBase.cs @@ -0,0 +1,13 @@ +//using Modules.Library.Database.Database.Models; +//using MongoDB.Bson; + +//namespace Modules.Library.Database.Repositories.Converters; + +//internal static class ToDbConverterBase +//{ +// internal static void SetId(Entity1 entity, string? domainEntityId = null) +// { +// if (string.IsNullOrWhiteSpace(domainEntityId)) entity.ObjectId = ObjectId.GenerateNewId(); +// else entity.ObjectId = ObjectId.Parse(domainEntityId); +// } +//} \ No newline at end of file diff --git a/Modules.Library.Database/Repositories/GenreRepository.cs b/Modules.Library.Database/Repositories/GenreRepository.cs new file mode 100644 index 0000000..9308c4a --- /dev/null +++ b/Modules.Library.Database/Repositories/GenreRepository.cs @@ -0,0 +1,18 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Database.Database; +using Modules.Library.Domain.Entities.Genre; +using MongoDB.Driver; + +namespace Modules.Library.Database.Repositories; + +public class GenreRepository(MongoDbContext context) : RepositoryBase(context), IGenreGateway +{ + protected override IMongoCollection GetCollections(MongoDbContext context) => context.Genres; + /* + protected override async Task SoftDeleteAsync(Genre entity) + { + //entity.Delete(); + return await UpdateAsync(entity); + } + */ +} \ No newline at end of file diff --git a/Modules.Library.Database/Repositories/IRepository.cs b/Modules.Library.Database/Repositories/IRepository.cs new file mode 100644 index 0000000..e90197b --- /dev/null +++ b/Modules.Library.Database/Repositories/IRepository.cs @@ -0,0 +1,31 @@ +using MongoDB.Bson; +using System.Linq.Expressions; + +using Modules.Library.Domain.Entities; + +namespace Modules.Library.Database.Repositories; + +internal interface IRepository : IRepository where T : Entity { } + +internal interface IRepository +{ + Task> GetAllAsync(); + + Task GetByIdAsync(TKey id); + Task GetByIdOrDefaultAsync(TKey id); + + Task> GetRangeByIdsAsync(List ids); + + Task GetFirstWhere(Expression> predicate); + Task GetFirstOrDefaultWhere(Expression> predicate); + + Task> GetWhere(Expression> predicate); + + Task AnyWhere(Expression> predicate); + + Task AddAsync(T entity); + + Task UpdateAsync(T entity); + + Task DeleteAsync(T entity); +} \ No newline at end of file diff --git a/Modules.Library.Database/Repositories/LanguageRepository.cs b/Modules.Library.Database/Repositories/LanguageRepository.cs new file mode 100644 index 0000000..3b1cda6 --- /dev/null +++ b/Modules.Library.Database/Repositories/LanguageRepository.cs @@ -0,0 +1,11 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Database.Database; +using Modules.Library.Domain.Entities.Language; +using MongoDB.Driver; + +namespace Modules.Library.Database.Repositories; + +public class LanguageRepository(MongoDbContext context) : RepositoryBase(context), ILanguageGateway +{ + protected override IMongoCollection GetCollections(MongoDbContext context) => context.Languages; +} \ No newline at end of file diff --git a/Modules.Library.Database/Repositories/MediaInfoRepository.cs b/Modules.Library.Database/Repositories/MediaInfoRepository.cs new file mode 100644 index 0000000..ceac82c --- /dev/null +++ b/Modules.Library.Database/Repositories/MediaInfoRepository.cs @@ -0,0 +1,11 @@ +using Modules.Library.Database.Database; +using Modules.Library.Domain.Entities; +using Modules.Library.Domain.Gateways; +using MongoDB.Driver; + +namespace Modules.Library.Database.Repositories; + +public class MediaInfoRepository(MongoDbContext context) : RepositoryBase(context), IMediaInfoGateway +{ + protected override IMongoCollection GetCollections(MongoDbContext context) => context.MediaInfos; +} \ No newline at end of file diff --git a/Modules.Library.Database/Repositories/RepositoryBase.cs b/Modules.Library.Database/Repositories/RepositoryBase.cs new file mode 100644 index 0000000..82b78ab --- /dev/null +++ b/Modules.Library.Database/Repositories/RepositoryBase.cs @@ -0,0 +1,58 @@ +using MongoDB.Driver; +using System.Linq.Expressions; +using Modules.Library.Database.Database; +using Modules.Library.Domain.Entities; + +namespace Modules.Library.Database.Repositories; + +public abstract class RepositoryBase(MongoDbContext context) : IRepository where T : Entity +{ + protected abstract IMongoCollection GetCollections(MongoDbContext context); + protected bool _useSoftDelete = true; + + public async Task AddAsync(T entity) + { + await GetCollections(context).InsertOneAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(T entity) + { + if (!_useSoftDelete) + { + var document = await GetCollections(context).FindOneAndDeleteAsync(q => q.Id == entity.Id); + return document != null; + } + else return await SoftDeleteAsync(entity); + } + + protected virtual Task SoftDeleteAsync(T entity) => throw new NotImplementedException(); + + public async Task> GetAllAsync() => await GetCollections(context).Find("{}").ToListAsync(); + + public async Task GetByIdAsync(Guid id) => await GetCollections(context).Find(q => q.Id == id).SingleAsync(); + + public async Task GetByIdOrDefaultAsync(Guid id) => await GetCollections(context).Find(q => q.Id == id).SingleOrDefaultAsync(); + + public async Task GetFirstWhere(Expression> predicate) => + await GetCollections(context).Find(predicate).SingleAsync(); + + public async Task GetFirstOrDefaultWhere(Expression> predicate) => + await GetCollections(context).Find(predicate).SingleOrDefaultAsync(); + + public async Task> GetRangeByIdsAsync(List ids) => + await GetCollections(context).Find(q => ids.Contains(q.Id)).ToListAsync(); + + public async Task> GetWhere(Expression> predicate) => + await GetCollections(context).Find(predicate).ToListAsync(); + + public async Task AnyWhere(Expression> predicate) => + await GetCollections(context).Find(predicate).AnyAsync(); + + public async Task UpdateAsync(T entity) + { + //var document = await _context.PaymentCollections.FindOneAndReplaceAsync(q => q.Id == entity.Id, entity, new () { ReturnDocument = ReturnDocument.After }); + var document = await GetCollections(context).FindOneAndReplaceAsync(q => q.Id == entity.Id, entity); + return document != null; + } +} \ No newline at end of file diff --git a/Modules.Library.Database/ServiceCollectionExtensions.cs b/Modules.Library.Database/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..1cacb01 --- /dev/null +++ b/Modules.Library.Database/ServiceCollectionExtensions.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.DependencyInjection; +using Modules.Library.Application.Gateways; +using Modules.Library.Database.Database; +using Modules.Library.Database.Repositories; +using Modules.Library.Domain.Gateways; +using MongoDB.Driver; + +namespace Modules.Library.Database; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddDatabase(this IServiceCollection services, string connectionString) + { + AddMongoDb(services, connectionString); + services.AddScoped(q => + { + var context = new MongoDbContext(q.GetService()); + context.Initialize(); + return context; + }); + AddRepositories(services); + return services; + } + + public static IServiceCollection AddDatabase(this IServiceCollection services, string connectionString, string? databaseName) + { + AddMongoDb(services, connectionString, databaseName); + //services.AddScoped(); + services.AddScoped(q => + { + var context = new MongoDbContext(q.GetService()); + context.Initialize(); + return context; + }); + AddRepositories(services); + return services; + } + + private static void AddRepositories(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + private static void AddMongoDb(this IServiceCollection services, string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentNullException(nameof(connectionString)); + services.AddSingleton(new MongoClient(connectionString)); + } + + private static void AddMongoDb(IServiceCollection services, string? connectionString, string? databaseName) + { + if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentNullException(nameof(connectionString)); + if (string.IsNullOrWhiteSpace(databaseName)) throw new ArgumentNullException(nameof(databaseName)); + services.AddSingleton(new MongoClient(connectionString).GetDatabase(databaseName)); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/Entity.cs b/Modules.Library.Domain/Entities/Entity.cs new file mode 100644 index 0000000..250b817 --- /dev/null +++ b/Modules.Library.Domain/Entities/Entity.cs @@ -0,0 +1,53 @@ +namespace Modules.Library.Domain.Entities; + +public abstract class Entity +{ + private int? _requestedHashCode; + + public virtual Guid Id { get; protected set; } = Guid.NewGuid(); + + public virtual bool Deleted { get; protected set; } + + internal virtual void SetDeleted() => Deleted = true; + + public bool IsTransient() => Id == default; + + public override bool Equals(object? obj) + { + if (obj == null || obj is not Entity) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (GetType() != obj.GetType()) + return false; + + Entity item = (Entity)obj; + + if (item.IsTransient() || IsTransient()) + return false; + else + return item.Id == Id; + } + + public override int GetHashCode() + { + if (!IsTransient()) + { + if (!_requestedHashCode.HasValue) + _requestedHashCode = Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) + return _requestedHashCode.Value; + } + else + return base.GetHashCode(); + } + + public static bool operator ==(Entity? left, Entity? right) => + Equals(left, null) ? Equals(right, null) : left.Equals(right); + + public static bool operator !=(Entity? left, Entity? right) + { + return !(left == right); + } +} diff --git a/Modules.Library.Domain/Entities/Genre/Genre.cs b/Modules.Library.Domain/Entities/Genre/Genre.cs new file mode 100644 index 0000000..57484f1 --- /dev/null +++ b/Modules.Library.Domain/Entities/Genre/Genre.cs @@ -0,0 +1,11 @@ +namespace Modules.Library.Domain.Entities.Genre; + +public class Genre : Entity +{ + [Required] + public string Name { get; private set; } = default!; + + //private Genre() { } + internal Genre(string name) { } + public void SetName(string name) => Name = name; +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/IOrderableItem.cs b/Modules.Library.Domain/Entities/IOrderableItem.cs new file mode 100644 index 0000000..aa2943f --- /dev/null +++ b/Modules.Library.Domain/Entities/IOrderableItem.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Domain.Entities; + +public interface IOrderableItem +{ + public ushort Order { get; } + + public void SetOrder(ushort order); +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/Language/Language.cs b/Modules.Library.Domain/Entities/Language/Language.cs new file mode 100644 index 0000000..f927f0d --- /dev/null +++ b/Modules.Library.Domain/Entities/Language/Language.cs @@ -0,0 +1,21 @@ +namespace Modules.Library.Domain.Entities.Language; + +public class Language : Entity +{ + [Required] + public string CodeIso2 { get; private set; } = default!; + [Required] + public string Name { get; private set; } = default!; + public Guid? IconId { get; private set; } + + private Language() { } + internal Language(string codeIso2, string name, Guid? iconId) + { + CodeIso2 = codeIso2; + Name = name; + IconId = iconId; + } + + internal void SetName(string name) => Name = name; + internal void SetIcon(Guid? iconId) => IconId = iconId; +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/CommonProperties/CommonProperties.cs b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/CommonProperties.cs new file mode 100644 index 0000000..967ffba --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/CommonProperties.cs @@ -0,0 +1,22 @@ +namespace Modules.Library.Domain.Entities.MediaContent.CommonProperties; + +public class CommonProperties : Entity +{ + public List Names { get; set; } = []; + public MediaInfo? Preview { get; set; } + + public List Descriptions { get; set; } = []; + public List Genres { get; set; } = []; + public List RelatedContent { get; set; } = []; + + public DateTimeOffset? AnnouncementDate { get; internal set; } + public DateTimeOffset? EstimatedReleaseDate { get; internal set; } + public DateTimeOffset? ReleaseDate { get; internal set; } + + internal CommonProperties() { } + + internal CommonProperties(string nameOriginal, Guid nameOriginalLanguageId) + { + Names.Add(new NameItem(nameOriginalLanguageId, NameType.Original, nameOriginal)); + } +} diff --git a/Modules.Library.Domain/Entities/MediaContent/CommonProperties/DescriptionItem.cs b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/DescriptionItem.cs new file mode 100644 index 0000000..18fc688 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/DescriptionItem.cs @@ -0,0 +1,18 @@ +namespace Modules.Library.Domain.Entities.MediaContent.CommonProperties; +public class DescriptionItem : Entity +{ + [Required] + public string Value { get; internal set; } = string.Empty; + public bool IsOriginal { get; init; } + [Required] + public Guid LanguageId { get; init; } = default!; + + internal DescriptionItem() { } + + internal DescriptionItem(Guid languageId, bool isOriginal, string value) + { + LanguageId = languageId; + IsOriginal = isOriginal; + Value = value; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/CommonProperties/GenreProportionItem.cs b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/GenreProportionItem.cs new file mode 100644 index 0000000..87ff647 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/GenreProportionItem.cs @@ -0,0 +1,16 @@ +namespace Modules.Library.Domain.Entities.MediaContent.CommonProperties; + +public class GenreProportionItem : Entity +{ + public decimal? Proportion { get; internal set; } + [Required] + public Guid GenreId { get; init; } = default!; + + private GenreProportionItem() { } + + internal GenreProportionItem(Guid genreId, decimal? proportion) + { + GenreId = genreId; + Proportion = proportion; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/CommonProperties/NameItem.cs b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/NameItem.cs new file mode 100644 index 0000000..f887dc0 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/NameItem.cs @@ -0,0 +1,32 @@ +namespace Modules.Library.Domain.Entities.MediaContent.CommonProperties; + +public class NameItem : Entity +{ + [Required] + public string Value { get; set; } = string.Empty; + public NameType Type { get; set; } + [Required] + public Guid LanguageId { get; set; } = default!; + + private NameItem() { } + + internal NameItem(Guid languageId, NameType type, string value) + { + LanguageId = languageId; + Type = type; + Value = value; + } + + public void SetValue(string value) + { + Value = value; + } +} + +public enum NameType +{ + Original, + OriginalInAnotherLanguage, + Translation, + Abbreviation, +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs new file mode 100644 index 0000000..00da1f1 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs @@ -0,0 +1,13 @@ +namespace Modules.Library.Domain.Entities.MediaContent.CommonProperties.ShadowEntities; + +internal class CommonPropertiesShadow +{ + internal List names = []; + internal MediaInfo? preview = null; + internal List descriptions = []; + internal List genres = []; + internal List relatedContent = []; + internal DateTimeOffset? announcementDate = null; + internal DateTimeOffset? estimatedReleaseDate = null; + internal DateTimeOffset? releaseDate = null; +} diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeEpisode.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeEpisode.cs new file mode 100644 index 0000000..69f1e74 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeEpisode.cs @@ -0,0 +1,19 @@ +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +public class AnimeEpisode : AnimeItemSingle +{ + public AnimeEpisodeType Type { get; set; } + public int? Number { get; set; } + + private AnimeEpisode() : base() { } + + internal AnimeEpisode(string nameOriginal, Guid nameOriginalLanguageId, AnimeEpisodeType type) : + base(nameOriginal, nameOriginalLanguageId) { Type = type; } +} + +public enum AnimeEpisodeType +{ + Regilar, + FullLength, + Ova, +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeItem.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeItem.cs new file mode 100644 index 0000000..2127258 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeItem.cs @@ -0,0 +1,17 @@ +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +public abstract class AnimeItem : Entity +{ + public CommonProperties.CommonProperties CommonProperties { get; set; } = new(); + public ushort Order { get; set; } + public ushort Variant { get; set; } + public bool Completed { get; set; } + public TimeSpan ExpirationTime { get; set; } + + internal AnimeItem() { } + + internal AnimeItem(string nameOriginal, Guid nameOriginalLanguageId) + { + CommonProperties = new(nameOriginal, nameOriginalLanguageId); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeItemSingle.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeItemSingle.cs new file mode 100644 index 0000000..d7383a6 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeItemSingle.cs @@ -0,0 +1,9 @@ +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +public abstract class AnimeItemSingle : AnimeItem +{ + public TimeSpan? Duration { get; set; } + + internal AnimeItemSingle() { } + internal AnimeItemSingle(string nameOriginal, Guid nameOriginalLanguageId) : base(nameOriginal, nameOriginalLanguageId) { } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeSeason.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeSeason.cs new file mode 100644 index 0000000..440081e --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeSeason.cs @@ -0,0 +1,14 @@ +using Modules.Library.Domain.Entities.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +public class AnimeSeason : AnimeItem +{ + public List Episodes { get; set; } = []; + + public int? Number { get; set; } + public string? Director { get; set; } + public string? OriginCountry { get; set; } + + internal AnimeSeason(string nameOriginal, Guid nameOriginalLanguageId) : base(nameOriginal, nameOriginalLanguageId) { } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeTitle.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeTitle.cs new file mode 100644 index 0000000..b2eac90 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/AnimeTitle.cs @@ -0,0 +1,177 @@ +using Modules.Library.Domain.Entities.MediaContent.CommonProperties; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +public class AnimeTitle : Entity +{ + public CommonProperties.CommonProperties CommonProperties { get; private set; } = new(); + + /* + private List _items = []; + public IReadOnlyList Items => _items.AsReadOnly(); + */ + public List Items { get; set; } = []; + + public bool Completed { get; internal set; } + + public TimeSpan ExpirationTime { get; set; } + + private AnimeTitle() { } + + internal AnimeTitle(string nameOriginal, Guid nameOriginalLanguageId) + { + CommonProperties.Names.Add(new NameItem(nameOriginalLanguageId, NameType.Original, nameOriginal)); + } + + private AnimeItem ConvertToAnimeItem(AnimeItemShadow shadow) + { + if (shadow is AnimeEpisodeShadow episode) return new AnimeEpisode(this, null, episode); + else if (shadow is AnimeSeasonShadow season) return new AnimeSeason(this, season); + else throw new NotImplementedException(); + } + + + public void AddEpisode(AnimeEpisodeType episodeType) + { + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = AnimeItem.GetNewOrder(_shadow.items), + number = _shadow.items.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + public void AddEpisodeAsVariant(AnimeEpisodeType episodeType, ushort order) + { + if (order < 0 || order > _shadow.items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = AnimeItem.GetNewVariant(_shadow.items, order), + number = _shadow.items.OfType().FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + public void RemoveEpisode(AnimeEpisode episode) + { + episode.SetDeleted(); + AnimeItem.Sort(_shadow.items.Where(q => !q.deleted)); + CheckIsCompleted(); + } + + internal void SetEpisodeOrder(ushort currentOrder, ushort newOrder) => + AnimeItem.SetItemOrder(_shadow.items, currentOrder, newOrder); + + internal void SetEpisodeVariant(ushort order, ushort currentVariant, ushort newVariant) => + AnimeItem.SetItemVariant(_shadow.items.OfType() + .Where(q => q.order == order), currentVariant, newVariant); + + internal void SetEpisodeCompleted(ushort order, ushort variant, bool value = true) => + SetCompleted(order, variant, value); + + internal void SetEpisodeExpirationTime(ushort order, ushort variant, TimeSpan value) => + SetExpirationTime(order, variant, value); + + + + public void AddSeason() + { + var episode = new AnimeSeasonShadow + { + order = AnimeItem.GetNewOrder(_shadow.items), + number = _shadow.items.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + /* + public void AddSeasonAsVariant(AnimeEpisodeType episodeType, ushort order) + { + if (order < 0 || order > _shadow.items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = AnimeItem.GetNewVariant(_shadow.items, order), + number = _shadow.items.OfType().FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + */ + + public void RemoveSeason(AnimeSeason season) + { + season.SetDeleted(); + AnimeItem.Sort(_shadow.items.Where(q => !q.deleted)); + CheckIsCompleted(); + } + + internal void SetSeasonCompleted(ushort order, ushort variant, bool value = true) => + SetCompleted(order, variant, value); + + internal void SetSeasonExpirationTime(ushort order, ushort variant, TimeSpan value) => + SetExpirationTime(order, variant, value); + + private void SetCompleted(ushort order, ushort variant, bool value = true) where T : AnimeItemShadow + { + var item = GetItem(order, variant); + item.completed = value; + CheckIsCompleted(); + } + + private void SetExpirationTime(ushort order, ushort variant, TimeSpan value) where T : AnimeItemShadow + { + var item = GetItem(order, variant); + item.expirationTime = value; + CheckIsCompleted(); + } + + private T GetItem(ushort order, ushort variant) where T : AnimeItemShadow + { + var type = typeof(T); + var itemName = string.Empty; + if (type == typeof(AnimeEpisodeShadow)) itemName = "Episode"; + else if (type == typeof(AnimeSeason)) itemName = "Season"; + else throw new NotImplementedException(); + return _shadow.items.OfType() + .FirstOrDefault(q => q.order == order && q.variant == variant) ?? + throw new Exception(string.Concat(itemName, " not found")); + } + + public void SetCompleted() => _shadow.completed = true; + + public void SetNotCompleted() => _shadow.completed = false; + + public void SetExpirationTime(TimeSpan value) + { + _shadow.expirationTime = value; + CheckIsCompleted(); + } + + public void Delete() => SetDeleted(); + internal override void SetDeleted() => _shadow.deleted = true; + + internal void CheckIsCompleted() + { + var itemsQuery = Items.AsQueryable(); + var ucompletedSeasons = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + if (ucompletedSeasons || lastEpisodeReleaseDate >= DateTime.UtcNow - ExpirationTime) SetNotCompleted(); + else SetCompleted(); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs new file mode 100644 index 0000000..0c52c6e --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs @@ -0,0 +1,12 @@ +using Modules.Library.Domain.Entities.MediaContent.CommonProperties.ShadowEntities; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeEpisodeShadow : AnimeItemSingleShadow +{ + internal AnimeEpisodeType type = AnimeEpisodeType.Regilar; + internal int? number = null; + + internal AnimeEpisodeShadow() : base(new CommonPropertiesShadow()) { } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs new file mode 100644 index 0000000..d680499 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs @@ -0,0 +1,14 @@ +using Modules.Library.Domain.Entities.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime.ShadowEntities; + +internal abstract class AnimeItemShadow(CommonPropertiesShadow commonProperties) +{ + internal string id = Guid.NewGuid().ToString(); + internal CommonPropertiesShadow commonProperties = commonProperties; + internal ushort order = 0; + internal ushort variant = 0; + internal bool completed = false; + internal TimeSpan expirationTime = TimeSpan.Zero; + internal bool deleted = false; +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs new file mode 100644 index 0000000..92f3eeb --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs @@ -0,0 +1,8 @@ +using Modules.Library.Domain.Entities.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime.ShadowEntities; + +internal abstract class AnimeItemSingleShadow(CommonPropertiesShadow commonProperties) : AnimeItemShadow(commonProperties) +{ + internal TimeSpan? duration = null; +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs new file mode 100644 index 0000000..c4b1026 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs @@ -0,0 +1,15 @@ +using Modules.Library.Domain.Entities.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeSeasonShadow : AnimeItemShadow +{ + internal List episodes => []; + internal int? number = null; + internal string? director = null; + internal string? originCountry = null; + + internal AnimeSeasonShadow() : base(new CommonPropertiesShadow()) + { + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs new file mode 100644 index 0000000..26e6629 --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs @@ -0,0 +1,13 @@ +using Modules.Library.Domain.Entities.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.Entities.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeTitleShadow(CommonPropertiesShadow commonPropertiesShadow) +{ + internal string id = Guid.NewGuid().ToString(); + internal CommonPropertiesShadow commonProperties = commonPropertiesShadow; + internal List items = []; + internal bool completed = false; + internal TimeSpan expirationTime = TimeSpan.Zero; + internal bool deleted = false; +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaInfo.cs b/Modules.Library.Domain/Entities/MediaInfo.cs new file mode 100644 index 0000000..b1228cc --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaInfo.cs @@ -0,0 +1,15 @@ +namespace Modules.Library.Domain.Entities; +public class MediaInfo : Entity +{ + public MediaInfoType Type { get; internal set; } = MediaInfoType.OtherFile; + //public string ContentType { get; set; } = default!; + public string Url { get; internal set; } = default!; + + internal MediaInfo() { } + + internal MediaInfo(string url, MediaInfoType type) + { + Url = url; + Type = type; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Entities/MediaInfoType.cs b/Modules.Library.Domain/Entities/MediaInfoType.cs new file mode 100644 index 0000000..be1b2cf --- /dev/null +++ b/Modules.Library.Domain/Entities/MediaInfoType.cs @@ -0,0 +1,9 @@ +namespace Modules.Library.Domain.Entities; + +public enum MediaInfoType +{ + Image, + Video, + Link, + OtherFile +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/Entity.cs b/Modules.Library.Domain/EntitiesV0/Entity.cs new file mode 100644 index 0000000..f2e3fc3 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/Entity.cs @@ -0,0 +1,53 @@ +namespace Modules.Library.Domain.EntitiesV0; + +public abstract class Entity +{ + private int? _requestedHashCode; + + public virtual string Id { get; protected set; } = Guid.NewGuid().ToString(); + + public virtual bool Deleted { get; protected set; } + + internal virtual void SetDeleted() => Deleted = true; + + public bool IsTransient() => Id == default; + + public override bool Equals(object? obj) + { + if (obj == null || obj is not Entity) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (GetType() != obj.GetType()) + return false; + + Entity item = (Entity)obj; + + if (item.IsTransient() || IsTransient()) + return false; + else + return item.Id == Id; + } + + public override int GetHashCode() + { + if (!IsTransient()) + { + if (!_requestedHashCode.HasValue) + _requestedHashCode = Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) + return _requestedHashCode.Value; + } + else + return base.GetHashCode(); + } + + public static bool operator ==(Entity? left, Entity? right) => + Equals(left, null) ? Equals(right, null) : left.Equals(right); + + public static bool operator !=(Entity? left, Entity? right) + { + return !(left == right); + } +} diff --git a/Modules.Library.Domain/EntitiesV0/Genre/Genre.cs b/Modules.Library.Domain/EntitiesV0/Genre/Genre.cs new file mode 100644 index 0000000..80c03a7 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/Genre/Genre.cs @@ -0,0 +1,12 @@ +namespace Modules.Library.Domain.EntitiesV0.Genre; + +public class Genre : Entity +{ + [Required] + public string Name { get; private set; } = default!; + private Genre() { } + + public static Genre New(string name) => new(){ Name = name }; + + public void SetName(string name) => Name = name; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/IOrderableItem.cs b/Modules.Library.Domain/EntitiesV0/IOrderableItem.cs new file mode 100644 index 0000000..55085fe --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/IOrderableItem.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Domain.EntitiesV0; + +public interface IOrderableItem +{ + public ushort Order { get; } + + public void SetOrder(ushort order); +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/Language/Language.cs b/Modules.Library.Domain/EntitiesV0/Language/Language.cs new file mode 100644 index 0000000..4a3b9ec --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/Language/Language.cs @@ -0,0 +1,18 @@ +namespace Modules.Library.Domain.EntitiesV0.Language; + +public class Language : Entity +{ + [Required] + public string CodeIso2 { get; private set; } = default!; + [Required] + public string Name { get; private set; } = default!; + public Guid? IconId { get; private set; } + + private Language() { } + + internal static Language New(string codeIso2, string name, Guid? iconId) => + new() { CodeIso2 = codeIso2, Name = name, IconId = iconId }; + + internal void SetName(string name) => Name = name; + internal void SetIcon(Guid? iconId) => IconId = iconId; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/CommonProperties.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/CommonProperties.cs new file mode 100644 index 0000000..1b5ecfe --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/CommonProperties.cs @@ -0,0 +1,240 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; + +public class CommonProperties : Entity +{ + private readonly CommonPropertiesShadow _shadow; + public IReadOnlyCollection Names => _shadow.names.AsReadOnly(); + public MediaInfo? Preview => _shadow.preview; + + public IReadOnlyCollection Descriptions => _shadow.descriptions.AsReadOnly(); + public IReadOnlyCollection Genres => _shadow.genres.AsReadOnly(); + public IReadOnlyCollection RelatedContent => _shadow.relatedContent.AsReadOnly(); + + public DateTimeOffset? AnnouncementDate => _shadow.announcementDate; + internal Action? AfterAnnouncementDateSet; + public DateTimeOffset? EstimatedReleaseDate => _shadow.estimatedReleaseDate; + internal Action? AfterEstimatedReleaseDateSet; + public DateTimeOffset? ReleaseDate => _shadow.releaseDate; + internal Action? AfterReleaseDateSet; + + public override bool Deleted => false; + + internal CommonProperties(CommonPropertiesShadow shadow, Action? afterAnnouncementDateSet, Action? afterEstimatedReleaseDateSet, Action? afterReleaseDateSet) + { + _shadow = shadow; + AfterAnnouncementDateSet = afterAnnouncementDateSet; + AfterEstimatedReleaseDateSet = afterEstimatedReleaseDateSet; + AfterReleaseDateSet = afterReleaseDateSet; + } + + + #region Name + internal void AddNameOriginal(string languageId, string value) + { + if (_shadow.names.Any(q => q.Type == NameType.Original)) throw new Exception("Original name is already exist."); + var name = new NameItem(languageId, NameType.Original, value); + AddName(name, value); + } + + internal void AddNameOriginalInAnotherLanguage(string languageId, string value) + { + if (languageId == _shadow.names.Single(q => q.Type == NameType.Original).LanguageId) + throw new Exception("Language must not match original name language"); + + if (_shadow.names.Any(q => q.Type == NameType.OriginalInAnotherLanguage && q.LanguageId == languageId)) + throw new Exception("Name in following language is already exist."); + + var name = new NameItem(languageId, NameType.OriginalInAnotherLanguage); + AddName(name, value); + } + internal void AddNameTranslation(string languageId, string value) + { + var name = new NameItem(languageId, NameType.Translation); + AddName(name, value); + } + internal void AddNameAbbreviation(string languageId, string value) + { + var name = new NameItem(languageId, NameType.Abbreviation); + AddName(name, value); + } + + private void AddName(NameItem nameItem, string value) + { + _shadow.names.Add(nameItem); + SetNameValue(nameItem, value); + } + + internal void RemoveName(string nameId) + { + var name = GetName(nameId); + if (name.Type == NameType.Original) throw new Exception($"Unable to remove original name"); + name.SetDeleted(); + //_names.Remove(name); + } + + internal void SetNameValue(string nameId, string value) => SetNameValue(GetName(nameId), value); + + private void SetNameValue(NameItem nameItem, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + if (nameItem.Type != NameType.Original && _shadow.names.Any(q => q.LanguageId == nameItem.LanguageId && q.Value == value)) + throw new Exception("Name item with in same language with same value is already exists"); + nameItem.SetValue(value); + } + + private NameItem GetName(string nameId) => _shadow.names.FirstOrDefault(q => q.Id == nameId) ?? + throw new Exception($"Name with id [{nameId}] is not found"); + #endregion + + internal void SetPreview(string url, MediaInfoType type) + { + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); + _shadow.preview ??= new MediaInfo(url, type); + _shadow.preview.SetUrl(url); + } + internal void DeletePreview() + { + _shadow.preview = null; + } + + #region Description + internal void AddOriginalDescription(string languageId, string value) + { + if (_shadow.descriptions.Any(q => q.IsOriginal)) throw new Exception("Original description is already exist."); + var description = new DescriptionItem(languageId, true); + _shadow.descriptions.Add(description); + SetDescriptionValue(description, value); + } + + internal void AddNotOriginalDescription(string languageId, string value) + { + var description = new DescriptionItem(languageId, false); + _shadow.descriptions.Add(description); + SetDescriptionValue(description, value); + } + + internal void RemoveDescription(string descriptionId) + { + var description = GetDescription(descriptionId); + if (description.IsOriginal) throw new Exception($"Unable to remove original description"); + //_descriptions.Remove(name); + description.SetDeleted(); + } + + internal void SetDescriptionValue(string descriptionId, string value) + { + var name = GetDescription(descriptionId); + SetDescriptionValue(name, value); + } + + private void SetDescriptionValue(DescriptionItem descriptionItem, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + if (!descriptionItem.IsOriginal && _shadow.descriptions.Any(q => q.LanguageId == descriptionItem.LanguageId && q.Value == value)) + throw new Exception("Descriptoin item with with same value is already exists"); + descriptionItem.SetValue(value); + } + + private DescriptionItem GetDescription(string descriptionId) + { + return _shadow.descriptions.FirstOrDefault(q => q.Id == descriptionId) ?? + throw new Exception($"Description with id [{descriptionId}] is not found"); + } + #endregion + + #region Genre + internal void AddGenre(string genreId, decimal? proportion = null) + { + if (_shadow.genres.Any(q => q.GenreId == genreId)) throw new Exception("Genre is already in the list"); + var genreProportionItem = new GenreProportionItem(genreId); + _shadow.genres.Add(genreProportionItem); + if (proportion.HasValue) genreProportionItem.SetProportion(proportion); + } + + internal void SetGenreProportion(string genreProportionItemId, decimal? value = null) + { + var genreProportionItem = GetGenreProportionItem(genreProportionItemId); + genreProportionItem.SetProportion(value); + } + + internal void RemoveGenre(string genreProportionItemId) + { + var genreProportionItem = GetGenreProportionItem(genreProportionItemId); + //_genres.Remove(genreProportionItem); + genreProportionItem.SetDeleted(); + } + + private GenreProportionItem GetGenreProportionItem(string genreProportionItemId) + { + return _shadow.genres.FirstOrDefault(q => q.Id == genreProportionItemId) ?? + throw new Exception($"Genre proportion item with id [{genreProportionItemId}] is not found"); + } +#endregion + + #region RelatedContent + + internal void AddRelatedContent(string url, MediaInfoType type) + { + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); + CheckIfCanAddOrEdit(url, type); + _shadow.relatedContent.Add(new MediaInfo(url, type)); + } + + internal void EditRelatedContent(string relatedContentId, string url, MediaInfoType type) + { + var relatedContent = GetRelatedContent(relatedContentId); + CheckIfCanAddOrEdit(url, type); + relatedContent.SetUrl(url); + relatedContent.SetType(type); + } + + internal void RemoveRelatedContent(string relatedContentId) + { + var relatedContent = GetRelatedContent(relatedContentId); + //_relatedContent.Remove(relatedContent); + relatedContent.SetDeleted(); + } + + private void CheckIfCanAddOrEdit(string url, MediaInfoType type) + { + if (_shadow.relatedContent.Any(q => q.Url == url && q.Type == type)) + throw new Exception("Related content with same url and same type is already exists"); + } + + + private MediaInfo GetRelatedContent(string relatedContentId) + { + return _shadow.relatedContent.FirstOrDefault(q => q.Id == relatedContentId) ?? + throw new Exception($"Related content with id [{relatedContentId}] is not found"); + } + #endregion + + + internal void SetAnnouncementDate(DateTimeOffset value) + { + if (value == default) throw new ArgumentNullException(nameof(value)); + _shadow.announcementDate = value; + AfterAnnouncementDateSet?.Invoke(); + } + + internal void SetEstimatedReleaseDate(DateTimeOffset value) + { + if (value == default) throw new ArgumentNullException(nameof(value)); + if (_shadow.announcementDate.HasValue && value <= _shadow.announcementDate.Value) + throw new Exception("Estimated release date can not be less or equal to announcement date"); + _shadow.estimatedReleaseDate = value; + AfterEstimatedReleaseDateSet?.Invoke(); + } + internal void SetReleaseDate(DateTimeOffset value) + { + if (value == default) throw new ArgumentNullException(nameof(value)); + if (_shadow.announcementDate.HasValue && value <= _shadow.announcementDate.Value) + throw new Exception("Release date can not be less or equal to announcement date"); + _shadow.releaseDate = value; + AfterReleaseDateSet?.Invoke(); + } + + internal override void SetDeleted() { } +} diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/DescriptionItem.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/DescriptionItem.cs new file mode 100644 index 0000000..4a57b10 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/DescriptionItem.cs @@ -0,0 +1,27 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; +public class DescriptionItem : Entity +{ + [Required] + public string Value { get; private set; } = string.Empty; + public bool IsOriginal { get; init; } + [Required] + public string LanguageId { get; init; } = default!; + + internal DescriptionItem(string languageId, bool isOriginal) + { + LanguageId = languageId; + IsOriginal = isOriginal; + } + + internal DescriptionItem(string languageId, bool isOriginal, string value) + { + LanguageId = languageId; + IsOriginal = isOriginal; + Value = value; + } + + public void SetValue(string value) + { + Value = value; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/GenreProportionItem.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/GenreProportionItem.cs new file mode 100644 index 0000000..64e3630 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/GenreProportionItem.cs @@ -0,0 +1,21 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; + +public class GenreProportionItem : Entity +{ + public decimal? Proportion { get; private set; } + [Required] + public string GenreId { get; init; } = default!; + + internal GenreProportionItem(string genreId) { GenreId = genreId; } + + internal GenreProportionItem(string genreId, decimal? proportion) + { + GenreId = genreId; + Proportion = proportion; + } + + public void SetProportion(decimal? proportion) + { + Proportion = proportion; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/NameItem.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/NameItem.cs new file mode 100644 index 0000000..2a3e4a3 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/NameItem.cs @@ -0,0 +1,36 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; + +public class NameItem : Entity +{ + [Required] + public string Value { get; private set; } = string.Empty; + public NameType Type { get; init; } + [Required] + public string LanguageId { get; init; } = default!; + + internal NameItem(string languageId, NameType type) + { + LanguageId = languageId; + Type = type; + } + + internal NameItem(string languageId, NameType type, string value) + { + LanguageId = languageId; + Type = type; + Value = value; + } + + public void SetValue(string value) + { + Value = value; + } +} + +public enum NameType +{ + Original, + OriginalInAnotherLanguage, + Translation, + Abbreviation, +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs new file mode 100644 index 0000000..f76717a --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs @@ -0,0 +1,13 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +internal class CommonPropertiesShadow +{ + internal List names = []; + internal MediaInfo? preview = null; + internal List descriptions = []; + internal List genres = []; + internal List relatedContent = []; + internal DateTimeOffset? announcementDate = null; + internal DateTimeOffset? estimatedReleaseDate = null; + internal DateTimeOffset? releaseDate = null; +} diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeEpisode.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeEpisode.cs new file mode 100644 index 0000000..504a89d --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeEpisode.cs @@ -0,0 +1,65 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public class AnimeEpisode : AnimeItemSingle +{ + private readonly AnimeTitle _title; + private readonly AnimeSeason? _season; + private readonly AnimeEpisodeShadow _shadow; + + public AnimeEpisodeType Type => _shadow.type; + public int? Number => _shadow.number; + + internal AnimeEpisode(AnimeTitle title, AnimeSeason? season, AnimeEpisodeShadow shadow) : base(shadow) + { + _title = title; + _season = season; + _shadow = shadow; + } + + public void SetNumber(int? number) => _shadow.number = number; + + public override void SetOrder(ushort order) + { + if (_season == null) SetItemOrder(_title._shadow.items, Order, order); + else _season.SetEpisodeOrder(Order, order); + } + + public override void SetVariant(ushort variant) + { + if (_season == null) + { + SetItemVariant(_title._shadow.items.OfType() + .Where(q => q.order == Order), Variant, variant); + } + else _season.SetEpisodeVariant(Order, Variant, variant); + } + + public override void SetCompleted() + { + if (_season == null) _title.SetEpisodeCompleted(Order, Variant, true); + else _season.SetEpisodeCompleted(Order, Variant, true); + } + + public override void SetNotCompleted() + { + if (_season == null) _title.SetEpisodeCompleted(Order, Variant, false); + else _season.SetEpisodeCompleted(Order, Variant, false); + } + + public override void SetExpirationTime(TimeSpan value) + { + if (_season == null) _title.SetEpisodeExpirationTime(Order, Variant, value); + else _season.SetEpisodeExpirationTime(Order, Variant, value); + } + + internal void SetDeleted() => _shadow.deleted = true; +} + +public enum AnimeEpisodeType +{ + Regilar, + FullLength, + Ova, +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeItem.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeItem.cs new file mode 100644 index 0000000..c3decec --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeItem.cs @@ -0,0 +1,89 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public abstract class AnimeItem : Entity +{ + private readonly AnimeItemShadow _shadow; + + public override string Id => _shadow.id; + + public CommonProperties.CommonProperties CommonProperties => GetCommonProperties(); + public ushort Order => _shadow.order; + public ushort Variant => _shadow.variant; + public bool Completed => _shadow.completed; + public override bool Deleted => _shadow.deleted; + public TimeSpan ExpirationTime => _shadow.expirationTime; + + internal AnimeItem(AnimeItemShadow shadow) { _shadow = shadow; } + + /* + private CommonProperties.CommonProperties GetCommonProperties() + { + new (shadow.commonProperties, afterAnnouncementDateSet, afterEstimatedReleaseDateSet, afterReleaseDateSet) + } + */ + + protected virtual CommonProperties.CommonProperties GetCommonProperties() => + new(_shadow.commonProperties, null, null, null); + + public abstract void SetOrder(ushort order); + internal static ushort GetNewOrder(IEnumerable items) => + (ushort)(items.Select(q => (int)q.order).DefaultIfEmpty(-1).Max() + 1); + + public abstract void SetVariant(ushort variant); + internal static ushort GetNewVariant(IEnumerable items, ushort order) => + (ushort)(items.Where(q => q.order == order) + .Select(q => (int)q.variant).DefaultIfEmpty(-1).Max() + 1); + + internal static void Sort(IEnumerable items) + { + var orderedItemGroups = items.GroupBy(q => q.order).Select(q => new { Order = q.Key, Items = items.Where(x => x.order == q.Key) }) + .OrderBy(q => q.Order).ToArray(); + for (int i = 0; i < orderedItemGroups.Length; i++) + { + foreach (var item in orderedItemGroups[i].Items) + { + item.order = (ushort)i; + } + } + } + + public abstract void SetCompleted(); + public abstract void SetNotCompleted(); + public abstract void SetExpirationTime(TimeSpan value); + + internal static void SetItemOrder(IEnumerable items, ushort lastOrder, ushort newOrder) + { + if (newOrder < 0 || newOrder > items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(newOrder)); + //if (lastOrder == newOrder) return; + var newOrderItem = items.First(q => q.order == newOrder); + items.First(q => q.order == lastOrder).order = newOrder; + foreach (var item in items.Where(q => q.order > Math.Min(lastOrder, newOrder) && q.order < Math.Max(lastOrder, newOrder))) + { + if (lastOrder > newOrder) item.order++; + else if (lastOrder < newOrder) item.order--; + } + if (lastOrder > newOrder) newOrderItem.order++; + else if (lastOrder < newOrder) newOrderItem.order--; + } + + internal static void SetItemVariant(IEnumerable items, ushort lastVariant, ushort newVariant) + { + if (newVariant < 0 || newVariant >= items.Count()) + throw new ArgumentOutOfRangeException(nameof(newVariant)); + //if (lastVariant == newVariant) return; + var newVariantItem = items.First(q => q.variant == newVariant); + items.First(q => q.variant == lastVariant).variant = newVariant; + foreach (var item in items.Where(q => q.variant > Math.Min(lastVariant, newVariant) && q.variant < Math.Max(lastVariant, newVariant))) + { + if (lastVariant > newVariant) item.variant++; + else if (lastVariant < newVariant) item.variant--; + } + if (lastVariant > newVariant) newVariantItem.variant++; + else if (lastVariant < newVariant) newVariantItem.variant--; + } + + internal override void SetDeleted() => _shadow.deleted = true; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeItemSingle.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeItemSingle.cs new file mode 100644 index 0000000..f17323c --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeItemSingle.cs @@ -0,0 +1,16 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public abstract class AnimeItemSingle : AnimeItem +{ + private readonly AnimeItemSingleShadow _shadow; + public TimeSpan? Duration => _shadow.duration; + + internal AnimeItemSingle(AnimeItemSingleShadow shadow) : base(shadow) + { + _shadow = shadow; + } + + public void SetDuration(TimeSpan value) => _shadow.duration = value; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeSeason.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeSeason.cs new file mode 100644 index 0000000..ad92861 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeSeason.cs @@ -0,0 +1,135 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public class AnimeSeason : AnimeItem +{ + private readonly AnimeTitle _title; + private readonly AnimeSeasonShadow _shadow; + + public IReadOnlyCollection Episodes => _shadow.episodes.Select(q => new AnimeEpisode(_title, this, q)).ToList().AsReadOnly(); + + public int? Number => _shadow.number; + public string? Director => _shadow.director; + public string? OriginCountry => _shadow.originCountry; + + internal AnimeSeason(AnimeTitle title, AnimeSeasonShadow shadow) : base(shadow) + { + _title = title; + _shadow = shadow; + } + + protected override CommonProperties.CommonProperties GetCommonProperties() => + new(_shadow.commonProperties, CheckIsCompleted, CheckIsCompleted, CheckIsCompleted); + + public void AddEpisode(AnimeEpisodeType episodeType) + { + StructureAction(() => + { + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = GetNewOrder(_shadow.episodes), + number = _shadow.episodes.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.episodes.Add(episode); + }); + } + + public void AddEpisodeAsVariant(AnimeEpisodeType episodeType, ushort order) + { + StructureAction(() => + { + if (order < 0 || order > _shadow.episodes.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = GetNewVariant(_shadow.episodes, order), + number = _shadow.episodes.FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.episodes.Add(episode); + }); + } + + public void RemoveEpisode(AnimeEpisode episode) + { + StructureAction(() => + { + episode.SetDeleted(); + Sort(_shadow.episodes.Where(q => !q.deleted)); + }); + } + + internal void SetEpisodeOrder(ushort currentOrder, ushort newOrder) => + SetItemOrder(_shadow.episodes, currentOrder, newOrder); + + internal void SetEpisodeVariant(ushort order, ushort currentVariant, ushort newVariant) => + SetItemVariant(_shadow.episodes.Where(q => q.order == order), currentVariant, newVariant); + + internal void SetEpisodeCompleted(ushort order, ushort variant, bool value = true) + { + StructureAction(() => + { + var episode = _shadow.episodes.FirstOrDefault(q => q.order == order && q.variant == variant) ?? + throw new Exception("Episode not found"); + episode.completed = value; + }); + } + + internal void SetEpisodeExpirationTime(ushort order, ushort variant, TimeSpan value) + { + StructureAction(() => + { + var episode = _shadow.episodes.FirstOrDefault(q => q.order == order && q.variant == variant) ?? + throw new Exception("Episode not found"); + episode.expirationTime = value; + }); + } + + private void StructureAction(Action action) + { + action(); + CheckIsCompleted(); + _title.CheckIsCompleted(); + //return new AnimeEpisode(title, this, episodeShadow); + } + + public override void SetOrder(ushort order) => SetItemOrder(_title._shadow.items, Order, order); + + public override void SetVariant(ushort variant) => throw new NotImplementedException(); + + public override void SetCompleted() + { + _title.SetSeasonCompleted(Order, Variant, true); + } + + public override void SetNotCompleted() + { + throw new NotImplementedException(); + } + + public override void SetExpirationTime(TimeSpan value) + { + throw new NotImplementedException(); + } + + internal void SetNumber(int? number) => _shadow.number = number; + internal void SetDirector(string? director) => _shadow.director = director; + internal void SetOriginCountry(string? originCountry) => _shadow.originCountry = originCountry; + + internal void CheckIsCompleted() + { + var itemsQuery = Episodes.Where(q => !q.Deleted).AsQueryable(); + var unreleasedEpisodesAreExists = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + //return unreleasedEpisodesAreExists || lastEpisodeReleaseDate - DateTime.UtcNow > expirePeriod; + if (unreleasedEpisodesAreExists || lastEpisodeReleaseDate >= DateTime.UtcNow - ExpirationTime) SetNotCompleted(); + else SetCompleted(); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeTitle.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeTitle.cs new file mode 100644 index 0000000..0177f83 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/AnimeTitle.cs @@ -0,0 +1,171 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public class AnimeTitle : Entity +{ + internal readonly AnimeTitleShadow _shadow; + + public override string Id => _shadow.id; + + public CommonProperties.CommonProperties CommonProperties => new(_shadow.commonProperties, CheckIsCompleted, CheckIsCompleted, CheckIsCompleted); + + public IReadOnlyCollection Items => _shadow.items.Select(ConvertToAnimeItem).ToList().AsReadOnly(); + + public bool Completed => _shadow.completed; + public override bool Deleted => _shadow.deleted; + public TimeSpan ExpirationTime => _shadow.expirationTime; + + internal AnimeTitle(AnimeTitleShadow shadow) => _shadow = shadow; + + private AnimeItem ConvertToAnimeItem(AnimeItemShadow shadow) + { + if (shadow is AnimeEpisodeShadow episode) return new AnimeEpisode(this, null, episode); + else if (shadow is AnimeSeasonShadow season) return new AnimeSeason(this, season); + else throw new NotImplementedException(); + } + + + public void AddEpisode(AnimeEpisodeType episodeType) + { + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = AnimeItem.GetNewOrder(_shadow.items), + number = _shadow.items.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + public void AddEpisodeAsVariant(AnimeEpisodeType episodeType, ushort order) + { + if (order < 0 || order > _shadow.items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = AnimeItem.GetNewVariant(_shadow.items, order), + number = _shadow.items.OfType().FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + public void RemoveEpisode(AnimeEpisode episode) + { + episode.SetDeleted(); + AnimeItem.Sort(_shadow.items.Where(q => !q.deleted)); + CheckIsCompleted(); + } + + internal void SetEpisodeOrder(ushort currentOrder, ushort newOrder) => + AnimeItem.SetItemOrder(_shadow.items, currentOrder, newOrder); + + internal void SetEpisodeVariant(ushort order, ushort currentVariant, ushort newVariant) => + AnimeItem.SetItemVariant(_shadow.items.OfType() + .Where(q => q.order == order), currentVariant, newVariant); + + internal void SetEpisodeCompleted(ushort order, ushort variant, bool value = true) => + SetCompleted(order, variant, value); + + internal void SetEpisodeExpirationTime(ushort order, ushort variant, TimeSpan value) => + SetExpirationTime(order, variant, value); + + + + public void AddSeason() + { + var episode = new AnimeSeasonShadow + { + order = AnimeItem.GetNewOrder(_shadow.items), + number = _shadow.items.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + /* + public void AddSeasonAsVariant(AnimeEpisodeType episodeType, ushort order) + { + if (order < 0 || order > _shadow.items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = AnimeItem.GetNewVariant(_shadow.items, order), + number = _shadow.items.OfType().FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + */ + + public void RemoveSeason(AnimeEpisode episode) + { + episode.SetDeleted(); + AnimeItem.Sort(_shadow.items.Where(q => !q.deleted)); + CheckIsCompleted(); + } + + internal void SetSeasonCompleted(ushort order, ushort variant, bool value = true) => + SetCompleted(order, variant, value); + + internal void SetSeasonExpirationTime(ushort order, ushort variant, TimeSpan value) => + SetExpirationTime(order, variant, value); + + private void SetCompleted(ushort order, ushort variant, bool value = true) where T : AnimeItemShadow + { + var item = GetItem(order, variant); + item.completed = value; + CheckIsCompleted(); + } + + private void SetExpirationTime(ushort order, ushort variant, TimeSpan value) where T : AnimeItemShadow + { + var item = GetItem(order, variant); + item.expirationTime = value; + CheckIsCompleted(); + } + + private T GetItem(ushort order, ushort variant) where T : AnimeItemShadow + { + var type = typeof(T); + var itemName = string.Empty; + if (type == typeof(AnimeEpisodeShadow)) itemName = "Episode"; + else if (type == typeof(AnimeSeason)) itemName = "Season"; + else throw new NotImplementedException(); + return _shadow.items.OfType() + .FirstOrDefault(q => q.order == order && q.variant == variant) ?? + throw new Exception(string.Concat(itemName, " not found")); + } + + public void SetCompleted() => _shadow.completed = true; + + public void SetNotCompleted() => _shadow.completed = false; + + public void SetExpirationTime(TimeSpan value) + { + _shadow.expirationTime = value; + CheckIsCompleted(); + } + + public void Delete() => SetDeleted(); + internal override void SetDeleted() => _shadow.deleted = true; + + internal void CheckIsCompleted() + { + var itemsQuery = Items.AsQueryable(); + var ucompletedSeasons = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + if (ucompletedSeasons || lastEpisodeReleaseDate >= DateTime.UtcNow - ExpirationTime) SetNotCompleted(); + else SetCompleted(); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs new file mode 100644 index 0000000..bdec4b5 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs @@ -0,0 +1,12 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeEpisodeShadow : AnimeItemSingleShadow +{ + internal AnimeEpisodeType type = AnimeEpisodeType.Regilar; + internal int? number = null; + + internal AnimeEpisodeShadow() : base(new CommonPropertiesShadow()) { } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs new file mode 100644 index 0000000..93cf7e5 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs @@ -0,0 +1,14 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal abstract class AnimeItemShadow(CommonPropertiesShadow commonProperties) +{ + internal string id = Guid.NewGuid().ToString(); + internal CommonPropertiesShadow commonProperties = commonProperties; + internal ushort order = 0; + internal ushort variant = 0; + internal bool completed = false; + internal TimeSpan expirationTime = TimeSpan.Zero; + internal bool deleted = false; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs new file mode 100644 index 0000000..fcc37ca --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs @@ -0,0 +1,8 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal abstract class AnimeItemSingleShadow(CommonPropertiesShadow commonProperties) : AnimeItemShadow(commonProperties) +{ + internal TimeSpan? duration = null; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs new file mode 100644 index 0000000..99356b6 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs @@ -0,0 +1,15 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeSeasonShadow : AnimeItemShadow +{ + internal List episodes => []; + internal int? number = null; + internal string? director = null; + internal string? originCountry = null; + + internal AnimeSeasonShadow() : base(new CommonPropertiesShadow()) + { + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs new file mode 100644 index 0000000..c266ca2 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs @@ -0,0 +1,13 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeTitleShadow(CommonPropertiesShadow commonPropertiesShadow) +{ + internal string id = Guid.NewGuid().ToString(); + internal CommonPropertiesShadow commonProperties = commonPropertiesShadow; + internal List items = []; + internal bool completed = false; + internal TimeSpan expirationTime = TimeSpan.Zero; + internal bool deleted = false; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaInfo.cs b/Modules.Library.Domain/EntitiesV0/MediaInfo.cs new file mode 100644 index 0000000..327afee --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaInfo.cs @@ -0,0 +1,22 @@ +namespace Modules.Library.Domain.EntitiesV0; +public class MediaInfo : Entity +{ + public MediaInfoType Type { get; private set; } = MediaInfoType.OtherFile; + //public string ContentType { get; set; } = default!; + public string Url { get; private set; } = default!; + + internal MediaInfo(string url, MediaInfoType type) + { + Url = url; + Type = type; + } + + internal void SetUrl(string value) + { + Url = value; + } + internal void SetType(MediaInfoType value) + { + Type = value; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV0/MediaInfoType.cs b/Modules.Library.Domain/EntitiesV0/MediaInfoType.cs new file mode 100644 index 0000000..0c6e95d --- /dev/null +++ b/Modules.Library.Domain/EntitiesV0/MediaInfoType.cs @@ -0,0 +1,9 @@ +namespace Modules.Library.Domain.EntitiesV0; + +public enum MediaInfoType +{ + Image, + Video, + Link, + OtherFile +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/Entity.cs b/Modules.Library.Domain/EntitiesV1/Entity.cs new file mode 100644 index 0000000..f18ec24 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/Entity.cs @@ -0,0 +1,53 @@ +namespace Modules.Library.Domain.EntitiesV1; + +public abstract class Entity +{ + private int? _requestedHashCode; + + public virtual string Id { get; protected set; } = Guid.NewGuid().ToString(); + + public virtual bool Deleted { get; protected set; } + + internal virtual void SetDeleted() => Deleted = true; + + public bool IsTransient() => Id == default; + + public override bool Equals(object? obj) + { + if (obj == null || obj is not Entity) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (GetType() != obj.GetType()) + return false; + + Entity item = (Entity)obj; + + if (item.IsTransient() || IsTransient()) + return false; + else + return item.Id == Id; + } + + public override int GetHashCode() + { + if (!IsTransient()) + { + if (!_requestedHashCode.HasValue) + _requestedHashCode = Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) + return _requestedHashCode.Value; + } + else + return base.GetHashCode(); + } + + public static bool operator ==(Entity? left, Entity? right) => + Equals(left, null) ? Equals(right, null) : left.Equals(right); + + public static bool operator !=(Entity? left, Entity? right) + { + return !(left == right); + } +} diff --git a/Modules.Library.Domain/EntitiesV1/Genre/Genre.cs b/Modules.Library.Domain/EntitiesV1/Genre/Genre.cs new file mode 100644 index 0000000..80c03a7 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/Genre/Genre.cs @@ -0,0 +1,12 @@ +namespace Modules.Library.Domain.EntitiesV0.Genre; + +public class Genre : Entity +{ + [Required] + public string Name { get; private set; } = default!; + private Genre() { } + + public static Genre New(string name) => new(){ Name = name }; + + public void SetName(string name) => Name = name; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/IOrderableItem.cs b/Modules.Library.Domain/EntitiesV1/IOrderableItem.cs new file mode 100644 index 0000000..38321d1 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/IOrderableItem.cs @@ -0,0 +1,8 @@ +namespace Modules.Library.Domain.EntitiesV1; + +public interface IOrderableItem +{ + public ushort Order { get; } + + public void SetOrder(ushort order); +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/Language/Language.cs b/Modules.Library.Domain/EntitiesV1/Language/Language.cs new file mode 100644 index 0000000..4a3b9ec --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/Language/Language.cs @@ -0,0 +1,18 @@ +namespace Modules.Library.Domain.EntitiesV0.Language; + +public class Language : Entity +{ + [Required] + public string CodeIso2 { get; private set; } = default!; + [Required] + public string Name { get; private set; } = default!; + public Guid? IconId { get; private set; } + + private Language() { } + + internal static Language New(string codeIso2, string name, Guid? iconId) => + new() { CodeIso2 = codeIso2, Name = name, IconId = iconId }; + + internal void SetName(string name) => Name = name; + internal void SetIcon(Guid? iconId) => IconId = iconId; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/CommonProperties.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/CommonProperties.cs new file mode 100644 index 0000000..1b5ecfe --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/CommonProperties.cs @@ -0,0 +1,240 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; + +public class CommonProperties : Entity +{ + private readonly CommonPropertiesShadow _shadow; + public IReadOnlyCollection Names => _shadow.names.AsReadOnly(); + public MediaInfo? Preview => _shadow.preview; + + public IReadOnlyCollection Descriptions => _shadow.descriptions.AsReadOnly(); + public IReadOnlyCollection Genres => _shadow.genres.AsReadOnly(); + public IReadOnlyCollection RelatedContent => _shadow.relatedContent.AsReadOnly(); + + public DateTimeOffset? AnnouncementDate => _shadow.announcementDate; + internal Action? AfterAnnouncementDateSet; + public DateTimeOffset? EstimatedReleaseDate => _shadow.estimatedReleaseDate; + internal Action? AfterEstimatedReleaseDateSet; + public DateTimeOffset? ReleaseDate => _shadow.releaseDate; + internal Action? AfterReleaseDateSet; + + public override bool Deleted => false; + + internal CommonProperties(CommonPropertiesShadow shadow, Action? afterAnnouncementDateSet, Action? afterEstimatedReleaseDateSet, Action? afterReleaseDateSet) + { + _shadow = shadow; + AfterAnnouncementDateSet = afterAnnouncementDateSet; + AfterEstimatedReleaseDateSet = afterEstimatedReleaseDateSet; + AfterReleaseDateSet = afterReleaseDateSet; + } + + + #region Name + internal void AddNameOriginal(string languageId, string value) + { + if (_shadow.names.Any(q => q.Type == NameType.Original)) throw new Exception("Original name is already exist."); + var name = new NameItem(languageId, NameType.Original, value); + AddName(name, value); + } + + internal void AddNameOriginalInAnotherLanguage(string languageId, string value) + { + if (languageId == _shadow.names.Single(q => q.Type == NameType.Original).LanguageId) + throw new Exception("Language must not match original name language"); + + if (_shadow.names.Any(q => q.Type == NameType.OriginalInAnotherLanguage && q.LanguageId == languageId)) + throw new Exception("Name in following language is already exist."); + + var name = new NameItem(languageId, NameType.OriginalInAnotherLanguage); + AddName(name, value); + } + internal void AddNameTranslation(string languageId, string value) + { + var name = new NameItem(languageId, NameType.Translation); + AddName(name, value); + } + internal void AddNameAbbreviation(string languageId, string value) + { + var name = new NameItem(languageId, NameType.Abbreviation); + AddName(name, value); + } + + private void AddName(NameItem nameItem, string value) + { + _shadow.names.Add(nameItem); + SetNameValue(nameItem, value); + } + + internal void RemoveName(string nameId) + { + var name = GetName(nameId); + if (name.Type == NameType.Original) throw new Exception($"Unable to remove original name"); + name.SetDeleted(); + //_names.Remove(name); + } + + internal void SetNameValue(string nameId, string value) => SetNameValue(GetName(nameId), value); + + private void SetNameValue(NameItem nameItem, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + if (nameItem.Type != NameType.Original && _shadow.names.Any(q => q.LanguageId == nameItem.LanguageId && q.Value == value)) + throw new Exception("Name item with in same language with same value is already exists"); + nameItem.SetValue(value); + } + + private NameItem GetName(string nameId) => _shadow.names.FirstOrDefault(q => q.Id == nameId) ?? + throw new Exception($"Name with id [{nameId}] is not found"); + #endregion + + internal void SetPreview(string url, MediaInfoType type) + { + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); + _shadow.preview ??= new MediaInfo(url, type); + _shadow.preview.SetUrl(url); + } + internal void DeletePreview() + { + _shadow.preview = null; + } + + #region Description + internal void AddOriginalDescription(string languageId, string value) + { + if (_shadow.descriptions.Any(q => q.IsOriginal)) throw new Exception("Original description is already exist."); + var description = new DescriptionItem(languageId, true); + _shadow.descriptions.Add(description); + SetDescriptionValue(description, value); + } + + internal void AddNotOriginalDescription(string languageId, string value) + { + var description = new DescriptionItem(languageId, false); + _shadow.descriptions.Add(description); + SetDescriptionValue(description, value); + } + + internal void RemoveDescription(string descriptionId) + { + var description = GetDescription(descriptionId); + if (description.IsOriginal) throw new Exception($"Unable to remove original description"); + //_descriptions.Remove(name); + description.SetDeleted(); + } + + internal void SetDescriptionValue(string descriptionId, string value) + { + var name = GetDescription(descriptionId); + SetDescriptionValue(name, value); + } + + private void SetDescriptionValue(DescriptionItem descriptionItem, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + if (!descriptionItem.IsOriginal && _shadow.descriptions.Any(q => q.LanguageId == descriptionItem.LanguageId && q.Value == value)) + throw new Exception("Descriptoin item with with same value is already exists"); + descriptionItem.SetValue(value); + } + + private DescriptionItem GetDescription(string descriptionId) + { + return _shadow.descriptions.FirstOrDefault(q => q.Id == descriptionId) ?? + throw new Exception($"Description with id [{descriptionId}] is not found"); + } + #endregion + + #region Genre + internal void AddGenre(string genreId, decimal? proportion = null) + { + if (_shadow.genres.Any(q => q.GenreId == genreId)) throw new Exception("Genre is already in the list"); + var genreProportionItem = new GenreProportionItem(genreId); + _shadow.genres.Add(genreProportionItem); + if (proportion.HasValue) genreProportionItem.SetProportion(proportion); + } + + internal void SetGenreProportion(string genreProportionItemId, decimal? value = null) + { + var genreProportionItem = GetGenreProportionItem(genreProportionItemId); + genreProportionItem.SetProportion(value); + } + + internal void RemoveGenre(string genreProportionItemId) + { + var genreProportionItem = GetGenreProportionItem(genreProportionItemId); + //_genres.Remove(genreProportionItem); + genreProportionItem.SetDeleted(); + } + + private GenreProportionItem GetGenreProportionItem(string genreProportionItemId) + { + return _shadow.genres.FirstOrDefault(q => q.Id == genreProportionItemId) ?? + throw new Exception($"Genre proportion item with id [{genreProportionItemId}] is not found"); + } +#endregion + + #region RelatedContent + + internal void AddRelatedContent(string url, MediaInfoType type) + { + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); + CheckIfCanAddOrEdit(url, type); + _shadow.relatedContent.Add(new MediaInfo(url, type)); + } + + internal void EditRelatedContent(string relatedContentId, string url, MediaInfoType type) + { + var relatedContent = GetRelatedContent(relatedContentId); + CheckIfCanAddOrEdit(url, type); + relatedContent.SetUrl(url); + relatedContent.SetType(type); + } + + internal void RemoveRelatedContent(string relatedContentId) + { + var relatedContent = GetRelatedContent(relatedContentId); + //_relatedContent.Remove(relatedContent); + relatedContent.SetDeleted(); + } + + private void CheckIfCanAddOrEdit(string url, MediaInfoType type) + { + if (_shadow.relatedContent.Any(q => q.Url == url && q.Type == type)) + throw new Exception("Related content with same url and same type is already exists"); + } + + + private MediaInfo GetRelatedContent(string relatedContentId) + { + return _shadow.relatedContent.FirstOrDefault(q => q.Id == relatedContentId) ?? + throw new Exception($"Related content with id [{relatedContentId}] is not found"); + } + #endregion + + + internal void SetAnnouncementDate(DateTimeOffset value) + { + if (value == default) throw new ArgumentNullException(nameof(value)); + _shadow.announcementDate = value; + AfterAnnouncementDateSet?.Invoke(); + } + + internal void SetEstimatedReleaseDate(DateTimeOffset value) + { + if (value == default) throw new ArgumentNullException(nameof(value)); + if (_shadow.announcementDate.HasValue && value <= _shadow.announcementDate.Value) + throw new Exception("Estimated release date can not be less or equal to announcement date"); + _shadow.estimatedReleaseDate = value; + AfterEstimatedReleaseDateSet?.Invoke(); + } + internal void SetReleaseDate(DateTimeOffset value) + { + if (value == default) throw new ArgumentNullException(nameof(value)); + if (_shadow.announcementDate.HasValue && value <= _shadow.announcementDate.Value) + throw new Exception("Release date can not be less or equal to announcement date"); + _shadow.releaseDate = value; + AfterReleaseDateSet?.Invoke(); + } + + internal override void SetDeleted() { } +} diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/DescriptionItem.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/DescriptionItem.cs new file mode 100644 index 0000000..4a57b10 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/DescriptionItem.cs @@ -0,0 +1,27 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; +public class DescriptionItem : Entity +{ + [Required] + public string Value { get; private set; } = string.Empty; + public bool IsOriginal { get; init; } + [Required] + public string LanguageId { get; init; } = default!; + + internal DescriptionItem(string languageId, bool isOriginal) + { + LanguageId = languageId; + IsOriginal = isOriginal; + } + + internal DescriptionItem(string languageId, bool isOriginal, string value) + { + LanguageId = languageId; + IsOriginal = isOriginal; + Value = value; + } + + public void SetValue(string value) + { + Value = value; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/GenreProportionItem.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/GenreProportionItem.cs new file mode 100644 index 0000000..64e3630 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/GenreProportionItem.cs @@ -0,0 +1,21 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; + +public class GenreProportionItem : Entity +{ + public decimal? Proportion { get; private set; } + [Required] + public string GenreId { get; init; } = default!; + + internal GenreProportionItem(string genreId) { GenreId = genreId; } + + internal GenreProportionItem(string genreId, decimal? proportion) + { + GenreId = genreId; + Proportion = proportion; + } + + public void SetProportion(decimal? proportion) + { + Proportion = proportion; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/NameItem.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/NameItem.cs new file mode 100644 index 0000000..2a3e4a3 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/NameItem.cs @@ -0,0 +1,36 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; + +public class NameItem : Entity +{ + [Required] + public string Value { get; private set; } = string.Empty; + public NameType Type { get; init; } + [Required] + public string LanguageId { get; init; } = default!; + + internal NameItem(string languageId, NameType type) + { + LanguageId = languageId; + Type = type; + } + + internal NameItem(string languageId, NameType type, string value) + { + LanguageId = languageId; + Type = type; + Value = value; + } + + public void SetValue(string value) + { + Value = value; + } +} + +public enum NameType +{ + Original, + OriginalInAnotherLanguage, + Translation, + Abbreviation, +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs new file mode 100644 index 0000000..f76717a --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/CommonProperties/ShadowEntities/CommonPropertiesShadow.cs @@ -0,0 +1,13 @@ +namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +internal class CommonPropertiesShadow +{ + internal List names = []; + internal MediaInfo? preview = null; + internal List descriptions = []; + internal List genres = []; + internal List relatedContent = []; + internal DateTimeOffset? announcementDate = null; + internal DateTimeOffset? estimatedReleaseDate = null; + internal DateTimeOffset? releaseDate = null; +} diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeEpisode.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeEpisode.cs new file mode 100644 index 0000000..504a89d --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeEpisode.cs @@ -0,0 +1,65 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public class AnimeEpisode : AnimeItemSingle +{ + private readonly AnimeTitle _title; + private readonly AnimeSeason? _season; + private readonly AnimeEpisodeShadow _shadow; + + public AnimeEpisodeType Type => _shadow.type; + public int? Number => _shadow.number; + + internal AnimeEpisode(AnimeTitle title, AnimeSeason? season, AnimeEpisodeShadow shadow) : base(shadow) + { + _title = title; + _season = season; + _shadow = shadow; + } + + public void SetNumber(int? number) => _shadow.number = number; + + public override void SetOrder(ushort order) + { + if (_season == null) SetItemOrder(_title._shadow.items, Order, order); + else _season.SetEpisodeOrder(Order, order); + } + + public override void SetVariant(ushort variant) + { + if (_season == null) + { + SetItemVariant(_title._shadow.items.OfType() + .Where(q => q.order == Order), Variant, variant); + } + else _season.SetEpisodeVariant(Order, Variant, variant); + } + + public override void SetCompleted() + { + if (_season == null) _title.SetEpisodeCompleted(Order, Variant, true); + else _season.SetEpisodeCompleted(Order, Variant, true); + } + + public override void SetNotCompleted() + { + if (_season == null) _title.SetEpisodeCompleted(Order, Variant, false); + else _season.SetEpisodeCompleted(Order, Variant, false); + } + + public override void SetExpirationTime(TimeSpan value) + { + if (_season == null) _title.SetEpisodeExpirationTime(Order, Variant, value); + else _season.SetEpisodeExpirationTime(Order, Variant, value); + } + + internal void SetDeleted() => _shadow.deleted = true; +} + +public enum AnimeEpisodeType +{ + Regilar, + FullLength, + Ova, +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeItem.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeItem.cs new file mode 100644 index 0000000..c3decec --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeItem.cs @@ -0,0 +1,89 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public abstract class AnimeItem : Entity +{ + private readonly AnimeItemShadow _shadow; + + public override string Id => _shadow.id; + + public CommonProperties.CommonProperties CommonProperties => GetCommonProperties(); + public ushort Order => _shadow.order; + public ushort Variant => _shadow.variant; + public bool Completed => _shadow.completed; + public override bool Deleted => _shadow.deleted; + public TimeSpan ExpirationTime => _shadow.expirationTime; + + internal AnimeItem(AnimeItemShadow shadow) { _shadow = shadow; } + + /* + private CommonProperties.CommonProperties GetCommonProperties() + { + new (shadow.commonProperties, afterAnnouncementDateSet, afterEstimatedReleaseDateSet, afterReleaseDateSet) + } + */ + + protected virtual CommonProperties.CommonProperties GetCommonProperties() => + new(_shadow.commonProperties, null, null, null); + + public abstract void SetOrder(ushort order); + internal static ushort GetNewOrder(IEnumerable items) => + (ushort)(items.Select(q => (int)q.order).DefaultIfEmpty(-1).Max() + 1); + + public abstract void SetVariant(ushort variant); + internal static ushort GetNewVariant(IEnumerable items, ushort order) => + (ushort)(items.Where(q => q.order == order) + .Select(q => (int)q.variant).DefaultIfEmpty(-1).Max() + 1); + + internal static void Sort(IEnumerable items) + { + var orderedItemGroups = items.GroupBy(q => q.order).Select(q => new { Order = q.Key, Items = items.Where(x => x.order == q.Key) }) + .OrderBy(q => q.Order).ToArray(); + for (int i = 0; i < orderedItemGroups.Length; i++) + { + foreach (var item in orderedItemGroups[i].Items) + { + item.order = (ushort)i; + } + } + } + + public abstract void SetCompleted(); + public abstract void SetNotCompleted(); + public abstract void SetExpirationTime(TimeSpan value); + + internal static void SetItemOrder(IEnumerable items, ushort lastOrder, ushort newOrder) + { + if (newOrder < 0 || newOrder > items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(newOrder)); + //if (lastOrder == newOrder) return; + var newOrderItem = items.First(q => q.order == newOrder); + items.First(q => q.order == lastOrder).order = newOrder; + foreach (var item in items.Where(q => q.order > Math.Min(lastOrder, newOrder) && q.order < Math.Max(lastOrder, newOrder))) + { + if (lastOrder > newOrder) item.order++; + else if (lastOrder < newOrder) item.order--; + } + if (lastOrder > newOrder) newOrderItem.order++; + else if (lastOrder < newOrder) newOrderItem.order--; + } + + internal static void SetItemVariant(IEnumerable items, ushort lastVariant, ushort newVariant) + { + if (newVariant < 0 || newVariant >= items.Count()) + throw new ArgumentOutOfRangeException(nameof(newVariant)); + //if (lastVariant == newVariant) return; + var newVariantItem = items.First(q => q.variant == newVariant); + items.First(q => q.variant == lastVariant).variant = newVariant; + foreach (var item in items.Where(q => q.variant > Math.Min(lastVariant, newVariant) && q.variant < Math.Max(lastVariant, newVariant))) + { + if (lastVariant > newVariant) item.variant++; + else if (lastVariant < newVariant) item.variant--; + } + if (lastVariant > newVariant) newVariantItem.variant++; + else if (lastVariant < newVariant) newVariantItem.variant--; + } + + internal override void SetDeleted() => _shadow.deleted = true; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeItemSingle.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeItemSingle.cs new file mode 100644 index 0000000..f17323c --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeItemSingle.cs @@ -0,0 +1,16 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public abstract class AnimeItemSingle : AnimeItem +{ + private readonly AnimeItemSingleShadow _shadow; + public TimeSpan? Duration => _shadow.duration; + + internal AnimeItemSingle(AnimeItemSingleShadow shadow) : base(shadow) + { + _shadow = shadow; + } + + public void SetDuration(TimeSpan value) => _shadow.duration = value; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeSeason.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeSeason.cs new file mode 100644 index 0000000..ad92861 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeSeason.cs @@ -0,0 +1,135 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public class AnimeSeason : AnimeItem +{ + private readonly AnimeTitle _title; + private readonly AnimeSeasonShadow _shadow; + + public IReadOnlyCollection Episodes => _shadow.episodes.Select(q => new AnimeEpisode(_title, this, q)).ToList().AsReadOnly(); + + public int? Number => _shadow.number; + public string? Director => _shadow.director; + public string? OriginCountry => _shadow.originCountry; + + internal AnimeSeason(AnimeTitle title, AnimeSeasonShadow shadow) : base(shadow) + { + _title = title; + _shadow = shadow; + } + + protected override CommonProperties.CommonProperties GetCommonProperties() => + new(_shadow.commonProperties, CheckIsCompleted, CheckIsCompleted, CheckIsCompleted); + + public void AddEpisode(AnimeEpisodeType episodeType) + { + StructureAction(() => + { + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = GetNewOrder(_shadow.episodes), + number = _shadow.episodes.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.episodes.Add(episode); + }); + } + + public void AddEpisodeAsVariant(AnimeEpisodeType episodeType, ushort order) + { + StructureAction(() => + { + if (order < 0 || order > _shadow.episodes.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = GetNewVariant(_shadow.episodes, order), + number = _shadow.episodes.FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.episodes.Add(episode); + }); + } + + public void RemoveEpisode(AnimeEpisode episode) + { + StructureAction(() => + { + episode.SetDeleted(); + Sort(_shadow.episodes.Where(q => !q.deleted)); + }); + } + + internal void SetEpisodeOrder(ushort currentOrder, ushort newOrder) => + SetItemOrder(_shadow.episodes, currentOrder, newOrder); + + internal void SetEpisodeVariant(ushort order, ushort currentVariant, ushort newVariant) => + SetItemVariant(_shadow.episodes.Where(q => q.order == order), currentVariant, newVariant); + + internal void SetEpisodeCompleted(ushort order, ushort variant, bool value = true) + { + StructureAction(() => + { + var episode = _shadow.episodes.FirstOrDefault(q => q.order == order && q.variant == variant) ?? + throw new Exception("Episode not found"); + episode.completed = value; + }); + } + + internal void SetEpisodeExpirationTime(ushort order, ushort variant, TimeSpan value) + { + StructureAction(() => + { + var episode = _shadow.episodes.FirstOrDefault(q => q.order == order && q.variant == variant) ?? + throw new Exception("Episode not found"); + episode.expirationTime = value; + }); + } + + private void StructureAction(Action action) + { + action(); + CheckIsCompleted(); + _title.CheckIsCompleted(); + //return new AnimeEpisode(title, this, episodeShadow); + } + + public override void SetOrder(ushort order) => SetItemOrder(_title._shadow.items, Order, order); + + public override void SetVariant(ushort variant) => throw new NotImplementedException(); + + public override void SetCompleted() + { + _title.SetSeasonCompleted(Order, Variant, true); + } + + public override void SetNotCompleted() + { + throw new NotImplementedException(); + } + + public override void SetExpirationTime(TimeSpan value) + { + throw new NotImplementedException(); + } + + internal void SetNumber(int? number) => _shadow.number = number; + internal void SetDirector(string? director) => _shadow.director = director; + internal void SetOriginCountry(string? originCountry) => _shadow.originCountry = originCountry; + + internal void CheckIsCompleted() + { + var itemsQuery = Episodes.Where(q => !q.Deleted).AsQueryable(); + var unreleasedEpisodesAreExists = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + //return unreleasedEpisodesAreExists || lastEpisodeReleaseDate - DateTime.UtcNow > expirePeriod; + if (unreleasedEpisodesAreExists || lastEpisodeReleaseDate >= DateTime.UtcNow - ExpirationTime) SetNotCompleted(); + else SetCompleted(); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeTitle.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeTitle.cs new file mode 100644 index 0000000..0177f83 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/AnimeTitle.cs @@ -0,0 +1,171 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +public class AnimeTitle : Entity +{ + internal readonly AnimeTitleShadow _shadow; + + public override string Id => _shadow.id; + + public CommonProperties.CommonProperties CommonProperties => new(_shadow.commonProperties, CheckIsCompleted, CheckIsCompleted, CheckIsCompleted); + + public IReadOnlyCollection Items => _shadow.items.Select(ConvertToAnimeItem).ToList().AsReadOnly(); + + public bool Completed => _shadow.completed; + public override bool Deleted => _shadow.deleted; + public TimeSpan ExpirationTime => _shadow.expirationTime; + + internal AnimeTitle(AnimeTitleShadow shadow) => _shadow = shadow; + + private AnimeItem ConvertToAnimeItem(AnimeItemShadow shadow) + { + if (shadow is AnimeEpisodeShadow episode) return new AnimeEpisode(this, null, episode); + else if (shadow is AnimeSeasonShadow season) return new AnimeSeason(this, season); + else throw new NotImplementedException(); + } + + + public void AddEpisode(AnimeEpisodeType episodeType) + { + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = AnimeItem.GetNewOrder(_shadow.items), + number = _shadow.items.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + public void AddEpisodeAsVariant(AnimeEpisodeType episodeType, ushort order) + { + if (order < 0 || order > _shadow.items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = AnimeItem.GetNewVariant(_shadow.items, order), + number = _shadow.items.OfType().FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + public void RemoveEpisode(AnimeEpisode episode) + { + episode.SetDeleted(); + AnimeItem.Sort(_shadow.items.Where(q => !q.deleted)); + CheckIsCompleted(); + } + + internal void SetEpisodeOrder(ushort currentOrder, ushort newOrder) => + AnimeItem.SetItemOrder(_shadow.items, currentOrder, newOrder); + + internal void SetEpisodeVariant(ushort order, ushort currentVariant, ushort newVariant) => + AnimeItem.SetItemVariant(_shadow.items.OfType() + .Where(q => q.order == order), currentVariant, newVariant); + + internal void SetEpisodeCompleted(ushort order, ushort variant, bool value = true) => + SetCompleted(order, variant, value); + + internal void SetEpisodeExpirationTime(ushort order, ushort variant, TimeSpan value) => + SetExpirationTime(order, variant, value); + + + + public void AddSeason() + { + var episode = new AnimeSeasonShadow + { + order = AnimeItem.GetNewOrder(_shadow.items), + number = _shadow.items.OfType().Select(q => q.number).DefaultIfEmpty(-1).Max() + 1, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + + /* + public void AddSeasonAsVariant(AnimeEpisodeType episodeType, ushort order) + { + if (order < 0 || order > _shadow.items.Select(q => q.order).DefaultIfEmpty(0).Max()) + throw new ArgumentOutOfRangeException(nameof(order)); + + var episode = new AnimeEpisodeShadow + { + type = episodeType, + order = order, + variant = AnimeItem.GetNewVariant(_shadow.items, order), + number = _shadow.items.OfType().FirstOrDefault(q => q.order == order)?.number, + }; + _shadow.items.Add(episode); + CheckIsCompleted(); + } + */ + + public void RemoveSeason(AnimeEpisode episode) + { + episode.SetDeleted(); + AnimeItem.Sort(_shadow.items.Where(q => !q.deleted)); + CheckIsCompleted(); + } + + internal void SetSeasonCompleted(ushort order, ushort variant, bool value = true) => + SetCompleted(order, variant, value); + + internal void SetSeasonExpirationTime(ushort order, ushort variant, TimeSpan value) => + SetExpirationTime(order, variant, value); + + private void SetCompleted(ushort order, ushort variant, bool value = true) where T : AnimeItemShadow + { + var item = GetItem(order, variant); + item.completed = value; + CheckIsCompleted(); + } + + private void SetExpirationTime(ushort order, ushort variant, TimeSpan value) where T : AnimeItemShadow + { + var item = GetItem(order, variant); + item.expirationTime = value; + CheckIsCompleted(); + } + + private T GetItem(ushort order, ushort variant) where T : AnimeItemShadow + { + var type = typeof(T); + var itemName = string.Empty; + if (type == typeof(AnimeEpisodeShadow)) itemName = "Episode"; + else if (type == typeof(AnimeSeason)) itemName = "Season"; + else throw new NotImplementedException(); + return _shadow.items.OfType() + .FirstOrDefault(q => q.order == order && q.variant == variant) ?? + throw new Exception(string.Concat(itemName, " not found")); + } + + public void SetCompleted() => _shadow.completed = true; + + public void SetNotCompleted() => _shadow.completed = false; + + public void SetExpirationTime(TimeSpan value) + { + _shadow.expirationTime = value; + CheckIsCompleted(); + } + + public void Delete() => SetDeleted(); + internal override void SetDeleted() => _shadow.deleted = true; + + internal void CheckIsCompleted() + { + var itemsQuery = Items.AsQueryable(); + var ucompletedSeasons = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + if (ucompletedSeasons || lastEpisodeReleaseDate >= DateTime.UtcNow - ExpirationTime) SetNotCompleted(); + else SetCompleted(); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs new file mode 100644 index 0000000..bdec4b5 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeEpisodeShadow.cs @@ -0,0 +1,12 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeEpisodeShadow : AnimeItemSingleShadow +{ + internal AnimeEpisodeType type = AnimeEpisodeType.Regilar; + internal int? number = null; + + internal AnimeEpisodeShadow() : base(new CommonPropertiesShadow()) { } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs new file mode 100644 index 0000000..93cf7e5 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeItemShadow.cs @@ -0,0 +1,14 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal abstract class AnimeItemShadow(CommonPropertiesShadow commonProperties) +{ + internal string id = Guid.NewGuid().ToString(); + internal CommonPropertiesShadow commonProperties = commonProperties; + internal ushort order = 0; + internal ushort variant = 0; + internal bool completed = false; + internal TimeSpan expirationTime = TimeSpan.Zero; + internal bool deleted = false; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs new file mode 100644 index 0000000..fcc37ca --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeItemSingleShadow.cs @@ -0,0 +1,8 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal abstract class AnimeItemSingleShadow(CommonPropertiesShadow commonProperties) : AnimeItemShadow(commonProperties) +{ + internal TimeSpan? duration = null; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs new file mode 100644 index 0000000..99356b6 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeSeasonShadow.cs @@ -0,0 +1,15 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeSeasonShadow : AnimeItemShadow +{ + internal List episodes => []; + internal int? number = null; + internal string? director = null; + internal string? originCountry = null; + + internal AnimeSeasonShadow() : base(new CommonPropertiesShadow()) + { + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs new file mode 100644 index 0000000..c266ca2 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaContent/Items/Anime/ShadowEntities/AnimeTitleShadow.cs @@ -0,0 +1,13 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +internal class AnimeTitleShadow(CommonPropertiesShadow commonPropertiesShadow) +{ + internal string id = Guid.NewGuid().ToString(); + internal CommonPropertiesShadow commonProperties = commonPropertiesShadow; + internal List items = []; + internal bool completed = false; + internal TimeSpan expirationTime = TimeSpan.Zero; + internal bool deleted = false; +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaInfo.cs b/Modules.Library.Domain/EntitiesV1/MediaInfo.cs new file mode 100644 index 0000000..92256a9 --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaInfo.cs @@ -0,0 +1,22 @@ +namespace Modules.Library.Domain.EntitiesV1; +public class MediaInfo : Entity +{ + public MediaInfoType Type { get; private set; } = MediaInfoType.OtherFile; + //public string ContentType { get; set; } = default!; + public string Url { get; private set; } = default!; + + internal MediaInfo(string url, MediaInfoType type) + { + Url = url; + Type = type; + } + + internal void SetUrl(string value) + { + Url = value; + } + internal void SetType(MediaInfoType value) + { + Type = value; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntitiesV1/MediaInfoType.cs b/Modules.Library.Domain/EntitiesV1/MediaInfoType.cs new file mode 100644 index 0000000..c69b98c --- /dev/null +++ b/Modules.Library.Domain/EntitiesV1/MediaInfoType.cs @@ -0,0 +1,9 @@ +namespace Modules.Library.Domain.EntitiesV1; + +public enum MediaInfoType +{ + Image, + Video, + Link, + OtherFile +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntityBuilders/AnimeEpisodeBuilder.cs b/Modules.Library.Domain/EntityBuilders/AnimeEpisodeBuilder.cs new file mode 100644 index 0000000..bf0435e --- /dev/null +++ b/Modules.Library.Domain/EntityBuilders/AnimeEpisodeBuilder.cs @@ -0,0 +1,83 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntityBuilders; + +public class AnimeEpisodeBuilder +{ + private AnimeEpisodeShadow episode = new(); + + public AnimeEpisodeBuilder SetCommonProperties(CommonPropertiesBuilder builder) + { + episode.commonProperties = builder.Build(); + return this; + } + + public AnimeEpisodeBuilder SetOrder(ushort value) + { + episode.order = value; + return this; + } + + public AnimeEpisodeBuilder SetVariant(ushort value) + { + episode.variant = value; + return this; + } + + public AnimeEpisodeBuilder SetDuration(TimeSpan value) + { + episode.duration = value; + return this; + } + + public AnimeEpisodeBuilder SetType(AnimeEpisodeType value) + { + episode.type = value; + return this; + } + + public AnimeEpisodeBuilder SetNumber(int? value) + { + episode.number = value; + return this; + } + + public AnimeEpisodeBuilder SetCompleted(bool value) + { + episode.completed = value; + return this; + } + + public AnimeEpisodeBuilder SetDeleted(bool value) + { + episode.deleted = value; + return this; + } + + public AnimeEpisodeBuilder SetExpirationTime(TimeSpan value) + { + episode.expirationTime = value; + return this; + } + + internal static AnimeEpisodeBuilder FromAnimeEpisode(AnimeEpisode episode) + { + var builder = new AnimeEpisodeBuilder(); + builder.SetCommonProperties(CommonPropertiesBuilder.FromCommonProperties(episode.CommonProperties)); + builder.SetOrder(episode.Order); + builder.SetVariant(episode.Variant); + if (episode.Duration.HasValue) builder.SetDuration(episode.Duration.Value); + builder.SetType(episode.Type); + builder.SetNumber(episode.Number); + builder.SetCompleted(episode.Completed); + builder.SetDeleted(episode.Deleted); + builder.SetExpirationTime(episode.ExpirationTime); + return builder; + } + + internal AnimeEpisodeShadow Build() + { + return episode; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntityBuilders/AnimeSeasonBuilder.cs b/Modules.Library.Domain/EntityBuilders/AnimeSeasonBuilder.cs new file mode 100644 index 0000000..7f8c500 --- /dev/null +++ b/Modules.Library.Domain/EntityBuilders/AnimeSeasonBuilder.cs @@ -0,0 +1,94 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntityBuilders; + +public class AnimeSeasonBuilder +{ + private AnimeSeasonShadow _season = new(); + private CommonPropertiesBuilder? _commonPropertiesBuilder; + + public AnimeSeasonBuilder SetCommonProperties(CommonPropertiesBuilder builder) + { + _commonPropertiesBuilder = builder; + return this; + } + + + public AnimeSeasonBuilder SetOrder(ushort value) + { + _season.order = value; + return this; + } + + public AnimeSeasonBuilder SetVariant(ushort value) + { + _season.variant = value; + return this; + } + + public AnimeSeasonBuilder SetNumber(int? value) + { + _season.number = value; + return this; + } + + + public AnimeSeasonBuilder SetDirector(string? value) + { + _season.director = value; + return this; + } + + public AnimeSeasonBuilder SetOriginCountry(string? value) + { + _season.originCountry = value; + return this; + } + + public AnimeSeasonBuilder SetCompleted(bool value) + { + _season.completed = value; + return this; + } + + public AnimeSeasonBuilder SetDeleted(bool value) + { + _season.deleted = value; + return this; + } + + public AnimeSeasonBuilder SetExpirationTime(TimeSpan value) + { + _season.expirationTime = value; + return this; + } + + public AnimeSeasonBuilder AddEpisode(AnimeEpisodeBuilder builder) + { + _season.episodes.Add(builder.Build()); + return this; + } + + internal static AnimeSeasonBuilder FromAnimeSeason(AnimeSeason season) + { + var builder = new AnimeSeasonBuilder(); + builder.SetCommonProperties(CommonPropertiesBuilder.FromCommonProperties(season.CommonProperties)); + builder.SetOrder(season.Order); + builder.SetVariant(season.Variant); + builder.SetNumber(season.Number); + builder.SetDirector(season.Director); + builder.SetOriginCountry(season.OriginCountry); + builder.SetCompleted(season.Completed); + builder.SetDeleted(season.Deleted); + builder.SetExpirationTime(season.ExpirationTime); + foreach (var episode in season.Episodes) { builder.AddEpisode(AnimeEpisodeBuilder.FromAnimeEpisode(episode)); } + return builder; + } + + internal AnimeSeasonShadow Build() + { + if (_commonPropertiesBuilder != null) _season.commonProperties = _commonPropertiesBuilder.Build(); + return _season; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntityBuilders/AnimeTitleBuilder.cs b/Modules.Library.Domain/EntityBuilders/AnimeTitleBuilder.cs new file mode 100644 index 0000000..449c4e1 --- /dev/null +++ b/Modules.Library.Domain/EntityBuilders/AnimeTitleBuilder.cs @@ -0,0 +1,75 @@ +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime; +using Modules.Library.Domain.EntitiesV0.MediaContent.Items.Anime.ShadowEntities; + +namespace Modules.Library.Domain.EntityBuilders; + +public class AnimeTitleBuilder +{ + private AnimeTitleShadow _title = new(new CommonPropertiesShadow()); + private CommonPropertiesBuilder? _commonPropertiesBuilder; + private List _seasonBuilders = []; + private List _episodeBuilders = []; + + public AnimeTitleBuilder SetCommonProperties(CommonPropertiesBuilder builder) + { + _commonPropertiesBuilder = builder; + return this; + } + + public AnimeTitleBuilder AddEpisode(AnimeEpisodeBuilder builder) + { + _episodeBuilders.Add(builder); + return this; + } + + public AnimeTitleBuilder AddSeason(AnimeSeasonBuilder builder) + { + _seasonBuilders.Add(builder); + return this; + } + + public AnimeTitleBuilder SetCompleted(bool value) + { + _title.completed = value; + return this; + } + + public AnimeTitleBuilder SetDeleted(bool value) + { + _title.deleted = value; + return this; + } + public AnimeTitleBuilder SetExpirationTime(TimeSpan value) + { + _title.expirationTime = value; + return this; + } + + public static AnimeTitleBuilder FromAnimeTitle(AnimeTitle title) + { + var builder = new AnimeTitleBuilder(); + builder.SetCommonProperties(CommonPropertiesBuilder.FromCommonProperties(title.CommonProperties)); + + foreach (var item in title.Items) + { + if (item is AnimeSeason season) builder.AddSeason(AnimeSeasonBuilder.FromAnimeSeason(season)); + else if (item is AnimeEpisode episode) builder.AddEpisode(AnimeEpisodeBuilder.FromAnimeEpisode(episode)); + else throw new NotImplementedException(); + } + + builder.SetCompleted(title.Completed); + builder.SetDeleted(title.Deleted); + builder.SetExpirationTime(title.ExpirationTime); + return builder; + } + + public AnimeTitle Build() + { + //_title.commonProperties = _commonPropertiesBuilder?.Build() ?? throw new Exception("No commonProperties"); + if (_commonPropertiesBuilder != null) _title.commonProperties = _commonPropertiesBuilder.Build(); + _title.items.AddRange(_seasonBuilders.Select(q => q.Build())); + _title.items.AddRange(_episodeBuilders.Select(q => q.Build())); + return new AnimeTitle(_title); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/EntityBuilders/CommonPropertiesBuilder.cs b/Modules.Library.Domain/EntityBuilders/CommonPropertiesBuilder.cs new file mode 100644 index 0000000..ab8bd85 --- /dev/null +++ b/Modules.Library.Domain/EntityBuilders/CommonPropertiesBuilder.cs @@ -0,0 +1,83 @@ +using Modules.Library.Domain.EntitiesV0; +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties; +using Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities; + +namespace Modules.Library.Domain.EntityBuilders; + +public class CommonPropertiesBuilder +{ + private CommonPropertiesShadow _commonProperties = new(); + + public CommonPropertiesBuilder AddName(string languageId, NameType type, string value) + { + _commonProperties.names.Add(new NameItem(languageId, type, value)); + return this; + } + + public CommonPropertiesBuilder SetPreview(string url, MediaInfoType type) + { + _commonProperties.preview = new MediaInfo(url, type); + return this; + } + + public CommonPropertiesBuilder AddDescription(string languageId, bool isOriginal, string value) + { + _commonProperties.descriptions.Add(new DescriptionItem(languageId, isOriginal, value)); + return this; + } + + public CommonPropertiesBuilder AddGenreProportion(string genreId, decimal? proportion) + { + _commonProperties.genres.Add(new GenreProportionItem(genreId, proportion)); + return this; + } + + public CommonPropertiesBuilder AddRelatedContent(string url, MediaInfoType type) + { + _commonProperties.relatedContent.Add(new MediaInfo(url, type)); + return this; + } + + public CommonPropertiesBuilder SetAnouncementDate(DateTimeOffset? value) + { + _commonProperties.announcementDate = value; + return this; + } + + public CommonPropertiesBuilder SetEstimatedReleaseDate(DateTimeOffset? value) + { + _commonProperties.estimatedReleaseDate = value; + return this; + } + + public CommonPropertiesBuilder SetReleaseDate(DateTimeOffset? value) + { + _commonProperties.releaseDate = value; + return this; + } + + internal static CommonPropertiesBuilder FromCommonProperties(CommonProperties item) + { + var builder = new CommonPropertiesBuilder(); + + foreach (var name in item.Names) { builder.AddName(name.LanguageId, name.Type, name.Value); } + + if (item.Preview != null) builder + .SetPreview(item.Preview.Url, item.Preview.Type); + + foreach (var description in item.Descriptions) { builder.AddDescription(description.LanguageId, description.IsOriginal, description.Value); } + foreach (var genreProportion in item.Genres) { builder.AddGenreProportion(genreProportion.GenreId, genreProportion.Proportion); } + foreach (var repatedContent in item.RelatedContent) { builder.AddRelatedContent(repatedContent.Url, repatedContent.Type); } + + builder.SetAnouncementDate(item.AnnouncementDate); + builder.SetEstimatedReleaseDate(item.EstimatedReleaseDate); + builder.SetReleaseDate(item.ReleaseDate); + + return builder; + } + + internal CommonPropertiesShadow Build() + { + return _commonProperties; + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Exceptions/Genre/GenreIsAlreadyExistException.cs b/Modules.Library.Domain/Exceptions/Genre/GenreIsAlreadyExistException.cs new file mode 100644 index 0000000..29bbe50 --- /dev/null +++ b/Modules.Library.Domain/Exceptions/Genre/GenreIsAlreadyExistException.cs @@ -0,0 +1,5 @@ +namespace Modules.Library.Domain.Exceptions.Genre; + +public class GenreIsAlreadyExistException : Exception +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Exceptions/Genre/GenreIsNotFoundException.cs b/Modules.Library.Domain/Exceptions/Genre/GenreIsNotFoundException.cs new file mode 100644 index 0000000..53721da --- /dev/null +++ b/Modules.Library.Domain/Exceptions/Genre/GenreIsNotFoundException.cs @@ -0,0 +1,5 @@ +namespace Modules.Library.Domain.Exceptions.Genre; + +public class GenreIsNotFoundException : Exception +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Exceptions/Genre/GenreWithSameNameIsAlreadyExistException.cs b/Modules.Library.Domain/Exceptions/Genre/GenreWithSameNameIsAlreadyExistException.cs new file mode 100644 index 0000000..eeaa375 --- /dev/null +++ b/Modules.Library.Domain/Exceptions/Genre/GenreWithSameNameIsAlreadyExistException.cs @@ -0,0 +1,5 @@ +namespace Modules.Library.Domain.Exceptions.Genre; + +public class GenreWithSameNameIsAlreadyExistException : Exception +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Exceptions/Language/LanguageIsAlreadyExistException.cs b/Modules.Library.Domain/Exceptions/Language/LanguageIsAlreadyExistException.cs new file mode 100644 index 0000000..68466d0 --- /dev/null +++ b/Modules.Library.Domain/Exceptions/Language/LanguageIsAlreadyExistException.cs @@ -0,0 +1,5 @@ +namespace Modules.Library.Domain.Exceptions.Language; + +public class LanguageIsAlreadyExistException : Exception +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Exceptions/Language/LanguageWithSameNameIsAlreadyExistException.cs b/Modules.Library.Domain/Exceptions/Language/LanguageWithSameNameIsAlreadyExistException.cs new file mode 100644 index 0000000..20af13c --- /dev/null +++ b/Modules.Library.Domain/Exceptions/Language/LanguageWithSameNameIsAlreadyExistException.cs @@ -0,0 +1,5 @@ +namespace Modules.Library.Domain.Exceptions.Language; + +public class LanguageWithSameNameIsAlreadyExistException : Exception +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Exceptions/MediaInfo/MediaInfoIsNotFoundException.cs b/Modules.Library.Domain/Exceptions/MediaInfo/MediaInfoIsNotFoundException.cs new file mode 100644 index 0000000..f3d2a56 --- /dev/null +++ b/Modules.Library.Domain/Exceptions/MediaInfo/MediaInfoIsNotFoundException.cs @@ -0,0 +1,5 @@ +namespace Modules.Library.Domain.Exceptions.MediaInfo; + +public class MediaInfoIsNotFoundException : Exception +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Gateways/ICommonPropertiesGateway.cs b/Modules.Library.Domain/Gateways/ICommonPropertiesGateway.cs new file mode 100644 index 0000000..e7c93d2 --- /dev/null +++ b/Modules.Library.Domain/Gateways/ICommonPropertiesGateway.cs @@ -0,0 +1,5 @@ +using Modules.Library.Domain.Entities.MediaContent.CommonProperties; + +namespace Modules.Library.Domain.Gateways; + +public interface ICommonPropertiesGateway : IRepository { } \ No newline at end of file diff --git a/Modules.Library.Domain/Gateways/IGenreGateway.cs b/Modules.Library.Domain/Gateways/IGenreGateway.cs new file mode 100644 index 0000000..f1a131a --- /dev/null +++ b/Modules.Library.Domain/Gateways/IGenreGateway.cs @@ -0,0 +1,5 @@ +using Modules.Library.Domain.Entities.Genre; + +namespace Modules.Library.Domain.Gateways; + +public interface IGenreGateway : IRepository { } \ No newline at end of file diff --git a/Modules.Library.Domain/Gateways/ILanguageGateway.cs b/Modules.Library.Domain/Gateways/ILanguageGateway.cs new file mode 100644 index 0000000..c51d58b --- /dev/null +++ b/Modules.Library.Domain/Gateways/ILanguageGateway.cs @@ -0,0 +1,5 @@ +using Modules.Library.Domain.Entities.Language; + +namespace Modules.Library.Domain.Gateways; + +public interface ILanguageGateway : IRepository { } \ No newline at end of file diff --git a/Modules.Library.Domain/Gateways/IMediaContentGateway.cs b/Modules.Library.Domain/Gateways/IMediaContentGateway.cs new file mode 100644 index 0000000..cb51e1f --- /dev/null +++ b/Modules.Library.Domain/Gateways/IMediaContentGateway.cs @@ -0,0 +1,5 @@ +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; + +namespace Modules.Library.Domain.Gateways; + +public interface IAnimeTitleGateway : IRepository { } \ No newline at end of file diff --git a/Modules.Library.Domain/Gateways/IMediaInfoGateway.cs b/Modules.Library.Domain/Gateways/IMediaInfoGateway.cs new file mode 100644 index 0000000..d98a696 --- /dev/null +++ b/Modules.Library.Domain/Gateways/IMediaInfoGateway.cs @@ -0,0 +1,5 @@ +using Modules.Library.Domain.Entities; + +namespace Modules.Library.Domain.Gateways; + +public interface IMediaInfoGateway : IRepository { } \ No newline at end of file diff --git a/Modules.Library.Domain/Gateways/IRepository.cs b/Modules.Library.Domain/Gateways/IRepository.cs new file mode 100644 index 0000000..6eacdc6 --- /dev/null +++ b/Modules.Library.Domain/Gateways/IRepository.cs @@ -0,0 +1,36 @@ +using Modules.Library.Domain.Entities; +using System.Linq.Expressions; + +namespace Modules.Library.Domain.Gateways; + +public interface IRepository +{ + Task> GetAllAsync(); + + Task GetByIdAsync(TKey id); + Task GetByIdOrDefaultAsync(TKey id); + + Task> GetRangeByIdsAsync(List ids); + + Task GetFirstWhere(Expression> predicate); + Task GetFirstOrDefaultWhere(Expression> predicate); + + Task> GetWhere(Expression> predicate); + + Task AnyWhere(Expression> predicate); + /* + Task AddAsync(T entity, IUser user); + + Task UpdateAsync(T entity, IUser user); + + Task DeleteAsync(T entity, IUser user); + */ + Task AddAsync(T entity); + + Task UpdateAsync(T entity); + + Task DeleteAsync(T entity); +} + +public interface IRepository : IRepository where T : Entity { } +//public interface IRepository : IRepository { } \ No newline at end of file diff --git a/Modules.Library.Domain/GlobalUsings.cs b/Modules.Library.Domain/GlobalUsings.cs new file mode 100644 index 0000000..426f8ca --- /dev/null +++ b/Modules.Library.Domain/GlobalUsings.cs @@ -0,0 +1 @@ +global using System.ComponentModel.DataAnnotations; \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/GenreInteractor.cs b/Modules.Library.Domain/Interactors/GenreInteractor.cs new file mode 100644 index 0000000..90a8c7d --- /dev/null +++ b/Modules.Library.Domain/Interactors/GenreInteractor.cs @@ -0,0 +1,35 @@ +using Modules.Library.Domain.Entities.Genre; +using Modules.Library.Domain.Exceptions.Genre; +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors; + +public class GenreInteractor(IGenreGateway gateway) +{ + public async Task Create(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(name); + if (await gateway.GetFirstOrDefaultWhere(q => q.Name == name) != null) throw new GenreIsAlreadyExistException(); + var newGenre = new Genre(name); + return await gateway.AddAsync(newGenre); + } + + public async Task Edit(Guid genreId, string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(name); + var genre = await gateway.GetByIdAsync(genreId); + if (await gateway.GetFirstOrDefaultWhere(q => q.Name == name) != null) throw new GenreWithSameNameIsAlreadyExistException(); + genre.SetName(name); + if (!await gateway.UpdateAsync(genre)) throw new Exception("Save unsuccessfull"); + } + + + public async Task Delete(Guid genreId) + { + var genre = await gateway.GetByIdAsync(genreId); + //await gateway.DeleteAsync(genre); + if (genre.Deleted) throw new Exception("AlreadyDeleted"); + genre.SetDeleted(); + if (!await gateway.UpdateAsync(genre)) throw new Exception("Save unsuccessfull"); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/LanguageInteractor.cs b/Modules.Library.Domain/Interactors/LanguageInteractor.cs new file mode 100644 index 0000000..8e0a1b5 --- /dev/null +++ b/Modules.Library.Domain/Interactors/LanguageInteractor.cs @@ -0,0 +1,36 @@ +using Modules.Library.Domain.Entities.Language; +using Modules.Library.Domain.Exceptions.Language; +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors; + +public class LanguageInteractor(ILanguageGateway gateway) +{ + public async Task Create(string codeIso2, string name, Guid? iconId) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(name); + if (await gateway.GetFirstOrDefaultWhere(q => q.Name == name) != null) throw new LanguageIsAlreadyExistException(); + var newLanguage = new Language(codeIso2, name, iconId); + return await gateway.AddAsync(newLanguage); + } + + public async Task Edit(Guid languageId, string name, Guid? iconId) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(name); + var language = await gateway.GetByIdAsync(languageId); + if (await gateway.GetFirstOrDefaultWhere(q => q.Name == name) != null) throw new LanguageWithSameNameIsAlreadyExistException(); + language.SetName(name); + language.SetIcon(iconId); + if (!await gateway.UpdateAsync(language)) throw new Exception("Save unsuccessfull"); + } + + + public async Task Delete(Guid languageId) + { + var language = await gateway.GetByIdAsync(languageId); + //await gateway.DeleteAsync(genre); + if (language.Deleted) throw new Exception("AlreadyDeleted"); + language.SetDeleted(); + if (!await gateway.UpdateAsync(language)) throw new Exception("Save unsuccessfull"); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/CommonPropertiesService.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/CommonPropertiesService.cs new file mode 100644 index 0000000..7dc9aff --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/CommonPropertiesService.cs @@ -0,0 +1,252 @@ +using Modules.Library.Domain.Entities; +using Modules.Library.Domain.Entities.MediaContent.CommonProperties; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.Exceptions.Genre; +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime; + +internal class CommonPropertiesService(ILanguageGateway languageGateway, IGenreGateway genreGateway) +{ + #region Name + internal async Task AddName(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, NameType nameType, Guid languageId, string value) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (!await languageGateway.AnyWhere(q => q.Id == languageId)) throw new Exception("Language is not found"); + switch (nameType) + { + case NameType.Original: + AddNameOriginal(commonProperties, languageId, value); + break; + case NameType.OriginalInAnotherLanguage: + AddNameOriginalInAnotherLanguage(commonProperties, languageId, value); + break; + case NameType.Translation: + AddNameTranslation(commonProperties, languageId, value); + break; + case NameType.Abbreviation: + AddNameAbbreviation(commonProperties, languageId, value); + break; + default: + throw new NotImplementedException(); + } + } + + private static void AddNameOriginal(CommonProperties commonProperties, Guid languageId, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + if (commonProperties.Names.Any(q => q.Type == NameType.Original)) throw new Exception("Original name is already exist."); + commonProperties.Names.Add(new NameItem(languageId, NameType.Original, value)); + } + + private static void AddNameOriginalInAnotherLanguage(CommonProperties commonProperties, Guid languageId, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + if (languageId == commonProperties.Names.SingleOrDefault(q => q.Type == NameType.Original)?.LanguageId) + throw new Exception("Language must not match original name language"); + + if (commonProperties.Names.Any(q => q.Type == NameType.OriginalInAnotherLanguage && q.LanguageId == languageId)) + throw new Exception("Name in following language is already exist."); + commonProperties.Names.Add(new NameItem(languageId, NameType.OriginalInAnotherLanguage, value)); + } + + private static void AddNameTranslation(CommonProperties commonProperties, Guid languageId, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + commonProperties.Names.Add(new NameItem(languageId, NameType.Translation, value)); + } + + private static void AddNameAbbreviation(CommonProperties commonProperties, Guid languageId, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + commonProperties.Names.Add(new NameItem(languageId, NameType.Abbreviation, value)); + } + + internal static void RemoveName(CommonProperties commonProperties, Guid nameId) + { + var name = GetName(commonProperties, nameId); + if (name.Type == NameType.Original) throw new Exception($"Unable to remove original name"); + commonProperties.Names.Remove(name); + } + + internal static void SetNameValue(CommonProperties commonProperties, Guid nameId, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + var name = GetName(commonProperties, nameId); + if (name.Type != NameType.Original && commonProperties.Names.Any(q => q.LanguageId == name.LanguageId && q.Value == value)) + throw new Exception("Name item with in same language with same value is already exists"); + name.SetValue(value); + } + + private static NameItem GetName(CommonProperties commonProperties, Guid nameId) => commonProperties.Names.FirstOrDefault(q => q.Id == nameId) ?? + throw new Exception($"Name with id [{nameId}] is not found"); + #endregion + + internal static void SetPreview(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, string url, MediaInfoType type) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); + commonProperties.Preview ??= new MediaInfo(url, type); + commonProperties.Preview.Url = url; + } + + internal static void DeletePreview(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + commonProperties.Preview = null; + } + + #region Description + internal async Task AddDescription(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid languageId, bool isOriginal, string value) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (isOriginal) + { + if (!await languageGateway.AnyWhere(q => q.Id == languageId)) throw new Exception("Language is not found"); + if (commonProperties.Descriptions.Any(q => q.IsOriginal)) throw new Exception("Original description is already exist."); + } + if (!await languageGateway.AnyWhere(q => q.Id == languageId)) throw new Exception("Language is not found"); + var description = new DescriptionItem(languageId, isOriginal, value); + commonProperties.Descriptions.Add(description); + } + + internal static void RemoveDescription(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid descriptionId) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + var description = GetDescription(commonProperties, descriptionId); + if (description.IsOriginal) throw new Exception($"Unable to remove original description"); + commonProperties.Descriptions.Remove(description); + + } + + internal static void SetDescriptionValue(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid descriptionId, string value) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + var name = GetDescription(commonProperties, descriptionId); + SetDescriptionValue(commonProperties, name, value); + } + + private static void SetDescriptionValue(CommonProperties commonProperties, DescriptionItem descriptionItem, string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value)); + if (!descriptionItem.IsOriginal && commonProperties.Descriptions.Any(q => q.LanguageId == descriptionItem.LanguageId && q.Value == value)) + throw new Exception("Descriptoin item with with same value is already exists"); + descriptionItem.Value = value; + } + + private static DescriptionItem GetDescription(CommonProperties commonProperties, Guid descriptionId) + { + return commonProperties.Descriptions.FirstOrDefault(q => q.Id == descriptionId) ?? + throw new Exception($"Description with id [{descriptionId}] is not found"); + } + #endregion + + #region RelatedContent + internal static void AddRelatedContent(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, string url, MediaInfoType type) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); + CheckIfCanAddOrEdit(commonProperties, url, type); + commonProperties.RelatedContent.Add(new MediaInfo(url, type)); + } + + internal static void EditRelatedContent(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid relatedContentId, string url, MediaInfoType type) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + var relatedContent = GetRelatedContent(commonProperties, relatedContentId); + CheckIfCanAddOrEdit(commonProperties, url, type); + relatedContent.Url = url; + relatedContent.Type = type; + } + + internal static void RemoveRelatedContent(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid relatedContentId) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + var relatedContent = GetRelatedContent(commonProperties, relatedContentId); + //_relatedContent.Remove(relatedContent); + commonProperties.RelatedContent.Remove(relatedContent); + } + + private static void CheckIfCanAddOrEdit(CommonProperties commonProperties, string url, MediaInfoType type) + { + if (commonProperties.RelatedContent.Any(q => q.Url == url && q.Type == type)) + throw new Exception("Related content with same url and same type is already exists"); + } + + private static MediaInfo GetRelatedContent(CommonProperties commonProperties, Guid relatedContentId) + { + return commonProperties.RelatedContent.FirstOrDefault(q => q.Id == relatedContentId) ?? + throw new Exception($"Related content with id [{relatedContentId}] is not found"); + } + #endregion + + #region Genre + internal async Task AddGenre(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid genreId, decimal? proportion) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (!await genreGateway.AnyWhere(q => q.Id == genreId)) throw new GenreIsNotFoundException(); + commonProperties.Genres.Add(new GenreProportionItem(genreId, proportion)); + } + + internal static void SetGenreProportion(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid genreProportionId, decimal? proportion) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + var genreProportion = commonProperties.Genres.First(q => q.GenreId == genreProportionId); + genreProportion.Proportion = proportion; + } + + internal static void RemoveGenre(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, Guid genreProportionId, decimal? proportion) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + var genreProportion = commonProperties.Genres.First(q => q.GenreId == genreProportionId); + commonProperties.Genres.Remove(genreProportion); + } + #endregion + + internal static void SetAnnouncementDate(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, DateTimeOffset value) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (value == default) throw new ArgumentNullException(nameof(value)); + commonProperties.AnnouncementDate = value; + } + + internal static void SetEstimatedReleaseDate(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, DateTimeOffset value) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (value == default) throw new ArgumentNullException(nameof(value)); + if (commonProperties.AnnouncementDate.HasValue && value <= commonProperties.AnnouncementDate.Value) + throw new Exception("Estimated release date can not be less or equal to announcement date"); + commonProperties.EstimatedReleaseDate = value; + } + + internal static void SetReleaseDate(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId, DateTimeOffset value) + { + var commonProperties = GetCommonProperties(title, animeSeasonId, animeEpisodeId); + if (value == default) throw new ArgumentNullException(nameof(value)); + if (commonProperties.AnnouncementDate.HasValue && value <= commonProperties.AnnouncementDate.Value) + throw new Exception("Release date can not be less or equal to announcement date"); + commonProperties.ReleaseDate = value; + } + + private static CommonProperties GetCommonProperties(AnimeTitle title, Guid? animeSeasonId, Guid? animeEpisodeId) + { + if (!animeSeasonId.HasValue && !animeEpisodeId.HasValue) return title.CommonProperties; + else if (animeSeasonId.HasValue && !animeEpisodeId.HasValue) + { + var season = title.Items.OfType().First(q => q.Id == animeSeasonId); + return season.CommonProperties; + } + else if (!animeSeasonId.HasValue && animeEpisodeId.HasValue) + { + //set to unseason episode + var episode = title.Items.OfType().First(q => q.Id == animeEpisodeId); + return episode.CommonProperties; + } + else + { + var season = title.Items.OfType().First(q => q.Id == animeSeasonId); + var episode = season.Episodes.First(q => q.Id == animeEpisodeId); + return episode.CommonProperties; + } + } +} diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/CreateInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/CreateInteractor.cs new file mode 100644 index 0000000..7a099e5 --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/CreateInteractor.cs @@ -0,0 +1,100 @@ +using Modules.Library.Domain.Entities; +using Modules.Library.Domain.Entities.MediaContent.CommonProperties; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.Gateways; +using Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Episode; + +public class CreateInteractor : InteractorBase +{ + private readonly IAnimeTitleGateway _gateway; + + public CreateInteractor(IAnimeTitleGateway gateway, ILanguageGateway languageGateway, IGenreGateway genreGateway) : base(gateway, languageGateway, genreGateway) + { + this._gateway = gateway; + } + + public async Task Create(Guid animeTitleId, AnimeEpisodeType type, string nameOriginal, Guid nameOriginalLanguageId) + { + var title = await _gateway.GetByIdAsync(animeTitleId); + var episode = new AnimeEpisode(nameOriginal, nameOriginalLanguageId, type); + title.Items.Add(episode); + await _gateway.UpdateAsync(title); + return episode.Id; + } + + public async Task CreateAnimeEpisode(Guid animeTitleId, Guid? animeSeasonId, AnimeEpisodeType type, string nameOriginal, Guid nameOriginalLanguageId) + { + var title = await GetTitle(animeTitleId); + if (animeSeasonId.HasValue) + { + var season = GetSeason(title, animeSeasonId.Value); + var episode = new AnimeEpisode(nameOriginal, nameOriginalLanguageId, type); + season.Episodes.Add(episode); + await _gateway.UpdateAsync(title); + return episode.Id; + } + else + { + var episode = new AnimeEpisode(nameOriginal, nameOriginalLanguageId, type); + title.Items.Add(episode); + await _gateway.UpdateAsync(title); + return episode.Id; + } + } + + + public async Task AddName(Guid animeTitleId, Guid? animeSeasonId, Guid animeEpisodeId, NameType nameType, Guid languageId, string value) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddName(title, animeSeasonId, animeEpisodeId, nameType, languageId, value); + await _gateway.UpdateAsync(title); + } + + internal async Task AddDescription(Guid animeTitleId, Guid? animeSeasonId, Guid animeEpisodeId, Guid languageId, bool isOriginal, string value) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddDescription(title, animeSeasonId, animeEpisodeId, languageId, isOriginal, value); + await _gateway.UpdateAsync(title); + } + + internal async Task AddRelatedContent(Guid animeTitleId, Guid? animeSeasonId, Guid animeEpisodeId, string url, MediaInfoType type) + { + var title = await GetTitle(animeTitleId); + CommonPropertiesService.AddRelatedContent(title, animeSeasonId, animeEpisodeId, url, type); + await _gateway.UpdateAsync(title); + } + + internal async Task AddGenreProportion(Guid animeTitleId, Guid? animeSeasonId, Guid animeEpisodeId, Guid genreId, decimal? proportion) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddGenre(title, animeSeasonId, animeEpisodeId, genreId, proportion); + await _gateway.UpdateAsync(title); + } + + + internal async Task SetAnnouncementDate(Guid animeTitleId, Guid? animeSeasonId, Guid animeEpisodeId, DateTimeOffset value) + { + var title = await _gateway.GetByIdAsync(animeTitleId); + CommonPropertiesService.SetAnnouncementDate(title, animeSeasonId, animeEpisodeId, value); + OnStructureActionCompleted(title, animeSeasonId.HasValue ? GetSeason(title, animeSeasonId.Value) : null); + await _gateway.UpdateAsync(title); + } + + internal async Task SetEstimatedReleaseDate(Guid animeTitleId, Guid? animeSeasonId, Guid? animeEpisodeId, DateTimeOffset value) + { + var title = await _gateway.GetByIdAsync(animeTitleId); + CommonPropertiesService.SetEstimatedReleaseDate(title, animeSeasonId, animeEpisodeId, value); + OnStructureActionCompleted(title, animeSeasonId.HasValue ? GetSeason(title, animeSeasonId.Value) : null); + await _gateway.UpdateAsync(title); + } + + internal async Task SetReleaseDate(Guid animeTitleId, Guid? animeSeasonId, Guid? animeEpisodeId, DateTimeOffset value) + { + var title = await _gateway.GetByIdAsync(animeTitleId); + CommonPropertiesService.SetReleaseDate(title, animeSeasonId, animeEpisodeId, value); + OnStructureActionCompleted(title, animeSeasonId.HasValue ? GetSeason(title, animeSeasonId.Value) : null); + await _gateway.UpdateAsync(title); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/DeleteInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/DeleteInteractor.cs new file mode 100644 index 0000000..ee38f94 --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/DeleteInteractor.cs @@ -0,0 +1,15 @@ +using Modules.Library.Domain.Gateways; +using Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Episode; + +public class DeleteInteractor(IAnimeTitleGateway gateway, ILanguageGateway languageGateway, IGenreGateway genreGateway) : + InteractorBase(gateway, languageGateway, genreGateway) +{ + public async Task Delete(Guid animeTitleId, Guid? animeSeasonId, Guid animeEpisodeId) + { + var title = await GetTitle(animeTitleId); + var episode = GetEpisode(title, animeSeasonId.HasValue ? GetSeason(title, animeSeasonId.Value) : null, animeEpisodeId); + episode.SetDeleted(); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/EditInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/EditInteractor.cs new file mode 100644 index 0000000..a624b8a --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Episode/EditInteractor.cs @@ -0,0 +1,22 @@ +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Episode; + +public class EditInteractor(IAnimeTitleGateway gateway) +{ + + #region CommonProperties + #endregion + + #region Name + #endregion + + #region Description + #endregion + + #region Genre + #endregion + + #region RelatedContent + #endregion +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/CreateInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/CreateInteractor.cs new file mode 100644 index 0000000..c106232 --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/CreateInteractor.cs @@ -0,0 +1,82 @@ +using Modules.Library.Domain.Entities; +using Modules.Library.Domain.Entities.MediaContent.CommonProperties; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.Gateways; +using Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Season; + +public class CreateInteractor : InteractorBase +{ + private readonly IAnimeTitleGateway _gateway; + + public CreateInteractor(IAnimeTitleGateway gateway, ILanguageGateway languageGateway, IGenreGateway genreGateway) : base(gateway, languageGateway, genreGateway) + { + this._gateway = gateway; + } + + public async Task Create(Guid animeTitleId, string nameOriginal, Guid nameOriginalLanguageId) + { + var title = await GetTitle(animeTitleId); + var season = new AnimeSeason(nameOriginal, nameOriginalLanguageId); + title.Items.Add(season); + await _gateway.UpdateAsync(title); + return season.Id; + } + + public async Task AddName(Guid animeTitleId, Guid? animeSeasonId, NameType nameType, Guid languageId, string value) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddName(title, animeSeasonId, null, nameType, languageId, value); + await _gateway.UpdateAsync(title); + } + + internal async Task AddDescription(Guid animeTitleId, Guid? animeSeasonId, Guid languageId, bool isOriginal, string value) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddDescription(title, animeSeasonId, null, languageId, isOriginal, value); + await _gateway.UpdateAsync(title); + } + + internal async Task AddRelatedContent(Guid animeTitleId, Guid animeSeasonId, string url, MediaInfoType type) + { + var title = await GetTitle(animeTitleId); + CommonPropertiesService.AddRelatedContent(title, animeSeasonId, null, url, type); + await _gateway.UpdateAsync(title); + } + + internal async Task AddGenreProportionToAnimeTitle(Guid animeTitleId, Guid genreId, decimal? proportion) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddGenre(title, null, null, genreId, proportion); + await _gateway.UpdateAsync(title); + } + + + internal async Task SetAnnouncementDate(Guid animeTitleId, Guid animeSeasonId, DateTimeOffset value) + { + var title = await _gateway.GetByIdAsync(animeTitleId); + CommonPropertiesService.SetAnnouncementDate(title, animeSeasonId, null, value); + var season = title.Items.OfType().First(q => q.Id == animeSeasonId); + OnStructureActionCompleted(title, season); + await _gateway.UpdateAsync(title); + } + + internal async Task SetEstimatedReleaseDate(Guid animeTitleId, Guid animeSeasonId, DateTimeOffset value) + { + var title = await _gateway.GetByIdAsync(animeTitleId); + CommonPropertiesService.SetEstimatedReleaseDate(title, animeSeasonId, null, value); + var season = title.Items.OfType().First(q => q.Id == animeSeasonId); + OnStructureActionCompleted(title, season); + await _gateway.UpdateAsync(title); + } + + internal async Task SetReleaseDate(Guid animeTitleId, Guid animeSeasonId, DateTimeOffset value) + { + var title = await _gateway.GetByIdAsync(animeTitleId); + CommonPropertiesService.SetReleaseDate(title, animeSeasonId, null, value); + var season = title.Items.OfType().First(q => q.Id == animeSeasonId); + OnStructureActionCompleted(title, season); + await _gateway.UpdateAsync(title); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/DeleteInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/DeleteInteractor.cs new file mode 100644 index 0000000..2a025f1 --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/DeleteInteractor.cs @@ -0,0 +1,7 @@ +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Season; + +public class DeleteInteractor(IAnimeTitleGateway gateway) +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/EditInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/EditInteractor.cs new file mode 100644 index 0000000..087b51a --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Season/EditInteractor.cs @@ -0,0 +1,22 @@ +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Season; + +public class EditInteractor(IAnimeTitleGateway gateway) +{ + + #region CommonProperties + #endregion + + #region Name + #endregion + + #region Description + #endregion + + #region Genre + #endregion + + #region RelatedContent + #endregion +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/CreateInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/CreateInteractor.cs new file mode 100644 index 0000000..bfda4e0 --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/CreateInteractor.cs @@ -0,0 +1,77 @@ +using Modules.Library.Domain.Entities; +using Modules.Library.Domain.Entities.MediaContent.CommonProperties; +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +public class CreateInteractor : InteractorBase +{ + private readonly IAnimeTitleGateway _gateway; + + public CreateInteractor(IAnimeTitleGateway gateway, ILanguageGateway languageGateway, IGenreGateway genreGateway) : base(gateway, languageGateway, genreGateway) + { + this._gateway = gateway; + } + + public async Task Create(string nameOriginal, Guid nameOriginalLanguageId) + { + var entity = new AnimeTitle(nameOriginal, nameOriginalLanguageId); + return await _gateway.AddAsync(entity); + } + + + public async Task AddName(Guid animeTitleId, NameType nameType, Guid languageId, string value) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddName(title, null, null, nameType, languageId, value); + await _gateway.UpdateAsync(title); + } + + internal async Task AddDescription(Guid animeTitleId, Guid languageId, bool isOriginal, string value) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddDescription(title, null, null, languageId, isOriginal, value); + await _gateway.UpdateAsync(title); + } + + internal async Task AddRelatedContent(Guid animeTitleId, string url, MediaInfoType type) + { + var title = await GetTitle(animeTitleId); + CommonPropertiesService.AddRelatedContent(title, null, null, url, type); + await _gateway.UpdateAsync(title); + } + + internal async Task AddGenreProportion(Guid animeTitleId, Guid genreId, decimal? proportion) + { + var title = await GetTitle(animeTitleId); + await _commonPropertiesService.AddGenre(title, null, null, genreId, proportion); + await _gateway.UpdateAsync(title); + } + + + internal async Task SetAnnouncementDate(Guid animeTitleId, DateTimeOffset value) + { + var title = await GetTitle(animeTitleId); + CommonPropertiesService.SetAnnouncementDate(title, null, null, value); + OnStructureActionCompleted(title); + await _gateway.UpdateAsync(title); + } + + + internal async Task SetEstimatedReleaseDate(Guid animeTitleId, Guid? animeSeasonId, Guid? animeEpisodeId, DateTimeOffset value) + { + var title = await GetTitle(animeTitleId); + CommonPropertiesService.SetEstimatedReleaseDate(title, null, null, value); + OnStructureActionCompleted(title); + await _gateway.UpdateAsync(title); + } + + internal async Task SetReleaseDate(Guid animeTitleId, Guid? animeSeasonId, Guid? animeEpisodeId, DateTimeOffset value) + { + var title = await GetTitle(animeTitleId); + CommonPropertiesService.SetReleaseDate(title, null, null, value); + OnStructureActionCompleted(title); + await _gateway.UpdateAsync(title); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/DeleteInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/DeleteInteractor.cs new file mode 100644 index 0000000..6e96efc --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/DeleteInteractor.cs @@ -0,0 +1,7 @@ +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +public class DeleteInteractor(IAnimeTitleGateway gateway) +{ +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/EditInteractor.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/EditInteractor.cs new file mode 100644 index 0000000..cc723e7 --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/EditInteractor.cs @@ -0,0 +1,22 @@ +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +public class EditInteractor(IAnimeTitleGateway gateway) +{ + + #region CommonProperties + #endregion + + #region Name + #endregion + + #region Description + #endregion + + #region Genre + #endregion + + #region RelatedContent + #endregion +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/InteractorBase.cs b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/InteractorBase.cs new file mode 100644 index 0000000..37269ac --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaContent/Anime/Title/InteractorBase.cs @@ -0,0 +1,48 @@ +using Modules.Library.Domain.Entities.MediaContent.Items.Anime; +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors.MediaContent.Anime.Title; + +public abstract class InteractorBase(IAnimeTitleGateway gateway, ILanguageGateway languageGateway, IGenreGateway genreGateway) +{ + internal readonly CommonPropertiesService _commonPropertiesService = new(languageGateway, genreGateway); + + protected async Task GetTitle(Guid titleId) => await gateway.GetByIdAsync(titleId); + protected static AnimeSeason GetSeason(AnimeTitle title, Guid seasonId) + { + return title.Items.OfType().First(q => q.Id == seasonId); + } + protected static AnimeEpisode GetEpisode(AnimeTitle title, AnimeSeason? season, Guid episodeId) + { + if (season != null) return season.Episodes.First(q => q.Id == episodeId); + else return title.Items.OfType().First(q => q.Id == episodeId); + } + + protected static void OnStructureActionCompleted(AnimeTitle title, AnimeSeason? season = null) + { + if (season != null) CheckIfAnimeSeasonCompleted(title, season); + CheckIfAnimeTitleCompleted(title); + } + + private static void CheckIfAnimeTitleCompleted(AnimeTitle title) + { + var itemsQuery = title.Items.AsQueryable(); + var ucompletedSeasons = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + title.Completed = !(ucompletedSeasons || lastEpisodeReleaseDate >= DateTime.UtcNow - title.ExpirationTime); + } + + private static void CheckIfAnimeSeasonCompleted(AnimeTitle title, AnimeSeason season) + { + var itemsQuery = title.Items.AsQueryable(); + var ucompletedSeasons = itemsQuery + .Any(q => !q.CommonProperties.ReleaseDate.HasValue); + var lastEpisodeReleaseDate = itemsQuery + .OrderByDescending(q => q.CommonProperties.ReleaseDate) + .FirstOrDefault()?.CommonProperties.ReleaseDate; + title.Completed = !(ucompletedSeasons || lastEpisodeReleaseDate >= DateTime.UtcNow - title.ExpirationTime); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Interactors/MediaInfoInteractor.cs b/Modules.Library.Domain/Interactors/MediaInfoInteractor.cs new file mode 100644 index 0000000..c10801c --- /dev/null +++ b/Modules.Library.Domain/Interactors/MediaInfoInteractor.cs @@ -0,0 +1,40 @@ +using Modules.Library.Domain.Entities; +using Modules.Library.Domain.Entities.MediaContent; +//using Modules.Library.Domain.Exceptions.MediaContent; +using Modules.Library.Domain.Gateways; + +namespace Modules.Library.Domain.Interactors; + +public class MediaInfoInteractor(IMediaInfoGateway gateway) +{ + public async Task Create(string url, MediaInfoType type) + { + //if (!user.CanAddMediaInfo()) throw new Exception("AccessDenied"); + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(url); + //if (await gateway.GetFirstOrDefaultWhere(q => q.Name == name) != null) throw new MediaInfoIsAlreadyExistException(); + var newMediaInfo = new MediaInfo(url, type); + return await gateway.AddAsync(newMediaInfo); + } + + public async Task Edit(Guid mediaInfoId, string url, MediaInfoType type) + { + //if (!user.CanEditMediaInfo()) throw new Exception("AccessDenied"); + if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(url); + var mediaInfo = await gateway.GetByIdAsync(mediaInfoId); + //if (await gateway.GetFirstOrDefaultWhere(q => q.Name == name) != null) throw new MediaInfoWithSameUrlIsAlreadyExistException(); + mediaInfo.SetUrl(url); + mediaInfo.SetType(type); + if (!await gateway.UpdateAsync(mediaInfo)) throw new Exception("Save unsuccessfull"); + } + + + public async Task Delete(Guid mediaInfoId) + { + //if (!user.CanRemoveMediaInfo()) throw new Exception("AccessDenied"); + var mediaInfo = await gateway.GetByIdAsync(mediaInfoId); + //await gateway.DeleteAsync(genre); + if (mediaInfo.Deleted) throw new Exception("AlreadyDeleted"); + mediaInfo.SetDeleted(); + if (!await gateway.UpdateAsync(mediaInfo)) throw new Exception("Save unsuccessfull"); + } +} \ No newline at end of file diff --git a/Modules.Library.Domain/Modules.Library.Domain.csproj b/Modules.Library.Domain/Modules.Library.Domain.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Modules.Library.Domain/Modules.Library.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Modules.Library.WebApi/Controllers/WeatherForecastController.cs b/Modules.Library.WebApi/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..e78ca4f --- /dev/null +++ b/Modules.Library.WebApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Modules.Library.Application.Services; +using Modules.Library.Application.Services.MediaContent; + +namespace Modules.Library.WebApi.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly LanguageService _languageService; + private readonly AnimeTitleService _animeTitleService; + private readonly ILogger _logger; + + public WeatherForecastController(LanguageService languageService, AnimeTitleService animeTitleService, ILogger logger) + { + _languageService = languageService; + _animeTitleService = animeTitleService; + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + + [HttpPost(Name = "A")] + public async Task A() + { + var newLanguageId = await _languageService.Add("RU", "Ðóñèéñêîâûé", null); + return await _animeTitleService.New("IJNF E FIJNF", newLanguageId); + } +} diff --git a/Modules.Library.WebApi/Modules.Library.WebApi.csproj b/Modules.Library.WebApi/Modules.Library.WebApi.csproj new file mode 100644 index 0000000..cf8d47c --- /dev/null +++ b/Modules.Library.WebApi/Modules.Library.WebApi.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/Modules.Library.WebApi/Modules.Library.WebApi.http b/Modules.Library.WebApi/Modules.Library.WebApi.http new file mode 100644 index 0000000..7294c69 --- /dev/null +++ b/Modules.Library.WebApi/Modules.Library.WebApi.http @@ -0,0 +1,6 @@ +@Modules.Library.WebApi_HostAddress = http://localhost:5198 + +GET {{Modules.Library.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Modules.Library.WebApi/Program.cs b/Modules.Library.WebApi/Program.cs new file mode 100644 index 0000000..29a0493 --- /dev/null +++ b/Modules.Library.WebApi/Program.cs @@ -0,0 +1,36 @@ +using Modules.Library.Application; +using Modules.Library.Database; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. +builder.Services.AddDatabase("mongodb://localhost:27017", "MyBookmarkDb"); + +builder.Services.AddApplicationServices(); + + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Modules.Library.WebApi/Properties/launchSettings.json b/Modules.Library.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..f9d6cd7 --- /dev/null +++ b/Modules.Library.WebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9360", + "sslPort": 44394 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5198", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7057;http://localhost:5198", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Modules.Library.WebApi/WeatherForecast.cs b/Modules.Library.WebApi/WeatherForecast.cs new file mode 100644 index 0000000..0a650d0 --- /dev/null +++ b/Modules.Library.WebApi/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace Modules.Library.WebApi; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/Modules.Library.WebApi/appsettings.Development.json b/Modules.Library.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Modules.Library.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Modules.Library.WebApi/appsettings.json b/Modules.Library.WebApi/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Modules.Library.WebApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Modules.Media.Core/Class1.cs b/Modules.Media.Core/Class1.cs new file mode 100644 index 0000000..d117cfa --- /dev/null +++ b/Modules.Media.Core/Class1.cs @@ -0,0 +1,7 @@ +namespace Modules.Media.Core +{ + public class Class1 + { + + } +} diff --git a/Modules.Media.Core/Domain/MediaInfo.cs b/Modules.Media.Core/Domain/MediaInfo.cs new file mode 100644 index 0000000..444fcb4 --- /dev/null +++ b/Modules.Media.Core/Domain/MediaInfo.cs @@ -0,0 +1,18 @@ +using Common.Domain.Abstractions; + +namespace Modules.Media.Core.Domain; + +public class MediaInfo : Entity +{ + public MediaInfoType Type { get; set; } = MediaInfoType.OtherFile; + //public string ContentType { get; set; } = default!; + public string Url { get; set; } = default!; +} + +public enum MediaInfoType +{ + Image, + Video, + Link, + OtherFile +} \ No newline at end of file diff --git a/Modules.Media.Core/Modules.Media.Core.csproj b/Modules.Media.Core/Modules.Media.Core.csproj new file mode 100644 index 0000000..dfd8e74 --- /dev/null +++ b/Modules.Media.Core/Modules.Media.Core.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/MyBookmark.AppHost/MyBookmark.AppHost.csproj b/MyBookmark.AppHost/MyBookmark.AppHost.csproj new file mode 100644 index 0000000..10c0069 --- /dev/null +++ b/MyBookmark.AppHost/MyBookmark.AppHost.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + true + 071e9371-93a9-4a01-8dd5-d848b63ba79a + + + + + + + + + + + + diff --git a/MyBookmark.AppHost/Program.cs b/MyBookmark.AppHost/Program.cs new file mode 100644 index 0000000..efc8f3a --- /dev/null +++ b/MyBookmark.AppHost/Program.cs @@ -0,0 +1,7 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddProject("mybookmark"); + +builder.AddProject("modules-library-webapi"); + +builder.Build().Run(); diff --git a/MyBookmark.AppHost/Properties/launchSettings.json b/MyBookmark.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..e9ad4d0 --- /dev/null +++ b/MyBookmark.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17196;http://localhost:15227", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21174", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22166" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15227", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19181", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20243" + } + } + } +} diff --git a/MyBookmark.AppHost/appsettings.Development.json b/MyBookmark.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MyBookmark.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MyBookmark.AppHost/appsettings.json b/MyBookmark.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/MyBookmark.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/MyBookmark.ServiceDefaults/Extensions.cs b/MyBookmark.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..ce94dc2 --- /dev/null +++ b/MyBookmark.ServiceDefaults/Extensions.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/MyBookmark.ServiceDefaults/MyBookmark.ServiceDefaults.csproj b/MyBookmark.ServiceDefaults/MyBookmark.ServiceDefaults.csproj new file mode 100644 index 0000000..84e9efc --- /dev/null +++ b/MyBookmark.ServiceDefaults/MyBookmark.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/MyBookmark.sln b/MyBookmark.sln new file mode 100644 index 0000000..5a3f52b --- /dev/null +++ b/MyBookmark.sln @@ -0,0 +1,128 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35219.272 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyBookmark", "MyBookmark\MyBookmark.csproj", "{DF9EE371-2865-4FCF-8700-F8DBF9CEFDFA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyBookmark.AppHost", "MyBookmark.AppHost\MyBookmark.AppHost.csproj", "{FC4343FD-8779-4A6F-894E-4D381A2483DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyBookmark.ServiceDefaults", "MyBookmark.ServiceDefaults\MyBookmark.ServiceDefaults.csproj", "{C23F1895-98CB-45EA-A2D2-D452D210365C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "User", "User", "{A006C232-0F12-4B56-9EA0-D8771ABE49AA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Account", "Account", "{9B8DB13A-9595-419D-83F3-7C537B903D9F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Views", "Views", "{A1136847-87B7-494C-8694-5BF759DFBB04}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Search", "Search", "{5BC05B7D-F17B-4776-91CA-A7552FD294EA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Rates", "Rates", "{C3D36A31-0F02-4E15-A57D-895E714FE8A7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UserNotes", "UserNotes", "{1A6524E9-DA15-4044-B29F-BFB337F490EE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ContentLists", "ContentLists", "{480FABB0-1306-46A6-9DCF-CB2FC81B8932}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Administration", "Administration", "{68EC1D53-93FA-4FDE-82CD-23F56A8CB881}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{D034BC31-002F-4F3D-B2C1-9E81B990C51A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modules.Library.Core", "Modules.Library.Core\Modules.Library.Core.csproj", "{CB83CDAB-2392-4E24-B109-8571863F1A85}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{BACBF195-FD74-4F57-AF8B-D8752C1EBBF0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Domain", "Common.Domain\Common.Domain.csproj", "{A58D5C12-C538-4039-AC43-8358EE126C5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Media", "Media", "{89F51AA1-EEA6-4BD1-B592-95313A146AEB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modules.Media.Core", "Modules.Media.Core\Modules.Media.Core.csproj", "{21319E69-EF88-44CC-8CAF-C163E0EB10CB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modules.Library.Domain", "Modules.Library.Domain\Modules.Library.Domain.csproj", "{4F0E4088-A9B6-44B9-851F-5170323BA34F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modules.Library.Application", "Modules.Library.Application\Modules.Library.Application.csproj", "{2A0C877B-8C39-4B91-A996-443A19C34FF6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{8008ADE4-84F8-41D9-B263-C75C86FA1FE8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modules.Library.Database", "Modules.Library.Database\Modules.Library.Database.csproj", "{43731C43-2BA9-4F2F-8A0F-2F157F05D5A7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Modules.Library.WebApi", "Modules.Library.WebApi\Modules.Library.WebApi.csproj", "{6DC1B759-12F6-415D-BCAC-60E9E15B1074}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DF9EE371-2865-4FCF-8700-F8DBF9CEFDFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF9EE371-2865-4FCF-8700-F8DBF9CEFDFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF9EE371-2865-4FCF-8700-F8DBF9CEFDFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF9EE371-2865-4FCF-8700-F8DBF9CEFDFA}.Release|Any CPU.Build.0 = Release|Any CPU + {FC4343FD-8779-4A6F-894E-4D381A2483DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC4343FD-8779-4A6F-894E-4D381A2483DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC4343FD-8779-4A6F-894E-4D381A2483DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC4343FD-8779-4A6F-894E-4D381A2483DF}.Release|Any CPU.Build.0 = Release|Any CPU + {C23F1895-98CB-45EA-A2D2-D452D210365C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C23F1895-98CB-45EA-A2D2-D452D210365C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C23F1895-98CB-45EA-A2D2-D452D210365C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C23F1895-98CB-45EA-A2D2-D452D210365C}.Release|Any CPU.Build.0 = Release|Any CPU + {CB83CDAB-2392-4E24-B109-8571863F1A85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB83CDAB-2392-4E24-B109-8571863F1A85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB83CDAB-2392-4E24-B109-8571863F1A85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB83CDAB-2392-4E24-B109-8571863F1A85}.Release|Any CPU.Build.0 = Release|Any CPU + {A58D5C12-C538-4039-AC43-8358EE126C5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A58D5C12-C538-4039-AC43-8358EE126C5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A58D5C12-C538-4039-AC43-8358EE126C5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A58D5C12-C538-4039-AC43-8358EE126C5D}.Release|Any CPU.Build.0 = Release|Any CPU + {21319E69-EF88-44CC-8CAF-C163E0EB10CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21319E69-EF88-44CC-8CAF-C163E0EB10CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21319E69-EF88-44CC-8CAF-C163E0EB10CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21319E69-EF88-44CC-8CAF-C163E0EB10CB}.Release|Any CPU.Build.0 = Release|Any CPU + {4F0E4088-A9B6-44B9-851F-5170323BA34F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F0E4088-A9B6-44B9-851F-5170323BA34F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F0E4088-A9B6-44B9-851F-5170323BA34F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F0E4088-A9B6-44B9-851F-5170323BA34F}.Release|Any CPU.Build.0 = Release|Any CPU + {2A0C877B-8C39-4B91-A996-443A19C34FF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A0C877B-8C39-4B91-A996-443A19C34FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A0C877B-8C39-4B91-A996-443A19C34FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A0C877B-8C39-4B91-A996-443A19C34FF6}.Release|Any CPU.Build.0 = Release|Any CPU + {43731C43-2BA9-4F2F-8A0F-2F157F05D5A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43731C43-2BA9-4F2F-8A0F-2F157F05D5A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43731C43-2BA9-4F2F-8A0F-2F157F05D5A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43731C43-2BA9-4F2F-8A0F-2F157F05D5A7}.Release|Any CPU.Build.0 = Release|Any CPU + {6DC1B759-12F6-415D-BCAC-60E9E15B1074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6DC1B759-12F6-415D-BCAC-60E9E15B1074}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6DC1B759-12F6-415D-BCAC-60E9E15B1074}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6DC1B759-12F6-415D-BCAC-60E9E15B1074}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {A006C232-0F12-4B56-9EA0-D8771ABE49AA} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {9B8DB13A-9595-419D-83F3-7C537B903D9F} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {A1136847-87B7-494C-8694-5BF759DFBB04} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {5BC05B7D-F17B-4776-91CA-A7552FD294EA} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {C3D36A31-0F02-4E15-A57D-895E714FE8A7} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {1A6524E9-DA15-4044-B29F-BFB337F490EE} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {480FABB0-1306-46A6-9DCF-CB2FC81B8932} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {68EC1D53-93FA-4FDE-82CD-23F56A8CB881} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {D034BC31-002F-4F3D-B2C1-9E81B990C51A} = {036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8} + {CB83CDAB-2392-4E24-B109-8571863F1A85} = {036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8} + {A58D5C12-C538-4039-AC43-8358EE126C5D} = {BACBF195-FD74-4F57-AF8B-D8752C1EBBF0} + {89F51AA1-EEA6-4BD1-B592-95313A146AEB} = {9ED4C881-CF0A-4F56-8314-ED7AF2E17DDB} + {21319E69-EF88-44CC-8CAF-C163E0EB10CB} = {89F51AA1-EEA6-4BD1-B592-95313A146AEB} + {4F0E4088-A9B6-44B9-851F-5170323BA34F} = {8008ADE4-84F8-41D9-B263-C75C86FA1FE8} + {2A0C877B-8C39-4B91-A996-443A19C34FF6} = {8008ADE4-84F8-41D9-B263-C75C86FA1FE8} + {8008ADE4-84F8-41D9-B263-C75C86FA1FE8} = {036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8} + {43731C43-2BA9-4F2F-8A0F-2F157F05D5A7} = {D034BC31-002F-4F3D-B2C1-9E81B990C51A} + {6DC1B759-12F6-415D-BCAC-60E9E15B1074} = {036DC6C2-18A3-49DB-92D3-6DFCDCE3D5F8} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4D16F124-695A-4782-BEFB-AAAFA0953732} + EndGlobalSection +EndGlobal diff --git a/MyBookmark/Controllers/WeatherForecastController.cs b/MyBookmark/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..b07f14b --- /dev/null +++ b/MyBookmark/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace MyBookmark.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/MyBookmark/Dockerfile b/MyBookmark/Dockerfile new file mode 100644 index 0000000..abf145f --- /dev/null +++ b/MyBookmark/Dockerfile @@ -0,0 +1,30 @@ +# Zobacz https://aka.ms/customizecontainer, aby dowiedzieć siÄ™, jak dostosować kontener debugowania i jak program Visual Studio używa tego pliku Dockerfile do kompilowania obrazów w celu szybszego debugowania. + +# Ten etap jest używany podczas uruchamiania z programu VS w trybie szybkim (domyÅ›lnie dla konfiguracji debugowania) +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + + +# Ten etap sÅ‚uży do kompilowania projektu usÅ‚ugi +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["MyBookmark/MyBookmark.csproj", "MyBookmark/"] +RUN dotnet restore "./MyBookmark/MyBookmark.csproj" +COPY . . +WORKDIR "/src/MyBookmark" +RUN dotnet build "./MyBookmark.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# Ten etap sÅ‚uży do publikowania projektu usÅ‚ugi do skopiowania do etapu koÅ„cowego +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./MyBookmark.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Ten etap jest używany w Å›rodowisku produkcyjnym lub w przypadku uruchamiania z programu VS w trybie regularnym (domyÅ›lnie, gdy nie jest używana konfiguracja debugowania) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "MyBookmark.dll"] \ No newline at end of file diff --git a/MyBookmark/MyBookmark.csproj b/MyBookmark/MyBookmark.csproj new file mode 100644 index 0000000..4f0c045 --- /dev/null +++ b/MyBookmark/MyBookmark.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + 19da3ba1-7451-44ed-a31a-501659bfe4f7 + Linux + + + + + + + + + + + + diff --git a/MyBookmark/MyBookmark.http b/MyBookmark/MyBookmark.http new file mode 100644 index 0000000..1db706c --- /dev/null +++ b/MyBookmark/MyBookmark.http @@ -0,0 +1,6 @@ +@MyBookmark_HostAddress = http://localhost:5224 + +GET {{MyBookmark_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/MyBookmark/Program.cs b/MyBookmark/Program.cs new file mode 100644 index 0000000..edda8a7 --- /dev/null +++ b/MyBookmark/Program.cs @@ -0,0 +1,29 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/MyBookmark/Properties/launchSettings.json b/MyBookmark/Properties/launchSettings.json new file mode 100644 index 0000000..2b33af1 --- /dev/null +++ b/MyBookmark/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5224" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7093;http://localhost:5224" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59492", + "sslPort": 44307 + } + } +} \ No newline at end of file diff --git a/MyBookmark/WeatherForecast.cs b/MyBookmark/WeatherForecast.cs new file mode 100644 index 0000000..79dc0dc --- /dev/null +++ b/MyBookmark/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace MyBookmark; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/MyBookmark/appsettings.Development.json b/MyBookmark/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MyBookmark/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MyBookmark/appsettings.json b/MyBookmark/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/MyBookmark/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}