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": "*"
}