Compare commits

...

2 Commits

Author SHA1 Message Date
THE_KONDRAT
993f48d878 m 2024-09-04 23:13:52 +03:00
THE_KONDRAT
1a18a31fdd Dodaj pliki projektów. 2024-09-04 23:08:56 +03:00
190 changed files with 6235 additions and 0 deletions

30
.dockerignore Normal file
View File

@ -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/**

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

1
.gitignore vendored
View File

@ -30,6 +30,7 @@ x86/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/

View File

@ -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<INotification> _domainEvents = [];
public IReadOnlyCollection<INotification> 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);
}
}

View File

@ -0,0 +1,3 @@
namespace Common.Domain.Abstractions;
public interface IAggregateRoot { }

7
Common.Domain/Class1.cs Normal file
View File

@ -0,0 +1,7 @@
namespace Common.Domain
{
public class Class1
{
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
using Modules.Library.Application.Interfaces;
namespace Modules.Library.Application.Gateways;
public interface IAnimeUserGateway
{
public IAnimeUser GetUserById(Guid Id);
}

View File

@ -0,0 +1,36 @@
using Modules.Library.Domain.Entities;
using System.Linq.Expressions;
namespace Modules.Library.Application.Gateways;
public interface IRepository<T, TKey>
{
Task<List<T>> GetAllAsync();
Task<T> GetByIdAsync(TKey id);
Task<T?> GetByIdOrDefaultAsync(TKey id);
Task<List<T>> GetRangeByIdsAsync(List<TKey> ids);
Task<T> GetFirstWhere(Expression<Func<T, bool>> predicate);
Task<T?> GetFirstOrDefaultWhere(Expression<Func<T, bool>> predicate);
Task<List<T>> GetWhere(Expression<Func<T, bool>> predicate);
Task<bool> AnyWhere(Expression<Func<T, bool>> predicate);
/*
Task AddAsync(T entity, IUser user);
Task<bool> UpdateAsync(T entity, IUser user);
Task<bool> DeleteAsync(T entity, IUser user);
*/
Task AddAsync(T entity);
Task<bool> UpdateAsync(T entity);
Task<bool> DeleteAsync(T entity);
}
public interface IRepository<T> : IRepository<T, string> where T : Entity { }
//public interface IRepository<T> : IRepository<T, string> { }

View File

@ -0,0 +1,8 @@
using Modules.Library.Application.Interfaces;
namespace Modules.Library.Application.Gateways;
public interface IUserGateway
{
public IUser GetUserById(Guid Id);
}

View File

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

View File

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

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Modules.Library.Domain\Modules.Library.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -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<AnimeTitleService>();
services.AddScoped<AnimeSeasonService>();
services.AddScoped<AnimeEpisodeService>();
services.AddScoped<AnimeEpisodeService>();
return services;
}
}

View File

@ -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<Guid> AddGenre(string name)
public async Task<Guid> 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);
}

View File

@ -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<Guid> AddGenre(string name)
//public async Task Add(IUser user, string codeIso2, string name, Guid? iconId)
public async Task<Guid> 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);
}

View File

@ -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<AnimeEpisode>() :
// title.Items.OfType<AnimeSeason>().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);
// }
}

View File

@ -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<AnimeEpisode>().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<AnimeEpisode>().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<AnimeTitle, AnimeSeason> 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);
// }
}

View File

@ -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<Guid> New(string nameOriginal, Guid nameOriginalLanguageId) =>
await createInteractor.Create(nameOriginal, nameOriginalLanguageId);
public async Task<Guid> AddSeason(Guid titleId, string nameOriginal, Guid nameOriginalLanguageId) =>
await createInteractor.CreateAnimeSeason(titleId, nameOriginal, nameOriginalLanguageId);
public async Task<Guid> 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<AnimeSeason>().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);
//}
}

View File

@ -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<Domain.Entities.MediaContent.CommonProperties.CommonProperties> GetCommonProperties(string commonPropertiesId)
{
return await commonPropertiesGateway.GetByMediaContentItemId(commonPropertiesId);
}
*/
}

View File

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

View File

@ -0,0 +1,8 @@
namespace Modules.Library.Core.Domain.Common;
public interface IOrderableItem
{
public uint Order { get; }
public void SetOrder(uint order);
}

View File

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

View File

@ -0,0 +1,7 @@
namespace Modules.Library.Core.Domain.Genre;
public class Genre : Entity
{
[Required]
public string Name { get; set; } = default!;
}

View File

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

View File

