diff --git a/FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj b/FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj index fae206f..2d82c93 100644 --- a/FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj +++ b/FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj @@ -8,6 +8,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/FictionArchive.Service.NovelService/GraphQL/Mutation.cs b/FictionArchive.Service.NovelService/GraphQL/Mutation.cs index 0a1ac91..7b014f5 100644 --- a/FictionArchive.Service.NovelService/GraphQL/Mutation.cs +++ b/FictionArchive.Service.NovelService/GraphQL/Mutation.cs @@ -1,107 +1,37 @@ using FictionArchive.Service.NovelService.Models.Enums; +using FictionArchive.Service.NovelService.Models.IntegrationEvents; using FictionArchive.Service.NovelService.Models.Localization; using FictionArchive.Service.NovelService.Models.Novels; using FictionArchive.Service.NovelService.Models.SourceAdapters; using FictionArchive.Service.NovelService.Services; using FictionArchive.Service.NovelService.Services.SourceAdapters; +using FictionArchive.Service.Shared.Services.EventBus; using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.NovelService.GraphQL; public class Mutation { - // TODO Make this kick off a job in the background somehow. Probably want to think of how jobs will work across services - // Also of course need to make it a proper 'upsert' - public async Task ImportNovel(string novelUrl, NovelServiceDbContext dbContext, - IEnumerable adapters) + public async Task ImportNovel(string novelUrl, IEventBus eventBus) { - NovelMetadata? metadata = null; - foreach (ISourceAdapter sourceAdapter in adapters) + var importNovelRequestEvent = new NovelUpdateRequestedEvent() { - if (await sourceAdapter.CanProcessNovel(novelUrl)) - { - metadata = await sourceAdapter.GetMetadata(novelUrl); - } - } - - if (metadata == null) - { - throw new NotSupportedException("The provided novel url is currently unsupported."); - } - - var systemTags = metadata.SystemTags.Select(tag => new NovelTag() - { - Key = tag, - DisplayName = LocalizationKey.CreateFromText(tag, metadata.RawLanguage), - TagType = TagType.System - }); - var sourceTags = metadata.SourceTags.Select(tag => new NovelTag() - { - Key = tag, - DisplayName = LocalizationKey.CreateFromText(tag, metadata.RawLanguage), - TagType = TagType.External - }); - - var addedNovel = dbContext.Novels.Add(new Novel() - { - Author = new Person() - { - Name = LocalizationKey.CreateFromText(metadata.AuthorName, metadata.RawLanguage), - ExternalUrl = metadata.AuthorUrl, - }, - RawLanguage = metadata.RawLanguage, - Url = metadata.Url, - ExternalId = metadata.ExternalId, - Chapters = metadata.Chapters.Select(chapter => - { - return new Chapter() - { - Order = chapter.Order, - Url = chapter.Url, - Revision = chapter.Revision, - Name = LocalizationKey.CreateFromText(chapter.Name, metadata.RawLanguage), - Body = new LocalizationKey() - { - Texts = new List() - } - }; - }).ToList(), - Description = LocalizationKey.CreateFromText(metadata.Description, metadata.RawLanguage), - Name = LocalizationKey.CreateFromText(metadata.Name, metadata.RawLanguage), - RawStatus = metadata.RawStatus, - Tags = sourceTags.Concat(systemTags).ToList(), - Source = new Source() - { - Name = metadata.SourceDescriptor.Name, - Url = metadata.SourceDescriptor.Url, - Key = metadata.SourceDescriptor.Key, - } - }); - await dbContext.SaveChangesAsync(); - - return addedNovel.Entity; + NovelUrl = novelUrl + }; + await eventBus.Publish(importNovelRequestEvent); + return importNovelRequestEvent; } - public async Task FetchChapterContents(uint novelId, + public async Task FetchChapterContents(uint novelId, uint chapterNumber, - NovelServiceDbContext dbContext, - IEnumerable sourceAdapters) + IEventBus eventBus) { - var novel = await dbContext.Novels.Where(novel => novel.Id == novelId) - .Include(novel => novel.Chapters) - .ThenInclude(chapter => chapter.Body) - .ThenInclude(body => body.Texts) - .Include(novel => novel.Source) - .FirstOrDefaultAsync(); - var chapter = novel.Chapters.Where(chapter => chapter.Order == chapterNumber).FirstOrDefault(); - var adapter = sourceAdapters.FirstOrDefault(adapter => adapter.SourceDescriptor.Key == novel.Source.Key); - var rawChapter = await adapter.GetRawChapter(chapter.Url); - chapter.Body.Texts.Add(new LocalizationText() + var chapterPullEvent = new ChapterPullRequestedEvent() { - Text = rawChapter, - Language = novel.RawLanguage - }); - await dbContext.SaveChangesAsync(); - return chapter; + NovelId = novelId, + ChapterNumber = chapterNumber + }; + await eventBus.Publish(chapterPullEvent); + return chapterPullEvent; } } \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Migrations/20251118045235_Initial.Designer.cs b/FictionArchive.Service.NovelService/Migrations/20251120012317_Initial.Designer.cs similarity index 81% rename from FictionArchive.Service.NovelService/Migrations/20251118045235_Initial.Designer.cs rename to FictionArchive.Service.NovelService/Migrations/20251120012317_Initial.Designer.cs index 800d85e..195f3c9 100644 --- a/FictionArchive.Service.NovelService/Migrations/20251118045235_Initial.Designer.cs +++ b/FictionArchive.Service.NovelService/Migrations/20251120012317_Initial.Designer.cs @@ -1,4 +1,5 @@ // +using System; using FictionArchive.Service.NovelService.Services; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -12,7 +13,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace FictionArchive.Service.NovelService.Migrations { [DbContext(typeof(NovelServiceDbContext))] - [Migration("20251118045235_Initial")] + [Migration("20251120012317_Initial")] partial class Initial { /// @@ -27,11 +28,9 @@ namespace FictionArchive.Service.NovelService.Migrations modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); @@ -41,16 +40,44 @@ namespace FictionArchive.Service.NovelService.Migrations b.HasKey("Id"); - b.ToTable("LocalizationKey"); + b.ToTable("LocalizationKeys"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineId") + .HasColumnType("bigint"); + + b.Property("KeyRequestedForTranslationId") + .HasColumnType("uuid"); + + b.Property("LastUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TranslateTo") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EngineId"); + + b.HasIndex("KeyRequestedForTranslationId"); + + b.ToTable("LocalizationRequests"); }); modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); @@ -61,8 +88,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("LocalizationKeyId") - .HasColumnType("bigint"); + b.Property("LocalizationKeyId") + .HasColumnType("uuid"); b.Property("Text") .IsRequired() @@ -88,8 +115,8 @@ namespace FictionArchive.Service.NovelService.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("BodyId") - .HasColumnType("bigint"); + b.Property("BodyId") + .HasColumnType("uuid"); b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); @@ -97,8 +124,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("NameId") - .HasColumnType("bigint"); + b.Property("NameId") + .HasColumnType("uuid"); b.Property("NovelId") .HasColumnType("bigint"); @@ -137,8 +164,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("DescriptionId") - .HasColumnType("bigint"); + b.Property("DescriptionId") + .HasColumnType("uuid"); b.Property("ExternalId") .IsRequired() @@ -147,8 +174,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("NameId") - .HasColumnType("bigint"); + b.Property("NameId") + .HasColumnType("uuid"); b.Property("RawLanguage") .HasColumnType("integer"); @@ -190,8 +217,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("DisplayNameId") - .HasColumnType("bigint"); + b.Property("DisplayNameId") + .HasColumnType("uuid"); b.Property("Key") .IsRequired() @@ -232,12 +259,13 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("Name") - .IsRequired() - .HasColumnType("text"); + b.Property("NameId") + .HasColumnType("uuid"); b.HasKey("Id"); + b.HasIndex("NameId"); + b.ToTable("Person"); }); @@ -283,10 +311,6 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("DisplayName") - .IsRequired() - .HasColumnType("text"); - b.Property("Key") .IsRequired() .HasColumnType("text"); @@ -314,6 +338,25 @@ namespace FictionArchive.Service.NovelService.Migrations b.ToTable("NovelNovelTag"); }); + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationRequest", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", "Engine") + .WithMany() + .HasForeignKey("EngineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "KeyRequestedForTranslation") + .WithMany() + .HasForeignKey("KeyRequestedForTranslationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Engine"); + + b.Navigation("KeyRequestedForTranslation"); + }); + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => { b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", null) @@ -402,6 +445,17 @@ namespace FictionArchive.Service.NovelService.Migrations b.Navigation("Source"); }); + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Person", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Name"); + }); + modelBuilder.Entity("NovelNovelTag", b => { b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) diff --git a/FictionArchive.Service.NovelService/Migrations/20251118045235_Initial.cs b/FictionArchive.Service.NovelService/Migrations/20251120012317_Initial.cs similarity index 76% rename from FictionArchive.Service.NovelService/Migrations/20251118045235_Initial.cs rename to FictionArchive.Service.NovelService/Migrations/20251120012317_Initial.cs index c003e82..0389a85 100644 --- a/FictionArchive.Service.NovelService/Migrations/20251118045235_Initial.cs +++ b/FictionArchive.Service.NovelService/Migrations/20251120012317_Initial.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; using NodaTime; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -13,33 +14,16 @@ namespace FictionArchive.Service.NovelService.Migrations protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "LocalizationKey", + name: "LocalizationKeys", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table.Column(type: "uuid", nullable: false), CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_LocalizationKey", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Person", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false), - ExternalUrl = table.Column(type: "text", nullable: true), - CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), - LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Person", x => x.Id); + table.PrimaryKey("PK_LocalizationKeys", x => x.Id); }); migrationBuilder.CreateTable( @@ -66,7 +50,6 @@ namespace FictionArchive.Service.NovelService.Migrations Id = table.Column(type: "bigint", nullable: false) .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), Key = table.Column(type: "text", nullable: false), - DisplayName = table.Column(type: "text", nullable: false), CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -75,6 +58,112 @@ namespace FictionArchive.Service.NovelService.Migrations table.PrimaryKey("PK_TranslationEngines", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Person", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + NameId = table.Column(type: "uuid", nullable: false), + ExternalUrl = table.Column(type: "text", nullable: true), + CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), + LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Person", x => x.Id); + table.ForeignKey( + name: "FK_Person_LocalizationKeys_NameId", + column: x => x.NameId, + principalTable: "LocalizationKeys", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Key = table.Column(type: "text", nullable: false), + DisplayNameId = table.Column(type: "uuid", nullable: false), + TagType = table.Column(type: "integer", nullable: false), + SourceId = table.Column(type: "bigint", nullable: true), + CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), + LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + table.ForeignKey( + name: "FK_Tags_LocalizationKeys_DisplayNameId", + column: x => x.DisplayNameId, + principalTable: "LocalizationKeys", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Tags_Sources_SourceId", + column: x => x.SourceId, + principalTable: "Sources", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "LocalizationRequests", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + KeyRequestedForTranslationId = table.Column(type: "uuid", nullable: false), + TranslateTo = table.Column(type: "integer", nullable: false), + EngineId = table.Column(type: "bigint", nullable: false), + CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), + LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LocalizationRequests", x => x.Id); + table.ForeignKey( + name: "FK_LocalizationRequests_LocalizationKeys_KeyRequestedForTransl~", + column: x => x.KeyRequestedForTranslationId, + principalTable: "LocalizationKeys", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_LocalizationRequests_TranslationEngines_EngineId", + column: x => x.EngineId, + principalTable: "TranslationEngines", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "LocalizationText", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Language = table.Column(type: "integer", nullable: false), + Text = table.Column(type: "text", nullable: false), + TranslationEngineId = table.Column(type: "bigint", nullable: true), + LocalizationKeyId = table.Column(type: "uuid", nullable: true), + CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), + LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LocalizationText", x => x.Id); + table.ForeignKey( + name: "FK_LocalizationText_LocalizationKeys_LocalizationKeyId", + column: x => x.LocalizationKeyId, + principalTable: "LocalizationKeys", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_LocalizationText_TranslationEngines_TranslationEngineId", + column: x => x.TranslationEngineId, + principalTable: "TranslationEngines", + principalColumn: "Id"); + }); + migrationBuilder.CreateTable( name: "Novels", columns: table => new @@ -88,8 +177,8 @@ namespace FictionArchive.Service.NovelService.Migrations StatusOverride = table.Column(type: "integer", nullable: true), SourceId = table.Column(type: "bigint", nullable: false), ExternalId = table.Column(type: "text", nullable: false), - NameId = table.Column(type: "bigint", nullable: false), - DescriptionId = table.Column(type: "bigint", nullable: false), + NameId = table.Column(type: "uuid", nullable: false), + DescriptionId = table.Column(type: "uuid", nullable: false), CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) }, @@ -97,15 +186,15 @@ namespace FictionArchive.Service.NovelService.Migrations { table.PrimaryKey("PK_Novels", x => x.Id); table.ForeignKey( - name: "FK_Novels_LocalizationKey_DescriptionId", + name: "FK_Novels_LocalizationKeys_DescriptionId", column: x => x.DescriptionId, - principalTable: "LocalizationKey", + principalTable: "LocalizationKeys", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_Novels_LocalizationKey_NameId", + name: "FK_Novels_LocalizationKeys_NameId", column: x => x.NameId, - principalTable: "LocalizationKey", + principalTable: "LocalizationKeys", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( @@ -122,63 +211,6 @@ namespace FictionArchive.Service.NovelService.Migrations onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "Tags", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Key = table.Column(type: "text", nullable: false), - DisplayNameId = table.Column(type: "bigint", nullable: false), - TagType = table.Column(type: "integer", nullable: false), - SourceId = table.Column(type: "bigint", nullable: true), - CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), - LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Tags", x => x.Id); - table.ForeignKey( - name: "FK_Tags_LocalizationKey_DisplayNameId", - column: x => x.DisplayNameId, - principalTable: "LocalizationKey", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Tags_Sources_SourceId", - column: x => x.SourceId, - principalTable: "Sources", - principalColumn: "Id"); - }); - - migrationBuilder.CreateTable( - name: "LocalizationText", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Language = table.Column(type: "integer", nullable: false), - Text = table.Column(type: "text", nullable: false), - TranslationEngineId = table.Column(type: "bigint", nullable: true), - LocalizationKeyId = table.Column(type: "bigint", nullable: true), - CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), - LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_LocalizationText", x => x.Id); - table.ForeignKey( - name: "FK_LocalizationText_LocalizationKey_LocalizationKeyId", - column: x => x.LocalizationKeyId, - principalTable: "LocalizationKey", - principalColumn: "Id"); - table.ForeignKey( - name: "FK_LocalizationText_TranslationEngines_TranslationEngineId", - column: x => x.TranslationEngineId, - principalTable: "TranslationEngines", - principalColumn: "Id"); - }); - migrationBuilder.CreateTable( name: "Chapter", columns: table => new @@ -188,8 +220,8 @@ namespace FictionArchive.Service.NovelService.Migrations Revision = table.Column(type: "bigint", nullable: false), Order = table.Column(type: "bigint", nullable: false), Url = table.Column(type: "text", nullable: true), - NameId = table.Column(type: "bigint", nullable: false), - BodyId = table.Column(type: "bigint", nullable: false), + NameId = table.Column(type: "uuid", nullable: false), + BodyId = table.Column(type: "uuid", nullable: false), NovelId = table.Column(type: "bigint", nullable: true), CreatedTime = table.Column(type: "timestamp with time zone", nullable: false), LastUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false) @@ -198,15 +230,15 @@ namespace FictionArchive.Service.NovelService.Migrations { table.PrimaryKey("PK_Chapter", x => x.Id); table.ForeignKey( - name: "FK_Chapter_LocalizationKey_BodyId", + name: "FK_Chapter_LocalizationKeys_BodyId", column: x => x.BodyId, - principalTable: "LocalizationKey", + principalTable: "LocalizationKeys", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( - name: "FK_Chapter_LocalizationKey_NameId", + name: "FK_Chapter_LocalizationKeys_NameId", column: x => x.NameId, - principalTable: "LocalizationKey", + principalTable: "LocalizationKeys", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( @@ -255,6 +287,16 @@ namespace FictionArchive.Service.NovelService.Migrations table: "Chapter", column: "NovelId"); + migrationBuilder.CreateIndex( + name: "IX_LocalizationRequests_EngineId", + table: "LocalizationRequests", + column: "EngineId"); + + migrationBuilder.CreateIndex( + name: "IX_LocalizationRequests_KeyRequestedForTranslationId", + table: "LocalizationRequests", + column: "KeyRequestedForTranslationId"); + migrationBuilder.CreateIndex( name: "IX_LocalizationText_LocalizationKeyId", table: "LocalizationText", @@ -290,6 +332,11 @@ namespace FictionArchive.Service.NovelService.Migrations table: "Novels", column: "SourceId"); + migrationBuilder.CreateIndex( + name: "IX_Person_NameId", + table: "Person", + column: "NameId"); + migrationBuilder.CreateIndex( name: "IX_Tags_DisplayNameId", table: "Tags", @@ -307,6 +354,9 @@ namespace FictionArchive.Service.NovelService.Migrations migrationBuilder.DropTable( name: "Chapter"); + migrationBuilder.DropTable( + name: "LocalizationRequests"); + migrationBuilder.DropTable( name: "LocalizationText"); @@ -326,10 +376,10 @@ namespace FictionArchive.Service.NovelService.Migrations name: "Person"); migrationBuilder.DropTable( - name: "LocalizationKey"); + name: "Sources"); migrationBuilder.DropTable( - name: "Sources"); + name: "LocalizationKeys"); } } } diff --git a/FictionArchive.Service.NovelService/Migrations/NovelServiceDbContextModelSnapshot.cs b/FictionArchive.Service.NovelService/Migrations/NovelServiceDbContextModelSnapshot.cs index e9fb627..ed31991 100644 --- a/FictionArchive.Service.NovelService/Migrations/NovelServiceDbContextModelSnapshot.cs +++ b/FictionArchive.Service.NovelService/Migrations/NovelServiceDbContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using FictionArchive.Service.NovelService.Services; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -24,11 +25,9 @@ namespace FictionArchive.Service.NovelService.Migrations modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); @@ -38,16 +37,44 @@ namespace FictionArchive.Service.NovelService.Migrations b.HasKey("Id"); - b.ToTable("LocalizationKey"); + b.ToTable("LocalizationKeys"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EngineId") + .HasColumnType("bigint"); + + b.Property("KeyRequestedForTranslationId") + .HasColumnType("uuid"); + + b.Property("LastUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TranslateTo") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EngineId"); + + b.HasIndex("KeyRequestedForTranslationId"); + + b.ToTable("LocalizationRequests"); }); modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + .HasColumnType("uuid"); b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); @@ -58,8 +85,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("LocalizationKeyId") - .HasColumnType("bigint"); + b.Property("LocalizationKeyId") + .HasColumnType("uuid"); b.Property("Text") .IsRequired() @@ -85,8 +112,8 @@ namespace FictionArchive.Service.NovelService.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("BodyId") - .HasColumnType("bigint"); + b.Property("BodyId") + .HasColumnType("uuid"); b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); @@ -94,8 +121,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("NameId") - .HasColumnType("bigint"); + b.Property("NameId") + .HasColumnType("uuid"); b.Property("NovelId") .HasColumnType("bigint"); @@ -134,8 +161,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("DescriptionId") - .HasColumnType("bigint"); + b.Property("DescriptionId") + .HasColumnType("uuid"); b.Property("ExternalId") .IsRequired() @@ -144,8 +171,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("NameId") - .HasColumnType("bigint"); + b.Property("NameId") + .HasColumnType("uuid"); b.Property("RawLanguage") .HasColumnType("integer"); @@ -187,8 +214,8 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("DisplayNameId") - .HasColumnType("bigint"); + b.Property("DisplayNameId") + .HasColumnType("uuid"); b.Property("Key") .IsRequired() @@ -229,12 +256,13 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("LastUpdatedTime") .HasColumnType("timestamp with time zone"); - b.Property("Name") - .IsRequired() - .HasColumnType("text"); + b.Property("NameId") + .HasColumnType("uuid"); b.HasKey("Id"); + b.HasIndex("NameId"); + b.ToTable("Person"); }); @@ -280,10 +308,6 @@ namespace FictionArchive.Service.NovelService.Migrations b.Property("CreatedTime") .HasColumnType("timestamp with time zone"); - b.Property("DisplayName") - .IsRequired() - .HasColumnType("text"); - b.Property("Key") .IsRequired() .HasColumnType("text"); @@ -311,6 +335,25 @@ namespace FictionArchive.Service.NovelService.Migrations b.ToTable("NovelNovelTag"); }); + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationRequest", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", "Engine") + .WithMany() + .HasForeignKey("EngineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "KeyRequestedForTranslation") + .WithMany() + .HasForeignKey("KeyRequestedForTranslationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Engine"); + + b.Navigation("KeyRequestedForTranslation"); + }); + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => { b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", null) @@ -399,6 +442,17 @@ namespace FictionArchive.Service.NovelService.Migrations b.Navigation("Source"); }); + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Person", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Name"); + }); + modelBuilder.Entity("NovelNovelTag", b => { b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterPullRequestedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterPullRequestedEvent.cs new file mode 100644 index 0000000..4c1e9b5 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/IntegrationEvents/ChapterPullRequestedEvent.cs @@ -0,0 +1,9 @@ +using FictionArchive.Service.Shared.Services.EventBus; + +namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; + +public class ChapterPullRequestedEvent : IntegrationEvent +{ + public uint NovelId { get; set; } + public uint ChapterNumber { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelUpdateRequestedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelUpdateRequestedEvent.cs new file mode 100644 index 0000000..46de067 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/IntegrationEvents/NovelUpdateRequestedEvent.cs @@ -0,0 +1,8 @@ +using FictionArchive.Service.Shared.Services.EventBus; + +namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; + +public class NovelUpdateRequestedEvent : IntegrationEvent +{ + public string NovelUrl { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs new file mode 100644 index 0000000..7c441f1 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs @@ -0,0 +1,17 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.Shared.Services.EventBus; + +namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; + +public class TranslationRequestCompletedEvent : IntegrationEvent +{ + /// + /// Maps this event back to a triggering request. + /// + public Guid? TranslationRequestId { get; set; } + + /// + /// The resulting text. + /// + public string? TranslatedText { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs b/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs new file mode 100644 index 0000000..ea33982 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs @@ -0,0 +1,13 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.Shared.Services.EventBus; + +namespace FictionArchive.Service.NovelService.Models.IntegrationEvents; + +public class TranslationRequestCreatedEvent : IntegrationEvent +{ + public Guid TranslationRequestId { get; set; } + public Language From { get; set; } + public Language To { get; set; } + public string Body { get; set; } + public string TranslationEngineKey { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Localization/LocalizationKey.cs b/FictionArchive.Service.NovelService/Models/Localization/LocalizationKey.cs index 7edea60..498e6c4 100644 --- a/FictionArchive.Service.NovelService/Models/Localization/LocalizationKey.cs +++ b/FictionArchive.Service.NovelService/Models/Localization/LocalizationKey.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore; namespace FictionArchive.Service.NovelService.Models.Localization; -public class LocalizationKey : BaseEntity +public class LocalizationKey : BaseEntity { public List Texts { get; set; } diff --git a/FictionArchive.Service.NovelService/Models/Localization/LocalizationRequest.cs b/FictionArchive.Service.NovelService/Models/Localization/LocalizationRequest.cs new file mode 100644 index 0000000..176a52f --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Localization/LocalizationRequest.cs @@ -0,0 +1,12 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.NovelService.Models.Novels; +using FictionArchive.Service.Shared.Models; + +namespace FictionArchive.Service.NovelService.Models.Localization; + +public class LocalizationRequest : BaseEntity +{ + public LocalizationKey KeyRequestedForTranslation { get; set; } + public Language TranslateTo { get; set; } + public TranslationEngine Engine { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Localization/LocalizationText.cs b/FictionArchive.Service.NovelService/Models/Localization/LocalizationText.cs index 431e09b..4eed3d8 100644 --- a/FictionArchive.Service.NovelService/Models/Localization/LocalizationText.cs +++ b/FictionArchive.Service.NovelService/Models/Localization/LocalizationText.cs @@ -4,7 +4,7 @@ using FictionArchive.Service.Shared.Models; namespace FictionArchive.Service.NovelService.Models.Localization; -public class LocalizationText : BaseEntity +public class LocalizationText : BaseEntity { public Language Language { get; set; } public string Text { get; set; } diff --git a/FictionArchive.Service.NovelService/Models/Novels/TranslationEngine.cs b/FictionArchive.Service.NovelService/Models/Novels/TranslationEngine.cs index 755e07d..f2748c5 100644 --- a/FictionArchive.Service.NovelService/Models/Novels/TranslationEngine.cs +++ b/FictionArchive.Service.NovelService/Models/Novels/TranslationEngine.cs @@ -5,5 +5,4 @@ namespace FictionArchive.Service.NovelService.Models.Novels; public class TranslationEngine : BaseEntity { public string Key { get; set; } - public string DisplayName { get; set; } } \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Program.cs b/FictionArchive.Service.NovelService/Program.cs index 290fb1b..79fa235 100644 --- a/FictionArchive.Service.NovelService/Program.cs +++ b/FictionArchive.Service.NovelService/Program.cs @@ -1,8 +1,11 @@ using FictionArchive.Service.NovelService.GraphQL; +using FictionArchive.Service.NovelService.Models.IntegrationEvents; using FictionArchive.Service.NovelService.Services; +using FictionArchive.Service.NovelService.Services.EventHandlers; using FictionArchive.Service.NovelService.Services.SourceAdapters; using FictionArchive.Service.NovelService.Services.SourceAdapters.Novelpia; using FictionArchive.Service.Shared.Extensions; +using FictionArchive.Service.Shared.Services.EventBus.Implementations; using FictionArchive.Service.Shared.Services.GraphQL; using Microsoft.EntityFrameworkCore; @@ -16,6 +19,18 @@ public class Program builder.Services.AddMemoryCache(); + #region Event Bus + + builder.Services.AddRabbitMQ(opt => + { + builder.Configuration.GetSection("RabbitMQ").Bind(opt); + }) + .Subscribe() + .Subscribe() + .Subscribe(); + + #endregion + #region GraphQL builder.Services.AddDefaultGraphQl(); @@ -40,6 +55,8 @@ public class Program client.BaseAddress = new Uri("https://novelpia.com"); }) .AddHttpMessageHandler(); + + builder.Services.AddTransient(); #endregion diff --git a/FictionArchive.Service.NovelService/Services/EventHandlers/ChapterPullRequestedEventHandler.cs b/FictionArchive.Service.NovelService/Services/EventHandlers/ChapterPullRequestedEventHandler.cs new file mode 100644 index 0000000..d05b52e --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/EventHandlers/ChapterPullRequestedEventHandler.cs @@ -0,0 +1,19 @@ +using FictionArchive.Service.NovelService.Models.IntegrationEvents; +using FictionArchive.Service.Shared.Services.EventBus; + +namespace FictionArchive.Service.NovelService.Services.EventHandlers; + +public class ChapterPullRequestedEventHandler : IIntegrationEventHandler +{ + private readonly NovelUpdateService _novelUpdateService; + + public ChapterPullRequestedEventHandler(NovelUpdateService novelUpdateService) + { + _novelUpdateService = novelUpdateService; + } + + public async Task Handle(ChapterPullRequestedEvent @event) + { + await _novelUpdateService.PullChapterContents(@event.NovelId, @event.ChapterNumber); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/EventHandlers/NovelUpdateRequestedEventHandler.cs b/FictionArchive.Service.NovelService/Services/EventHandlers/NovelUpdateRequestedEventHandler.cs new file mode 100644 index 0000000..5fad1e0 --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/EventHandlers/NovelUpdateRequestedEventHandler.cs @@ -0,0 +1,23 @@ +using FictionArchive.Service.NovelService.Models.IntegrationEvents; +using FictionArchive.Service.Shared.Services.EventBus; + +namespace FictionArchive.Service.NovelService.Services.EventHandlers; + +public class NovelUpdateRequestedEventHandler : IIntegrationEventHandler +{ + private readonly ILogger _logger; + private readonly IEventBus _eventBus; + private readonly NovelUpdateService _novelUpdateService; + + public NovelUpdateRequestedEventHandler(ILogger logger, IEventBus eventBus, NovelUpdateService novelUpdateService) + { + _logger = logger; + _eventBus = eventBus; + _novelUpdateService = novelUpdateService; + } + + public async Task Handle(NovelUpdateRequestedEvent @event) + { + await _novelUpdateService.ImportNovel(@event.NovelUrl); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/EventHandlers/TranslationRequestCompletedEventHandler.cs b/FictionArchive.Service.NovelService/Services/EventHandlers/TranslationRequestCompletedEventHandler.cs new file mode 100644 index 0000000..632d26c --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/EventHandlers/TranslationRequestCompletedEventHandler.cs @@ -0,0 +1,39 @@ +using FictionArchive.Service.NovelService.Models.IntegrationEvents; +using FictionArchive.Service.NovelService.Models.Localization; +using FictionArchive.Service.Shared.Services.EventBus; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.NovelService.Services.EventHandlers; + +public class TranslationRequestCompletedEventHandler : IIntegrationEventHandler +{ + private readonly ILogger _logger; + private readonly NovelServiceDbContext _dbContext; + + public TranslationRequestCompletedEventHandler(ILogger logger, NovelServiceDbContext dbContext) + { + _logger = logger; + _dbContext = dbContext; + } + + public async Task Handle(TranslationRequestCompletedEvent @event) + { + var localizationRequest = await _dbContext.LocalizationRequests.Include(r => r.KeyRequestedForTranslation) + .ThenInclude(lk => lk.Texts) + .FirstOrDefaultAsync(lk => lk.Id == @event.TranslationRequestId); + if (localizationRequest == null) + { + // Not one of our requests, discard it + return; + } + + localizationRequest.KeyRequestedForTranslation.Texts.Add(new LocalizationText() + { + Language = localizationRequest.TranslateTo, + Text = @event.TranslatedText, + TranslationEngine = localizationRequest.Engine + }); + _dbContext.LocalizationRequests.Remove(localizationRequest); + await _dbContext.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/NovelServiceDbContext.cs b/FictionArchive.Service.NovelService/Services/NovelServiceDbContext.cs index 6513c92..a6d0204 100644 --- a/FictionArchive.Service.NovelService/Services/NovelServiceDbContext.cs +++ b/FictionArchive.Service.NovelService/Services/NovelServiceDbContext.cs @@ -1,3 +1,4 @@ +using FictionArchive.Service.NovelService.Models.Localization; using FictionArchive.Service.NovelService.Models.Novels; using FictionArchive.Service.Shared.Services.Database; using Microsoft.EntityFrameworkCore; @@ -11,4 +12,6 @@ public class NovelServiceDbContext(DbContextOptions options, ILogger Sources { get; set; } public DbSet TranslationEngines { get; set; } public DbSet Tags { get; set; } + public DbSet LocalizationKeys { get; set; } + public DbSet LocalizationRequests { get; set; } } \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs b/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs new file mode 100644 index 0000000..cf3669e --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/NovelUpdateService.cs @@ -0,0 +1,111 @@ +using FictionArchive.Service.NovelService.Models.Enums; +using FictionArchive.Service.NovelService.Models.Localization; +using FictionArchive.Service.NovelService.Models.Novels; +using FictionArchive.Service.NovelService.Models.SourceAdapters; +using FictionArchive.Service.NovelService.Services.SourceAdapters; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.NovelService.Services; + +public class NovelUpdateService +{ + private readonly NovelServiceDbContext _dbContext; + private readonly ILogger _logger; + private readonly IEnumerable _sourceAdapters; + + public NovelUpdateService(NovelServiceDbContext dbContext, ILogger logger, IEnumerable sourceAdapters) + { + _dbContext = dbContext; + _logger = logger; + _sourceAdapters = sourceAdapters; + } + + public async Task ImportNovel(string novelUrl) + { + NovelMetadata? metadata = null; + foreach (ISourceAdapter sourceAdapter in _sourceAdapters) + { + if (await sourceAdapter.CanProcessNovel(novelUrl)) + { + metadata = await sourceAdapter.GetMetadata(novelUrl); + } + } + + if (metadata == null) + { + throw new NotSupportedException("The provided novel url is currently unsupported."); + } + + var systemTags = metadata.SystemTags.Select(tag => new NovelTag() + { + Key = tag, + DisplayName = LocalizationKey.CreateFromText(tag, metadata.RawLanguage), + TagType = TagType.System + }); + var sourceTags = metadata.SourceTags.Select(tag => new NovelTag() + { + Key = tag, + DisplayName = LocalizationKey.CreateFromText(tag, metadata.RawLanguage), + TagType = TagType.External + }); + + var addedNovel = _dbContext.Novels.Add(new Novel() + { + Author = new Person() + { + Name = LocalizationKey.CreateFromText(metadata.AuthorName, metadata.RawLanguage), + ExternalUrl = metadata.AuthorUrl, + }, + RawLanguage = metadata.RawLanguage, + Url = metadata.Url, + ExternalId = metadata.ExternalId, + Chapters = metadata.Chapters.Select(chapter => + { + return new Chapter() + { + Order = chapter.Order, + Url = chapter.Url, + Revision = chapter.Revision, + Name = LocalizationKey.CreateFromText(chapter.Name, metadata.RawLanguage), + Body = new LocalizationKey() + { + Texts = new List() + } + }; + }).ToList(), + Description = LocalizationKey.CreateFromText(metadata.Description, metadata.RawLanguage), + Name = LocalizationKey.CreateFromText(metadata.Name, metadata.RawLanguage), + RawStatus = metadata.RawStatus, + Tags = sourceTags.Concat(systemTags).ToList(), + Source = new Source() + { + Name = metadata.SourceDescriptor.Name, + Url = metadata.SourceDescriptor.Url, + Key = metadata.SourceDescriptor.Key, + } + }); + await _dbContext.SaveChangesAsync(); + + return addedNovel.Entity; + } + + public async Task PullChapterContents(uint novelId, uint chapterNumber) + { + var novel = await _dbContext.Novels.Where(novel => novel.Id == novelId) + .Include(novel => novel.Chapters) + .ThenInclude(chapter => chapter.Body) + .ThenInclude(body => body.Texts) + .Include(novel => novel.Source) + .FirstOrDefaultAsync(); + var chapter = novel.Chapters.Where(chapter => chapter.Order == chapterNumber).FirstOrDefault(); + var adapter = _sourceAdapters.FirstOrDefault(adapter => adapter.SourceDescriptor.Key == novel.Source.Key); + var rawChapter = await adapter.GetRawChapter(chapter.Url); + chapter.Body.Texts.Add(new LocalizationText() + { + Text = rawChapter, + Language = novel.RawLanguage + }); + await _dbContext.SaveChangesAsync(); + return chapter; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/appsettings.json b/FictionArchive.Service.NovelService/appsettings.json index 985ae9d..bde9f89 100644 --- a/FictionArchive.Service.NovelService/appsettings.json +++ b/FictionArchive.Service.NovelService/appsettings.json @@ -12,5 +12,9 @@ "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=FictionArchive_NovelService;Username=postgres;password=postgres" }, + "RabbitMQ": { + "ConnectionString": "amqp://localhost", + "ClientIdentifier": "NovelService" + }, "AllowedHosts": "*" } diff --git a/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj b/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj index f7cb209..e27a5be 100644 --- a/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj +++ b/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj @@ -22,6 +22,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -30,9 +32,5 @@ - - - - diff --git a/FictionArchive.Service.Shared/Models/BaseEntity.cs b/FictionArchive.Service.Shared/Models/BaseEntity.cs index 3a8f6fc..44c4d9b 100644 --- a/FictionArchive.Service.Shared/Models/BaseEntity.cs +++ b/FictionArchive.Service.Shared/Models/BaseEntity.cs @@ -5,7 +5,7 @@ namespace FictionArchive.Service.Shared.Models; public abstract class BaseEntity : IAuditable { - public uint Id { get; set; } + public TKey Id { get; set; } public Instant CreatedTime { get; set; } public Instant LastUpdatedTime { get; set; } } \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/EventBusBuilder.cs b/FictionArchive.Service.Shared/Services/EventBus/EventBusBuilder.cs new file mode 100644 index 0000000..b09c202 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/EventBusBuilder.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace FictionArchive.Service.Shared.Services.EventBus; + +public class EventBusBuilder where TEventBus : class, IEventBus +{ + private readonly IServiceCollection _services; + private readonly SubscriptionManager _subscriptionManager; + + public EventBusBuilder(IServiceCollection services) + { + _services = services; + _services.AddSingleton(); + + _subscriptionManager = new SubscriptionManager(); + _services.AddSingleton(_subscriptionManager); + } + + public EventBusBuilder Subscribe() where TEvent : IntegrationEvent where TEventHandler : class, IIntegrationEventHandler + { + _services.AddKeyedTransient(typeof(TEvent).Name); + _subscriptionManager.RegisterSubscription(); + return this; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/EventBusExtensions.cs b/FictionArchive.Service.Shared/Services/EventBus/EventBusExtensions.cs new file mode 100644 index 0000000..417bfa8 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/EventBusExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace FictionArchive.Service.Shared.Services.EventBus; + +public static class EventBusExtensions +{ + public static EventBusBuilder AddEventBus(this IServiceCollection services) + where TEventBus : class, IEventBus + { + return new EventBusBuilder(services); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/IEventBus.cs b/FictionArchive.Service.Shared/Services/EventBus/IEventBus.cs new file mode 100644 index 0000000..d3433c1 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/IEventBus.cs @@ -0,0 +1,6 @@ +namespace FictionArchive.Service.Shared.Services.EventBus; + +public interface IEventBus +{ + Task Publish(TEvent integrationEvent) where TEvent : IntegrationEvent; +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEventHandler.cs b/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEventHandler.cs new file mode 100644 index 0000000..7aa3049 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/IIntegrationEventHandler.cs @@ -0,0 +1,12 @@ +namespace FictionArchive.Service.Shared.Services.EventBus; + +public interface IIntegrationEventHandler : IIntegrationEventHandler where TEvent : IntegrationEvent +{ + Task Handle(TEvent @event); + Task IIntegrationEventHandler.Handle(IntegrationEvent @event) => Handle((TEvent)@event); +} + +public interface IIntegrationEventHandler +{ + Task Handle(IntegrationEvent @event); +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQConnectionProvider.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQConnectionProvider.cs new file mode 100644 index 0000000..ad93539 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQConnectionProvider.cs @@ -0,0 +1,35 @@ +using RabbitMQ.Client; + +namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; + +public class RabbitMQConnectionProvider +{ + private readonly IConnectionFactory _connectionFactory; + + private IConnection Connection { get; set; } + private IChannel DefaultChannel { get; set; } + + public RabbitMQConnectionProvider(IConnectionFactory connectionFactory) + { + _connectionFactory = connectionFactory; + } + + public async Task GetConnectionAsync() + { + if (Connection == null) + { + Connection = await _connectionFactory.CreateConnectionAsync(); + } + + return Connection; + } + + public async Task GetDefaultChannelAsync() + { + if (DefaultChannel == null) + { + DefaultChannel = await (await GetConnectionAsync()).CreateChannelAsync(); + } + return DefaultChannel; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQEventBus.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQEventBus.cs new file mode 100644 index 0000000..cdf83d8 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQEventBus.cs @@ -0,0 +1,124 @@ +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using NodaTime; +using NodaTime.Serialization.JsonNet; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; + +public class RabbitMQEventBus : IEventBus, IHostedService +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly RabbitMQConnectionProvider _connectionProvider; + private readonly RabbitMQOptions _options; + private readonly SubscriptionManager _subscriptionManager; + private readonly ILogger _logger; + + private readonly JsonSerializerSettings _jsonSerializerSettings; + + private const string ExchangeName = "fiction-archive-event-bus"; + + public RabbitMQEventBus(IServiceScopeFactory serviceScopeFactory, RabbitMQConnectionProvider connectionProvider, IOptions options, SubscriptionManager subscriptionManager, ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory; + _connectionProvider = connectionProvider; + _subscriptionManager = subscriptionManager; + _logger = logger; + _options = options.Value; + _jsonSerializerSettings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + } + + public async Task Publish(TEvent integrationEvent) where TEvent : IntegrationEvent + { + var routingKey = typeof(TEvent).Name; + var channel = await _connectionProvider.GetDefaultChannelAsync(); + + // Set integration event values + integrationEvent.CreatedAt = Instant.FromDateTimeUtc(DateTime.UtcNow); + integrationEvent.EventId = Guid.NewGuid(); + + var body = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(integrationEvent)); + await channel.BasicPublishAsync(ExchangeName, routingKey, true, body); + _logger.LogInformation("Published event {EventName}", routingKey); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _ = Task.Factory.StartNew(async () => + { + try + { + var channel = await _connectionProvider.GetDefaultChannelAsync(); + await channel.ExchangeDeclareAsync(ExchangeName, ExchangeType.Direct, + cancellationToken: cancellationToken); + + await channel.QueueDeclareAsync(_options.ClientIdentifier, true, false, false, + cancellationToken: cancellationToken); + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.ReceivedAsync += (sender, @event) => + { + return OnReceivedEvent(sender, @event, channel); + }; + + foreach (var subscription in _subscriptionManager.Subscriptions) + { + await channel.QueueBindAsync(_options.ClientIdentifier, ExchangeName, subscription.Key, + cancellationToken: cancellationToken); + _logger.LogInformation("Subscribed to {SubscriptionKey}", subscription.Key); + } + + await channel.BasicConsumeAsync(_options.ClientIdentifier, false, consumer, cancellationToken: cancellationToken); + + _logger.LogInformation("RabbitMQ EventBus started."); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while starting the RabbitMQ EventBus"); + } + }, cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private async Task OnReceivedEvent(object sender, BasicDeliverEventArgs @event, IChannel channel) + { + var eventName = @event.RoutingKey; + _logger.LogInformation("Received event {EventName}", eventName); + try + { + if (!_subscriptionManager.Subscriptions.ContainsKey(eventName)) + { + _logger.LogWarning("Received event without subscription entry."); + return; + } + + var eventBody = Encoding.UTF8.GetString(@event.Body.Span); + var eventObject = JsonConvert.DeserializeObject(eventBody, _subscriptionManager.Subscriptions[eventName], _jsonSerializerSettings) as IntegrationEvent; + + using var scope = _serviceScopeFactory.CreateScope(); + + foreach (var service in scope.ServiceProvider.GetKeyedServices(eventName)) + { + await service.Handle(eventObject); + } + _logger.LogInformation("Finished handling event with name {EventName}", eventName); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while handling an event."); + } + finally + { + await channel.BasicAckAsync(@event.DeliveryTag, false); + } + + } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQExtensions.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQExtensions.cs new file mode 100644 index 0000000..14546d4 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; + +namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; + +public static class RabbitMQExtensions +{ + public static EventBusBuilder AddRabbitMQ(this IServiceCollection services, Action configure) + { + services.Configure(configure); + services.AddSingleton(provider => + { + var options = provider.GetService>(); + ConnectionFactory factory = new ConnectionFactory(); + factory.Uri = new Uri(options.Value.ConnectionString); + return factory; + }); + services.AddSingleton(); + services.AddHostedService(); + return services.AddEventBus(); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQOptions.cs b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQOptions.cs new file mode 100644 index 0000000..f385a35 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/Implementations/RabbitMQOptions.cs @@ -0,0 +1,7 @@ +namespace FictionArchive.Service.Shared.Services.EventBus.Implementations; + +public class RabbitMQOptions +{ + public string ConnectionString { get; set; } + public string ClientIdentifier { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/IntegrationEvent.cs b/FictionArchive.Service.Shared/Services/EventBus/IntegrationEvent.cs new file mode 100644 index 0000000..efa4b16 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/IntegrationEvent.cs @@ -0,0 +1,9 @@ +using NodaTime; + +namespace FictionArchive.Service.Shared.Services.EventBus; + +public abstract class IntegrationEvent +{ + public Guid EventId { get; set; } + public Instant CreatedAt { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/EventBus/SubscriptionManager.cs b/FictionArchive.Service.Shared/Services/EventBus/SubscriptionManager.cs new file mode 100644 index 0000000..73b8c37 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/EventBus/SubscriptionManager.cs @@ -0,0 +1,11 @@ +namespace FictionArchive.Service.Shared.Services.EventBus; + +public class SubscriptionManager +{ + public Dictionary Subscriptions { get; } = new Dictionary(); + + public void RegisterSubscription() + { + Subscriptions.Add(typeof(TEvent).Name, typeof(TEvent)); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/GraphQL/Mutation.cs b/FictionArchive.Service.TranslationService/GraphQL/Mutation.cs index 46a2bd2..d74a337 100644 --- a/FictionArchive.Service.TranslationService/GraphQL/Mutation.cs +++ b/FictionArchive.Service.TranslationService/GraphQL/Mutation.cs @@ -1,6 +1,8 @@ using FictionArchive.Common.Enums; +using FictionArchive.Service.TranslationService.Models; using FictionArchive.Service.TranslationService.Models.Database; using FictionArchive.Service.TranslationService.Models.Enums; +using FictionArchive.Service.TranslationService.Services; using FictionArchive.Service.TranslationService.Services.Database; using FictionArchive.Service.TranslationService.Services.TranslationEngines; @@ -8,23 +10,10 @@ namespace FictionArchive.Service.TranslationService.GraphQL; public class Mutation { - public async Task TranslateText(string text, Language from, Language to, string translationEngineKey, IEnumerable translationEngines, TranslationServiceDbContext dbContext) + public async Task TranslateText(string text, Language from, Language to, string translationEngineKey, TranslationEngineService translationEngineService) { - var engine = translationEngines.FirstOrDefault(engine => engine.Descriptor.Key == translationEngineKey); - var translation = await engine.GetTranslation(text, from, to); - - dbContext.TranslationRequests.Add(new TranslationRequest() - { - OriginalText = text, - BilledCharacterCount = 0, // FILL ME - From = from, - To = to, - Status = translation != null ? TranslationRequestStatus.Success : TranslationRequestStatus.Failed, - TranslatedText = translation, - TranslationEngineKey = translationEngineKey - }); - await dbContext.SaveChangesAsync(); + var result = await translationEngineService.Translate(from, to, text, translationEngineKey); - return translation; + return result; } } \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs b/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs new file mode 100644 index 0000000..6e2c2e2 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCompletedEvent.cs @@ -0,0 +1,18 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.TranslationService.Models.Enums; + +namespace FictionArchive.Service.TranslationService.Models.IntegrationEvents; + +public class TranslationRequestCompletedEvent : IntegrationEvent +{ + /// + /// Maps this event back to a triggering request. + /// + public Guid? TranslationRequestId { get; set; } + + /// + /// The resulting text. + /// + public string? TranslatedText { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs b/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs new file mode 100644 index 0000000..028b283 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Models/IntegrationEvents/TranslationRequestCreatedEvent.cs @@ -0,0 +1,13 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.Shared.Services.EventBus; + +namespace FictionArchive.Service.TranslationService.Models.IntegrationEvents; + +public class TranslationRequestCreatedEvent : IntegrationEvent +{ + public Guid TranslationRequestId { get; set; } + public Language From { get; set; } + public Language To { get; set; } + public string Body { get; set; } + public string TranslationEngineKey { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Models/TranslationResult.cs b/FictionArchive.Service.TranslationService/Models/TranslationResult.cs new file mode 100644 index 0000000..920d4b1 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Models/TranslationResult.cs @@ -0,0 +1,15 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.TranslationService.Models.Enums; + +namespace FictionArchive.Service.TranslationService.Models; + +public class TranslationResult +{ + public required string OriginalText { get; set; } + public string? TranslatedText { get; set; } + public Language From { get; set; } + public Language To { get; set; } + public required string TranslationEngineKey { get; set; } + public TranslationRequestStatus Status { get; set; } + public uint BilledCharacterCount { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Program.cs b/FictionArchive.Service.TranslationService/Program.cs index 12c1e31..4a632bb 100644 --- a/FictionArchive.Service.TranslationService/Program.cs +++ b/FictionArchive.Service.TranslationService/Program.cs @@ -1,11 +1,16 @@ using DeepL; using FictionArchive.Common.Extensions; using FictionArchive.Service.Shared.Extensions; +using FictionArchive.Service.Shared.Services.EventBus.Implementations; using FictionArchive.Service.Shared.Services.GraphQL; using FictionArchive.Service.TranslationService.GraphQL; +using FictionArchive.Service.TranslationService.Models.IntegrationEvents; +using FictionArchive.Service.TranslationService.Services; using FictionArchive.Service.TranslationService.Services.Database; +using FictionArchive.Service.TranslationService.Services.EventHandlers; using FictionArchive.Service.TranslationService.Services.TranslationEngines; using FictionArchive.Service.TranslationService.Services.TranslationEngines.DeepLTranslate; +using RabbitMQ.Client; namespace FictionArchive.Service.TranslationService; @@ -18,6 +23,17 @@ public class Program builder.Services.AddHealthChecks(); + #region Event Bus + + builder.Services.AddRabbitMQ(opt => + { + builder.Configuration.GetSection("RabbitMQ").Bind(opt); + }) + .Subscribe(); + + #endregion + + #region Database builder.Services.RegisterDbContext(builder.Configuration.GetConnectionString("DefaultConnection")); @@ -37,6 +53,8 @@ public class Program return new DeepLClient(builder.Configuration["DeepL:ApiKey"]); }); builder.Services.AddTransient(); + + builder.Services.AddTransient(); #endregion diff --git a/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslationRequestCreatedEventHandler.cs b/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslationRequestCreatedEventHandler.cs new file mode 100644 index 0000000..a824d34 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Services/EventHandlers/TranslationRequestCreatedEventHandler.cs @@ -0,0 +1,31 @@ +using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.TranslationService.Models.Enums; +using FictionArchive.Service.TranslationService.Models.IntegrationEvents; + +namespace FictionArchive.Service.TranslationService.Services.EventHandlers; + +public class TranslationRequestCreatedEventHandler : IIntegrationEventHandler +{ + private readonly ILogger _logger; + private readonly TranslationEngineService _translationEngineService; + private readonly IEventBus _eventBus; + + public TranslationRequestCreatedEventHandler(ILogger logger, TranslationEngineService translationEngineService) + { + _logger = logger; + _translationEngineService = translationEngineService; + } + + public async Task Handle(TranslationRequestCreatedEvent @event) + { + var result = await _translationEngineService.Translate(@event.From, @event.To, @event.Body, @event.TranslationEngineKey); + if (result.Status == TranslationRequestStatus.Success) + { + await _eventBus.Publish(new TranslationRequestCompletedEvent() + { + TranslatedText = result.TranslatedText, + TranslationRequestId = @event.TranslationRequestId, + }); + } + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Services/TranslationEngineService.cs b/FictionArchive.Service.TranslationService/Services/TranslationEngineService.cs new file mode 100644 index 0000000..3768704 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Services/TranslationEngineService.cs @@ -0,0 +1,47 @@ +using System.Text; +using FictionArchive.Common.Enums; +using FictionArchive.Service.Shared.Services.EventBus; +using FictionArchive.Service.Shared.Services.EventBus.Implementations; +using FictionArchive.Service.TranslationService.Models; +using FictionArchive.Service.TranslationService.Models.Database; +using FictionArchive.Service.TranslationService.Models.Enums; +using FictionArchive.Service.TranslationService.Models.IntegrationEvents; +using FictionArchive.Service.TranslationService.Services.Database; +using FictionArchive.Service.TranslationService.Services.TranslationEngines; +using RabbitMQ.Client; + +namespace FictionArchive.Service.TranslationService.Services; + +public class TranslationEngineService +{ + private readonly IEnumerable _translationEngines; + private readonly IEventBus _eventBus; + private readonly TranslationServiceDbContext _dbContext; + + public TranslationEngineService(IEnumerable translationEngines, TranslationServiceDbContext dbContext, IEventBus eventBus) + { + _translationEngines = translationEngines; + _dbContext = dbContext; + _eventBus = eventBus; + } + + public async Task Translate(Language from, Language to, string text, string translationEngineKey) + { + var engine = _translationEngines.FirstOrDefault(engine => engine.Descriptor.Key == translationEngineKey); + var translation = await engine.GetTranslation(text, from, to); + + _dbContext.TranslationRequests.Add(new TranslationRequest() + { + OriginalText = text, + BilledCharacterCount = translation.BilledCharacterCount, // FILL ME + From = from, + To = to, + Status = translation != null ? TranslationRequestStatus.Success : TranslationRequestStatus.Failed, + TranslatedText = translation.TranslatedText, + TranslationEngineKey = translationEngineKey + }); + await _dbContext.SaveChangesAsync(); + + return translation; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Services/TranslationEngines/DeepLTranslate/DeepLTranslationEngine.cs b/FictionArchive.Service.TranslationService/Services/TranslationEngines/DeepLTranslate/DeepLTranslationEngine.cs index 195f817..c119a08 100644 --- a/FictionArchive.Service.TranslationService/Services/TranslationEngines/DeepLTranslate/DeepLTranslationEngine.cs +++ b/FictionArchive.Service.TranslationService/Services/TranslationEngines/DeepLTranslate/DeepLTranslationEngine.cs @@ -1,6 +1,7 @@ using DeepL; using DeepL.Model; using FictionArchive.Service.TranslationService.Models; +using FictionArchive.Service.TranslationService.Models.Enums; using Language = FictionArchive.Common.Enums.Language; namespace FictionArchive.Service.TranslationService.Services.TranslationEngines.DeepLTranslate; @@ -31,11 +32,20 @@ public class DeepLTranslationEngine : ITranslationEngine } } - public async Task GetTranslation(string body, Language from, Language to) + public async Task GetTranslation(string body, Language from, Language to) { TextResult translationResult = await _deepLClient.TranslateTextAsync(body, GetLanguageCode(from), GetLanguageCode(to)); _logger.LogInformation("Translated text. Usage statistics: CHARACTERS BILLED {TranslationResultBilledCharacters}", translationResult.BilledCharacters); - return translationResult.Text; + return new TranslationResult() + { + OriginalText = body, + From = from, + To = to, + TranslationEngineKey = Key, + BilledCharacterCount = (uint)translationResult.BilledCharacters, + Status = TranslationRequestStatus.Success, + TranslatedText = translationResult.Text + }; } private string GetLanguageCode(Language language) diff --git a/FictionArchive.Service.TranslationService/Services/TranslationEngines/ITranslationEngine.cs b/FictionArchive.Service.TranslationService/Services/TranslationEngines/ITranslationEngine.cs index a494fb9..a820da9 100644 --- a/FictionArchive.Service.TranslationService/Services/TranslationEngines/ITranslationEngine.cs +++ b/FictionArchive.Service.TranslationService/Services/TranslationEngines/ITranslationEngine.cs @@ -6,5 +6,5 @@ namespace FictionArchive.Service.TranslationService.Services.TranslationEngines; public interface ITranslationEngine { public TranslationEngineDescriptor Descriptor { get; } - public Task GetTranslation(string body, Language from, Language to); + public Task GetTranslation(string body, Language from, Language to); } \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/appsettings.json b/FictionArchive.Service.TranslationService/appsettings.json index 049c26f..4f0d4e1 100644 --- a/FictionArchive.Service.TranslationService/appsettings.json +++ b/FictionArchive.Service.TranslationService/appsettings.json @@ -11,5 +11,9 @@ "ConnectionStrings": { "DefaultConnection": "Host=localhost;Database=FictionArchive_NovelService;Username=postgres;password=postgres" }, + "RabbitMQ": { + "ConnectionString": "amqp://localhost", + "ClientIdentifier": "TranslationService" + }, "AllowedHosts": "*" }