@ -0,0 +1,164 @@
using System.Transactions;
namespace Modules.Library.Core.Domain.MediaContent.CommonProperties;
public class CommonProperties : Entity
{
private List<NameItem> _names = [];
public IReadOnlyCollection<NameItem> Names => _names.AsReadOnly();
public MediaInfo? Preview { get; private set; }
private List<DescriptionItem> _descriptions = [];
public IReadOnlyCollection<DescriptionItem> Descriptions => _descriptions.AsReadOnly();
public List<Genre_Percentage> _genres = [];
public IReadOnlyCollection<Genre_Percentage> Genres => _genres.AsReadOnly();
public List<MediaInfo> _relatedContent = [];
public IReadOnlyCollection<MediaInfo> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
namespace Modules.Library.Core.Domain.MediaContent.Items.Anime;
public abstract class AnimeItem : MediaContentItem
{
}

View File

@ -0,0 +1,6 @@
namespace Modules.Library.Core.Domain.MediaContent.Items.Anime;
public abstract class AnimeItemSingle: AnimeItem, INotContainer
{
public TimeSpan? Duration { get; set; }
}

View File

@ -0,0 +1,44 @@
namespace Modules.Library.Core.Domain.MediaContent.Items.Anime;
public class AnimeSeason : AnimeItem, IContainer<AnimeItem>
{
public int? Number { get; set; }
public string Director { get; protected set; } = default!;
public string OriginCountry { get; protected set; } = default!;
protected List<AnimeItem> _items = [];
public IReadOnlyCollection<AnimeItem> 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();
}
}

View File

@ -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<IOrderableItem>()
.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<AnimeEpisode>()
.FirstOrDefault(q => q.Id == episodeId);
}
else
{
result = _items.OfType<AnimeEpisode>()
.FirstOrDefault(q => q.Id == episodeId);
}
return result ?? throw new Exception($"Could not find episode with id [{episodeId}]");
}
private AnimeSeason GetSeason(Guid seasonId) =>
_items.OfType<AnimeSeason>().FirstOrDefault(q => q.Id == seasonId) ??
throw new Exception($"Could not find season with id [{seasonId}]");
}

View File

@ -0,0 +1,6 @@
namespace Modules.Library.Core.Domain.MediaContent.Items;
public interface IContainer <T>
{
public IReadOnlyCollection<T> Items { get; }
}

View File

@ -0,0 +1,8 @@
namespace Modules.Library.Core.Domain.MediaContent.Items;
public interface INotContainer { }
public interface INotContainer<T>
{
public IContainer<T> Container { get; }
}

View File

@ -0,0 +1,8 @@
namespace Modules.Library.Core.Domain.MediaContent.Items;
public interface IVariantItem
{
public uint Variant { get; }
public void SetVariant(uint variant);
}

View File

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

View File

@ -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<MediaContentItem> _items = [];
public IReadOnlyCollection<MediaContentItem> 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;
}

View File

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

View File

@ -0,0 +1,8 @@
using Common.Domain.Abstractions;
namespace Modules.Library.Core.Domain.User;
public class User : Entity
{
}

View File

@ -0,0 +1,4 @@
global using Common.Domain.Abstractions;
global using Modules.Library.Core.Domain.Common;
global using System.ComponentModel.DataAnnotations;

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Domain\Models\MediaContent\" />
<Folder Include="EventHandlers\" />
<Folder Include="DomainEvents\" />
<Folder Include="Repositories\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common.Domain\Common.Domain.csproj" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

@ -0,0 +1,10 @@
namespace Modules.Library.Database.Database.Models.Anime;
public class AnimeSeasonq : AnimeItem1
{
public List<AnimeEpisode1> Episodes { get; set; } = [];
public int? Number { get; set; }
public string? Director { get; set; }
public string? OriginCountry { get; set; }
}

View File

@ -0,0 +1,9 @@
namespace Modules.Library.Database.Database.Models.Anime;
public class AnimeTitle1 : Entity1
{
public CommonProperties.CommonProperties1 CommonProperties { get; set; } = default!;
public List<AnimeItem1> Items { get; set; } = [];
public bool Completed { get; set; }
public TimeSpan ExpirationTime { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace Modules.Library.Database.Database.Models.CommonProperties;
public class CommonProperties1 : Entity1
{
public List<NameItem1> Names { get; set; } = [];
public MediaInfo1? Preview { get; set; }
public List<DescriptionItem1> Descriptions { get; set; } = [];
public List<GenreProportionItem1> Genres { get; set; } = [];
public List<MediaInfo1> RelatedContent { get; set; } = [];
public DateTimeOffset? AnnouncementDate { get; set; }
public DateTimeOffset? EstimatedReleaseDate { get; set; }
public DateTimeOffset? ReleaseDate { get; set; }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
namespace Modules.Library.Database.Database.Models.Genre;
public class Genre1 : Entity1
{
public string Name { get; set; } = default!;
}

View File

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

View File

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

View File

@ -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<TDocument> GetCollection<TDocument>(string collectionName)
{
if (!_initialized) throw new Exception(string.Concat(nameof(MongoDbContext), " has not initialized yet"));
return database.GetCollection<TDocument>(collectionName);
}
public IMongoCollection<Genre> Genres => GetCollection<Genre>(nameof(Genre));
public IMongoCollection<Language> Languages => GetCollection<Language>(nameof(Language));
public IMongoCollection<MediaInfo> MediaInfos => GetCollection<MediaInfo>(nameof(MediaInfo));
private const string _mediaContentItemsCollectionName = "MediaContentItems";
public IMongoCollection<AnimeTitle> AnimeTitles => GetCollection<AnimeTitle>(_mediaContentItemsCollectionName);
public void Initialize()
{
BsonClassMap.RegisterClassMap<AnimeTitle>(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<int> GetNextPaymentProviderId() => _paymentProviderIdSequence.GetNextSequenceValue(context);
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.28.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Modules.Library.Application\Modules.Library.Application.csproj" />
</ItemGroup>
</Project>

View File

@ -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<AnimeTitle>(context), IAnimeTitleGateway
{
protected override IMongoCollection<AnimeTitle> GetCollections(MongoDbContext context) => context.AnimeTitles;
protected override async Task<bool> 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<bool> AnyWhere(Expression<Func<Domain.Entities.MediaContent.Items.Anime.AnimeTitle, bool>> predicate)
{
var p = predicate.
}
public Task<bool> DeleteAsync(Domain.Entities.MediaContent.Items.Anime.AnimeTitle entity, IUser user)
{
throw new NotImplementedException();
}
public Task<Domain.Entities.MediaContent.Items.Anime.AnimeTitle> GetByIdAsync(string id)
{
throw new NotImplementedException();
}
public Task<Domain.Entities.MediaContent.Items.Anime.AnimeTitle?> GetByIdOrDefaultAsync(string id)
{
throw new NotImplementedException();
}
public Task<Domain.Entities.MediaContent.Items.Anime.AnimeTitle?> GetFirstOrDefaultWhere(Expression<Func<Domain.Entities.MediaContent.Items.Anime.AnimeTitle, bool>> predicate)
{
throw new NotImplementedException();
}
public Task<Domain.Entities.MediaContent.Items.Anime.AnimeTitle> GetFirstWhere(Expression<Func<Domain.Entities.MediaContent.Items.Anime.AnimeTitle, bool>> predicate)
{
throw new NotImplementedException();
}
public Task<List<Domain.Entities.MediaContent.Items.Anime.AnimeTitle>> GetRangeByIdsAsync(List<string> ids)
{
throw new NotImplementedException();
}
public Task<List<Domain.Entities.MediaContent.Items.Anime.AnimeTitle>> GetWhere(Expression<Func<Domain.Entities.MediaContent.Items.Anime.AnimeTitle, bool>> predicate)
{
throw new NotImplementedException();
}
public Task<bool> UpdateAsync(Domain.Entities.MediaContent.Items.Anime.AnimeTitle entity, IUser user)
{
throw new NotImplementedException();
}
Task<List<Domain.Entities.MediaContent.Items.Anime.AnimeTitle>> Application.Gateways.IRepository<Domain.Entities.MediaContent.Items.Anime.AnimeTitle, string>.GetAllAsync()
{
throw new NotImplementedException();
}
*/
}

View File

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

View File

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

View File

@ -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<Genre>(context), IGenreGateway
{
protected override IMongoCollection<Genre> GetCollections(MongoDbContext context) => context.Genres;
/*
protected override async Task<bool> SoftDeleteAsync(Genre entity)
{
//entity.Delete();
return await UpdateAsync(entity);
}
*/
}

View File

@ -0,0 +1,31 @@
using MongoDB.Bson;
using System.Linq.Expressions;
using Modules.Library.Domain.Entities;
namespace Modules.Library.Database.Repositories;
internal interface IRepository<T> : IRepository<T, Guid> where T : Entity { }
internal interface IRepository<T, TKey>
{
Task<List<T>> GetAllAsync();
Task<T> GetByIdAsync(TKey id);
Task<T?> GetByIdOrDefaultAsync(TKey id);
Task<List<T>> GetRangeByIdsAsync(List<TKey> ids);
Task<T> GetFirstWhere(Expression<Func<T, bool>> predicate);
Task<T?> GetFirstOrDefaultWhere(Expression<Func<T, bool>> predicate);
Task<List<T>> GetWhere(Expression<Func<T, bool>> predicate);
Task<bool> AnyWhere(Expression<Func<T, bool>> predicate);
Task<TKey> AddAsync(T entity);
Task<bool> UpdateAsync(T entity);
Task<bool> DeleteAsync(T entity);
}

View File

@ -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<Language>(context), ILanguageGateway
{
protected override IMongoCollection<Language> GetCollections(MongoDbContext context) => context.Languages;
}

View File

@ -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<MediaInfo>(context), IMediaInfoGateway
{
protected override IMongoCollection<MediaInfo> GetCollections(MongoDbContext context) => context.MediaInfos;
}

View File

@ -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<T>(MongoDbContext context) : IRepository<T> where T : Entity
{
protected abstract IMongoCollection<T> GetCollections(MongoDbContext context);
protected bool _useSoftDelete = true;
public async Task<Guid> AddAsync(T entity)
{
await GetCollections(context).InsertOneAsync(entity);
return entity.Id;
}
public async Task<bool> 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<bool> SoftDeleteAsync(T entity) => throw new NotImplementedException();
public async Task<List<T>> GetAllAsync() => await GetCollections(context).Find("{}").ToListAsync();
public async Task<T> GetByIdAsync(Guid id) => await GetCollections(context).Find(q => q.Id == id).SingleAsync();
public async Task<T?> GetByIdOrDefaultAsync(Guid id) => await GetCollections(context).Find(q => q.Id == id).SingleOrDefaultAsync();
public async Task<T> GetFirstWhere(Expression<Func<T, bool>> predicate) =>
await GetCollections(context).Find(predicate).SingleAsync();
public async Task<T?> GetFirstOrDefaultWhere(Expression<Func<T, bool>> predicate) =>
await GetCollections(context).Find(predicate).SingleOrDefaultAsync();
public async Task<List<T>> GetRangeByIdsAsync(List<Guid> ids) =>
await GetCollections(context).Find(q => ids.Contains(q.Id)).ToListAsync();
public async Task<List<T>> GetWhere(Expression<Func<T, bool>> predicate) =>
await GetCollections(context).Find(predicate).ToListAsync();
public async Task<bool> AnyWhere(Expression<Func<T, bool>> predicate) =>
await GetCollections(context).Find(predicate).AnyAsync();
public async Task<bool> 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;
}
}

View File

@ -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<MongoDbContext>(q =>
{
var context = new MongoDbContext(q.GetService<IMongoDatabase>());
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<MongoDbContext>();
services.AddScoped<MongoDbContext>(q =>
{
var context = new MongoDbContext(q.GetService<IMongoDatabase>());
context.Initialize();
return context;
});
AddRepositories(services);
return services;
}
private static void AddRepositories(IServiceCollection services)
{
services.AddScoped<IAnimeTitleGateway, AnimeTitleRepository>();
services.AddScoped<IGenreGateway, GenreRepository>();
services.AddScoped<ILanguageGateway, LanguageRepository>();
services.AddScoped<IMediaInfoGateway, MediaInfoRepository>();
}
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));
}
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
namespace Modules.Library.Domain.Entities;
public interface IOrderableItem
{
public ushort Order { get; }
public void SetOrder(ushort order);
}

View File

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

View File

@ -0,0 +1,22 @@
namespace Modules.Library.Domain.Entities.MediaContent.CommonProperties;
public class CommonProperties : Entity
{
public List<NameItem> Names { get; set; } = [];
public MediaInfo? Preview { get; set; }
public List<DescriptionItem> Descriptions { get; set; } = [];
public List<GenreProportionItem> Genres { get; set; } = [];
public List<MediaInfo> 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));
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
namespace Modules.Library.Domain.Entities.MediaContent.CommonProperties.ShadowEntities;
internal class CommonPropertiesShadow
{
internal List<NameItem> names = [];
internal MediaInfo? preview = null;
internal List<DescriptionItem> descriptions = [];
internal List<GenreProportionItem> genres = [];
internal List<MediaInfo> relatedContent = [];
internal DateTimeOffset? announcementDate = null;
internal DateTimeOffset? estimatedReleaseDate = null;
internal DateTimeOffset? releaseDate = null;
}

View File

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

View File

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

View File

@ -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) { }
}

View File

@ -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<AnimeEpisode> 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) { }
}

View File

@ -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<AnimeItem> _items = [];
public IReadOnlyList<AnimeItem> Items => _items.AsReadOnly();
*/
public List<AnimeItem> 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<AnimeEpisodeShadow>().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<ushort>(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<AnimeEpisodeShadow>().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<AnimeEpisodeShadow>()
.Where(q => q.order == order), currentVariant, newVariant);
internal void SetEpisodeCompleted(ushort order, ushort variant, bool value = true) =>
SetCompleted<AnimeEpisodeShadow>(order, variant, value);
internal void SetEpisodeExpirationTime(ushort order, ushort variant, TimeSpan value) =>
SetExpirationTime<AnimeEpisodeShadow>(order, variant, value);
public void AddSeason()
{
var episode = new AnimeSeasonShadow
{
order = AnimeItem.GetNewOrder(_shadow.items),
number = _shadow.items.OfType<AnimeSeasonShadow>().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<ushort>(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<AnimeEpisodeShadow>().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<AnimeSeasonShadow>(order, variant, value);
internal void SetSeasonExpirationTime(ushort order, ushort variant, TimeSpan value) =>
SetExpirationTime<AnimeSeasonShadow>(order, variant, value);
private void SetCompleted<T>(ushort order, ushort variant, bool value = true) where T : AnimeItemShadow
{
var item = GetItem<T>(order, variant);
item.completed = value;
CheckIsCompleted();
}
private void SetExpirationTime<T>(ushort order, ushort variant, TimeSpan value) where T : AnimeItemShadow
{
var item = GetItem<T>(order, variant);
item.expirationTime = value;
CheckIsCompleted();
}
private T GetItem<T>(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<T>()
.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();
}
}

View File

@ -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()) { }
}

View File

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

View File

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

View File

@ -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<AnimeEpisodeShadow> episodes => [];
internal int? number = null;
internal string? director = null;
internal string? originCountry = null;
internal AnimeSeasonShadow() : base(new CommonPropertiesShadow())
{
}
}

View File

@ -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<AnimeItemShadow> items = [];
internal bool completed = false;
internal TimeSpan expirationTime = TimeSpan.Zero;
internal bool deleted = false;
}

View File

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

View File

@ -0,0 +1,9 @@
namespace Modules.Library.Domain.Entities;
public enum MediaInfoType
{
Image,
Video,
Link,
OtherFile
}

View File

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

View File

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

View File

@ -0,0 +1,8 @@
namespace Modules.Library.Domain.EntitiesV0;
public interface IOrderableItem
{
public ushort Order { get; }
public void SetOrder(ushort order);
}

View File

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

View File

@ -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<NameItem> Names => _shadow.names.AsReadOnly();
public MediaInfo? Preview => _shadow.preview;
public IReadOnlyCollection<DescriptionItem> Descriptions => _shadow.descriptions.AsReadOnly();
public IReadOnlyCollection<GenreProportionItem> Genres => _shadow.genres.AsReadOnly();
public IReadOnlyCollection<MediaInfo> 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() { }
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
namespace Modules.Library.Domain.EntitiesV0.MediaContent.CommonProperties.ShadowEntities;
internal class CommonPropertiesShadow
{
internal List<NameItem> names = [];
internal MediaInfo? preview = null;
internal List<DescriptionItem> descriptions = [];
internal List<GenreProportionItem> genres = [];
internal List<MediaInfo> relatedContent = [];
internal DateTimeOffset? announcementDate = null;
internal DateTimeOffset? estimatedReleaseDate = null;
internal DateTimeOffset? releaseDate = null;
}

View File

@ -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<AnimeEpisodeShadow>()
.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,
}

View File

@ -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<AnimeItemShadow> items) =>
(ushort)(items.Select(q => (int)q.order).DefaultIfEmpty(-1).Max() + 1);
public abstract void SetVariant(ushort variant);
internal static ushort GetNewVariant(IEnumerable<AnimeItemShadow> items, ushort order) =>
(ushort)(items.Where(q => q.order == order)
.Select(q => (int)q.variant).DefaultIfEmpty(-1).Max() + 1);
internal static void Sort(IEnumerable<AnimeItemShadow> 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<AnimeItemShadow> items, ushort lastOrder, ushort newOrder)
{
if (newOrder < 0 || newOrder > items.Select(q => q.order).DefaultIfEmpty<ushort>(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<AnimeItemShadow> 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;
}

View File

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

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