commit 3bb8f7f1588fd0af519f08353efb7a212b64bb52 Author: gamer147 Date: Mon Nov 17 22:58:50 2025 -0500 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c7f0e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.svclog +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml +*.azurePubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +packages/ +## TODO: If the tool you use requires repositories.config, also uncomment the next line +!packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +![Ss]tyle[Cc]op.targets +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml + +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +# ========================= +# Windows detritus +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac desktop service store files +.DS_Store + +_NCrunch* diff --git a/FictionArchive.API/Controllers/TestController.cs b/FictionArchive.API/Controllers/TestController.cs new file mode 100644 index 0000000..c6efa45 --- /dev/null +++ b/FictionArchive.API/Controllers/TestController.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.API.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TestController : ControllerBase + { + /*private readonly FictionArchiveDbContext _dbContext; + private readonly ISourceAdapter _novelpiaAdapter; + private readonly ITranslationEngineAdapter _translationEngine; + + public TestController(ISourceAdapter novelpiaAdapter, FictionArchiveDbContext dbContext, ITranslationEngineAdapter translationEngine) + { + _novelpiaAdapter = novelpiaAdapter; + _dbContext = dbContext; + _translationEngine = translationEngine; + } + + [HttpGet("GetNovel")] + public async Task GetNovel(string novelUrl) + { + var novel = await _novelpiaAdapter.GetMetadata(novelUrl); + novel.Source = new Source() + { + Name = "Novelpia", + Id = 1, + Url = "https://novelpia.com" + }; + _dbContext.Novels.Add(novel); + await _dbContext.SaveChangesAsync(); + return novel; + } + + [HttpGet("GetChapter")] + public async Task GetChapter(uint novelId, uint chapterNumber) + { + var novel = await _dbContext.Novels.Include(n => n.Chapters).ThenInclude(c => c.Translations).FirstOrDefaultAsync(n => n.Id == novelId); + var chapter = novel.Chapters.FirstOrDefault(c => c.Order == chapterNumber); + var rawChapter = await _novelpiaAdapter.GetRawChapter(chapter.Url); + chapter.Translations.Add(new ChapterTranslation() + { + Language = novel.RawLanguage, + Body = rawChapter + }); + await _dbContext.SaveChangesAsync(); + return rawChapter; + } + + [HttpPost("TranslateChapter")] + public async Task TranslateChapter(uint novelId, uint chapterNumber, Language to) + { + var novel = await _dbContext.Novels.Include(n => n.Chapters) + .ThenInclude(c => c.Translations).FirstOrDefaultAsync(novel => novel.Id == novelId); + var chapter = novel.Chapters.FirstOrDefault(c => c.Order == chapterNumber); + var chapterRaw = chapter.Translations.FirstOrDefault(ct => ct.Language == novel.RawLanguage); + var newTranslation = new ChapterTranslation() + { + Language = to, + TranslationEngine = new TranslationEngine() + { + Name = "DeepL" + } + }; + var translation = await _translationEngine.GetTranslation(chapterRaw.Body, novel.RawLanguage, to); + newTranslation.Body = translation; + chapter.Translations.Add(newTranslation); + await _dbContext.SaveChangesAsync(); + + return newTranslation; + }*/ + } +} diff --git a/FictionArchive.API/Dockerfile b/FictionArchive.API/Dockerfile new file mode 100644 index 0000000..18cacd4 --- /dev/null +++ b/FictionArchive.API/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["FictionArchive.API/FictionArchive.API.csproj", "FictionArchive.API/"] +RUN dotnet restore "FictionArchive.API/FictionArchive.API.csproj" +COPY . . +WORKDIR "/src/FictionArchive.API" +RUN dotnet build "./FictionArchive.API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./FictionArchive.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "FictionArchive.API.dll"] diff --git a/FictionArchive.API/FictionArchive.API.csproj b/FictionArchive.API/FictionArchive.API.csproj new file mode 100644 index 0000000..aafc33d --- /dev/null +++ b/FictionArchive.API/FictionArchive.API.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + .dockerignore + + + + + + + + + diff --git a/FictionArchive.API/FictionArchive.API.http b/FictionArchive.API/FictionArchive.API.http new file mode 100644 index 0000000..5336b85 --- /dev/null +++ b/FictionArchive.API/FictionArchive.API.http @@ -0,0 +1,6 @@ +@FictionArchive.API_HostAddress = http://localhost:5234 + +GET {{FictionArchive.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/FictionArchive.API/Program.cs b/FictionArchive.API/Program.cs new file mode 100644 index 0000000..5426d26 --- /dev/null +++ b/FictionArchive.API/Program.cs @@ -0,0 +1,48 @@ +namespace FictionArchive.API; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + // OpenAPI & REST + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + builder.Services.AddMemoryCache(); + + + + builder.Services.AddHealthChecks(); + + var app = builder.Build(); + + + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + app.UseAuthentication(); + + app.UseAuthorization(); + + app.MapGraphQL(); + + app.MapHealthChecks("/healthz"); + + app.MapControllers(); + + app.Run(); + } +} \ No newline at end of file diff --git a/FictionArchive.API/Properties/launchSettings.json b/FictionArchive.API/Properties/launchSettings.json new file mode 100644 index 0000000..dddb254 --- /dev/null +++ b/FictionArchive.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50224", + "sslPort": 44385 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5234", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7063;http://localhost:5234", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/FictionArchive.API/appsettings.Development.json b/FictionArchive.API/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/FictionArchive.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/FictionArchive.API/appsettings.json b/FictionArchive.API/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/FictionArchive.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/FictionArchive.Common/Enums/Language.cs b/FictionArchive.Common/Enums/Language.cs new file mode 100644 index 0000000..8eccacf --- /dev/null +++ b/FictionArchive.Common/Enums/Language.cs @@ -0,0 +1,9 @@ +namespace FictionArchive.Common.Enums; + +public enum Language +{ + En, + Kr, + Ch, + Ja +} \ No newline at end of file diff --git a/FictionArchive.Common/FictionArchive.Common.csproj b/FictionArchive.Common/FictionArchive.Common.csproj new file mode 100644 index 0000000..1467768 --- /dev/null +++ b/FictionArchive.Common/FictionArchive.Common.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/FictionArchive.Service.NovelService/Constants/SystemTags.cs b/FictionArchive.Service.NovelService/Constants/SystemTags.cs new file mode 100644 index 0000000..5ea0ed1 --- /dev/null +++ b/FictionArchive.Service.NovelService/Constants/SystemTags.cs @@ -0,0 +1,6 @@ +namespace FictionArchive.Service.NovelService.Constants; + +public static class SystemTags +{ + public const string Nsfw = "Nsfw"; +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Dockerfile b/FictionArchive.Service.NovelService/Dockerfile new file mode 100644 index 0000000..6390c36 --- /dev/null +++ b/FictionArchive.Service.NovelService/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj", "FictionArchive.Service.NovelService/"] +RUN dotnet restore "FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj" +COPY . . +WORKDIR "/src/FictionArchive.Service.NovelService" +RUN dotnet build "./FictionArchive.Service.NovelService.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./FictionArchive.Service.NovelService.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "FictionArchive.Service.NovelService.dll"] diff --git a/FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj b/FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj new file mode 100644 index 0000000..5ac6730 --- /dev/null +++ b/FictionArchive.Service.NovelService/FictionArchive.Service.NovelService.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + .dockerignore + + + + + + + + + + + + + + diff --git a/FictionArchive.Service.NovelService/GraphQL/Mutation.cs b/FictionArchive.Service.NovelService/GraphQL/Mutation.cs new file mode 100644 index 0000000..ae2b0d0 --- /dev/null +++ b/FictionArchive.Service.NovelService/GraphQL/Mutation.cs @@ -0,0 +1,107 @@ +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; +using FictionArchive.Service.NovelService.Services.SourceAdapters; +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) + { + NovelMetadata? metadata = null; + foreach (ISourceAdapter sourceAdapter in adapters) + { + 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 = metadata.AuthorName, + 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 FetchChapterContents(uint novelId, + uint chapterNumber, + NovelServiceDbContext dbContext, + IEnumerable sourceAdapters) + { + 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/GraphQL/Query.cs b/FictionArchive.Service.NovelService/GraphQL/Query.cs new file mode 100644 index 0000000..27e3860 --- /dev/null +++ b/FictionArchive.Service.NovelService/GraphQL/Query.cs @@ -0,0 +1,16 @@ +using FictionArchive.Service.NovelService.Models.Novels; +using FictionArchive.Service.NovelService.Services; + +namespace FictionArchive.Service.NovelService.GraphQL; + +public class Query +{ + [UsePaging] + [UseProjection] + [UseFiltering] + [UseSorting] + public IQueryable GetNovels(NovelServiceDbContext dbContext) + { + return dbContext.Novels.AsQueryable(); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Migrations/20251118021857_Initial.Designer.cs b/FictionArchive.Service.NovelService/Migrations/20251118021857_Initial.Designer.cs new file mode 100644 index 0000000..3893ff6 --- /dev/null +++ b/FictionArchive.Service.NovelService/Migrations/20251118021857_Initial.Designer.cs @@ -0,0 +1,409 @@ +// +using System; +using FictionArchive.Service.NovelService.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FictionArchive.Service.NovelService.Migrations +{ + [DbContext(typeof(NovelServiceDbContext))] + [Migration("20251118021857_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("LocalizationKey"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("LocalizationKeyId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("TranslationEngineId") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocalizationKeyId"); + + b.HasIndex("TranslationEngineId"); + + b.ToTable("LocalizationText"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BodyId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("NovelId") + .HasColumnType("bigint"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Revision") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BodyId"); + + b.HasIndex("NameId"); + + b.HasIndex("NovelId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionId") + .HasColumnType("bigint"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("RawLanguage") + .HasColumnType("integer"); + + b.Property("RawStatus") + .HasColumnType("integer"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("StatusOverride") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("DescriptionId"); + + b.HasIndex("NameId"); + + b.HasIndex("SourceId"); + + b.ToTable("Novels"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameId") + .HasColumnType("bigint"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("NovelId") + .HasColumnType("bigint"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("TagType") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DisplayNameId"); + + b.HasIndex("NovelId"); + + b.HasIndex("SourceId"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("TranslationEngines"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", null) + .WithMany("Texts") + .HasForeignKey("LocalizationKeyId"); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", "TranslationEngine") + .WithMany() + .HasForeignKey("TranslationEngineId"); + + b.Navigation("TranslationEngine"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Body") + .WithMany() + .HasForeignKey("BodyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany("Chapters") + .HasForeignKey("NovelId"); + + b.Navigation("Body"); + + b.Navigation("Name"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Person", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Description") + .WithMany() + .HasForeignKey("DescriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Description"); + + b.Navigation("Name"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "DisplayName") + .WithMany() + .HasForeignKey("DisplayNameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany("Tags") + .HasForeignKey("NovelId"); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId"); + + b.Navigation("DisplayName"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Navigation("Texts"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Navigation("Chapters"); + + b.Navigation("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FictionArchive.Service.NovelService/Migrations/20251118021857_Initial.cs b/FictionArchive.Service.NovelService/Migrations/20251118021857_Initial.cs new file mode 100644 index 0000000..d99ce90 --- /dev/null +++ b/FictionArchive.Service.NovelService/Migrations/20251118021857_Initial.cs @@ -0,0 +1,313 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FictionArchive.Service.NovelService.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LocalizationKey", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = 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), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Person", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Sources", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Url = table.Column(type: "text", nullable: false), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Sources", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TranslationEngines", + columns: table => new + { + 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), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TranslationEngines", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Novels", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + AuthorId = table.Column(type: "bigint", nullable: false), + Url = table.Column(type: "text", nullable: false), + RawLanguage = table.Column(type: "integer", nullable: false), + RawStatus = table.Column(type: "integer", nullable: false), + 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), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Novels", x => x.Id); + table.ForeignKey( + name: "FK_Novels_LocalizationKey_DescriptionId", + column: x => x.DescriptionId, + principalTable: "LocalizationKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Novels_LocalizationKey_NameId", + column: x => x.NameId, + principalTable: "LocalizationKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Novels_Person_AuthorId", + column: x => x.AuthorId, + principalTable: "Person", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Novels_Sources_SourceId", + column: x => x.SourceId, + principalTable: "Sources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + 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), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = 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 + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + 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), + NovelId = table.Column(type: "bigint", nullable: true), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Chapter", x => x.Id); + table.ForeignKey( + name: "FK_Chapter_LocalizationKey_BodyId", + column: x => x.BodyId, + principalTable: "LocalizationKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Chapter_LocalizationKey_NameId", + column: x => x.NameId, + principalTable: "LocalizationKey", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Chapter_Novels_NovelId", + column: x => x.NovelId, + principalTable: "Novels", + principalColumn: "Id"); + }); + + 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), + NovelId = table.Column(type: "bigint", nullable: true), + CreatedUtc = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedUtc = 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_Novels_NovelId", + column: x => x.NovelId, + principalTable: "Novels", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Tags_Sources_SourceId", + column: x => x.SourceId, + principalTable: "Sources", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Chapter_BodyId", + table: "Chapter", + column: "BodyId"); + + migrationBuilder.CreateIndex( + name: "IX_Chapter_NameId", + table: "Chapter", + column: "NameId"); + + migrationBuilder.CreateIndex( + name: "IX_Chapter_NovelId", + table: "Chapter", + column: "NovelId"); + + migrationBuilder.CreateIndex( + name: "IX_LocalizationText_LocalizationKeyId", + table: "LocalizationText", + column: "LocalizationKeyId"); + + migrationBuilder.CreateIndex( + name: "IX_LocalizationText_TranslationEngineId", + table: "LocalizationText", + column: "TranslationEngineId"); + + migrationBuilder.CreateIndex( + name: "IX_Novels_AuthorId", + table: "Novels", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Novels_DescriptionId", + table: "Novels", + column: "DescriptionId"); + + migrationBuilder.CreateIndex( + name: "IX_Novels_NameId", + table: "Novels", + column: "NameId"); + + migrationBuilder.CreateIndex( + name: "IX_Novels_SourceId", + table: "Novels", + column: "SourceId"); + + migrationBuilder.CreateIndex( + name: "IX_Tags_DisplayNameId", + table: "Tags", + column: "DisplayNameId"); + + migrationBuilder.CreateIndex( + name: "IX_Tags_NovelId", + table: "Tags", + column: "NovelId"); + + migrationBuilder.CreateIndex( + name: "IX_Tags_SourceId", + table: "Tags", + column: "SourceId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Chapter"); + + migrationBuilder.DropTable( + name: "LocalizationText"); + + migrationBuilder.DropTable( + name: "Tags"); + + migrationBuilder.DropTable( + name: "TranslationEngines"); + + migrationBuilder.DropTable( + name: "Novels"); + + migrationBuilder.DropTable( + name: "LocalizationKey"); + + migrationBuilder.DropTable( + name: "Person"); + + migrationBuilder.DropTable( + name: "Sources"); + } + } +} diff --git a/FictionArchive.Service.NovelService/Migrations/20251118023157_AddSourceKey.Designer.cs b/FictionArchive.Service.NovelService/Migrations/20251118023157_AddSourceKey.Designer.cs new file mode 100644 index 0000000..5a9570b --- /dev/null +++ b/FictionArchive.Service.NovelService/Migrations/20251118023157_AddSourceKey.Designer.cs @@ -0,0 +1,413 @@ +// +using System; +using FictionArchive.Service.NovelService.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FictionArchive.Service.NovelService.Migrations +{ + [DbContext(typeof(NovelServiceDbContext))] + [Migration("20251118023157_AddSourceKey")] + partial class AddSourceKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("LocalizationKey"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("LocalizationKeyId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("TranslationEngineId") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocalizationKeyId"); + + b.HasIndex("TranslationEngineId"); + + b.ToTable("LocalizationText"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BodyId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("NovelId") + .HasColumnType("bigint"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Revision") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BodyId"); + + b.HasIndex("NameId"); + + b.HasIndex("NovelId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionId") + .HasColumnType("bigint"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("RawLanguage") + .HasColumnType("integer"); + + b.Property("RawStatus") + .HasColumnType("integer"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("StatusOverride") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("DescriptionId"); + + b.HasIndex("NameId"); + + b.HasIndex("SourceId"); + + b.ToTable("Novels"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameId") + .HasColumnType("bigint"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("NovelId") + .HasColumnType("bigint"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("TagType") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DisplayNameId"); + + b.HasIndex("NovelId"); + + b.HasIndex("SourceId"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("TranslationEngines"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", null) + .WithMany("Texts") + .HasForeignKey("LocalizationKeyId"); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", "TranslationEngine") + .WithMany() + .HasForeignKey("TranslationEngineId"); + + b.Navigation("TranslationEngine"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Body") + .WithMany() + .HasForeignKey("BodyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany("Chapters") + .HasForeignKey("NovelId"); + + b.Navigation("Body"); + + b.Navigation("Name"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Person", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Description") + .WithMany() + .HasForeignKey("DescriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Description"); + + b.Navigation("Name"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "DisplayName") + .WithMany() + .HasForeignKey("DisplayNameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany("Tags") + .HasForeignKey("NovelId"); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId"); + + b.Navigation("DisplayName"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Navigation("Texts"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Navigation("Chapters"); + + b.Navigation("Tags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FictionArchive.Service.NovelService/Migrations/20251118023157_AddSourceKey.cs b/FictionArchive.Service.NovelService/Migrations/20251118023157_AddSourceKey.cs new file mode 100644 index 0000000..6f93508 --- /dev/null +++ b/FictionArchive.Service.NovelService/Migrations/20251118023157_AddSourceKey.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FictionArchive.Service.NovelService.Migrations +{ + /// + public partial class AddSourceKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Key", + table: "Sources", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Key", + table: "Sources"); + } + } +} diff --git a/FictionArchive.Service.NovelService/Migrations/20251118030953_FixTagAssociation.Designer.cs b/FictionArchive.Service.NovelService/Migrations/20251118030953_FixTagAssociation.Designer.cs new file mode 100644 index 0000000..bb3e3e3 --- /dev/null +++ b/FictionArchive.Service.NovelService/Migrations/20251118030953_FixTagAssociation.Designer.cs @@ -0,0 +1,432 @@ +// +using System; +using FictionArchive.Service.NovelService.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FictionArchive.Service.NovelService.Migrations +{ + [DbContext(typeof(NovelServiceDbContext))] + [Migration("20251118030953_FixTagAssociation")] + partial class FixTagAssociation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("LocalizationKey"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("LocalizationKeyId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("TranslationEngineId") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocalizationKeyId"); + + b.HasIndex("TranslationEngineId"); + + b.ToTable("LocalizationText"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BodyId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("NovelId") + .HasColumnType("bigint"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Revision") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BodyId"); + + b.HasIndex("NameId"); + + b.HasIndex("NovelId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionId") + .HasColumnType("bigint"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("RawLanguage") + .HasColumnType("integer"); + + b.Property("RawStatus") + .HasColumnType("integer"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("StatusOverride") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("DescriptionId"); + + b.HasIndex("NameId"); + + b.HasIndex("SourceId"); + + b.ToTable("Novels"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameId") + .HasColumnType("bigint"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("TagType") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DisplayNameId"); + + b.HasIndex("SourceId"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("TranslationEngines"); + }); + + modelBuilder.Entity("NovelNovelTag", b => + { + b.Property("NovelsId") + .HasColumnType("bigint"); + + b.Property("TagsId") + .HasColumnType("bigint"); + + b.HasKey("NovelsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("NovelNovelTag"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", null) + .WithMany("Texts") + .HasForeignKey("LocalizationKeyId"); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", "TranslationEngine") + .WithMany() + .HasForeignKey("TranslationEngineId"); + + b.Navigation("TranslationEngine"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Body") + .WithMany() + .HasForeignKey("BodyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany("Chapters") + .HasForeignKey("NovelId"); + + b.Navigation("Body"); + + b.Navigation("Name"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Person", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Description") + .WithMany() + .HasForeignKey("DescriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Description"); + + b.Navigation("Name"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "DisplayName") + .WithMany() + .HasForeignKey("DisplayNameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId"); + + b.Navigation("DisplayName"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("NovelNovelTag", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany() + .HasForeignKey("NovelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.NovelTag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Navigation("Texts"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FictionArchive.Service.NovelService/Migrations/20251118030953_FixTagAssociation.cs b/FictionArchive.Service.NovelService/Migrations/20251118030953_FixTagAssociation.cs new file mode 100644 index 0000000..d441835 --- /dev/null +++ b/FictionArchive.Service.NovelService/Migrations/20251118030953_FixTagAssociation.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FictionArchive.Service.NovelService.Migrations +{ + /// + public partial class FixTagAssociation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Tags_Novels_NovelId", + table: "Tags"); + + migrationBuilder.DropIndex( + name: "IX_Tags_NovelId", + table: "Tags"); + + migrationBuilder.DropColumn( + name: "NovelId", + table: "Tags"); + + migrationBuilder.CreateTable( + name: "NovelNovelTag", + columns: table => new + { + NovelsId = table.Column(type: "bigint", nullable: false), + TagsId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_NovelNovelTag", x => new { x.NovelsId, x.TagsId }); + table.ForeignKey( + name: "FK_NovelNovelTag_Novels_NovelsId", + column: x => x.NovelsId, + principalTable: "Novels", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_NovelNovelTag_Tags_TagsId", + column: x => x.TagsId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_NovelNovelTag_TagsId", + table: "NovelNovelTag", + column: "TagsId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "NovelNovelTag"); + + migrationBuilder.AddColumn( + name: "NovelId", + table: "Tags", + type: "bigint", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Tags_NovelId", + table: "Tags", + column: "NovelId"); + + migrationBuilder.AddForeignKey( + name: "FK_Tags_Novels_NovelId", + table: "Tags", + column: "NovelId", + principalTable: "Novels", + principalColumn: "Id"); + } + } +} diff --git a/FictionArchive.Service.NovelService/Migrations/NovelServiceDbContextModelSnapshot.cs b/FictionArchive.Service.NovelService/Migrations/NovelServiceDbContextModelSnapshot.cs new file mode 100644 index 0000000..745f440 --- /dev/null +++ b/FictionArchive.Service.NovelService/Migrations/NovelServiceDbContextModelSnapshot.cs @@ -0,0 +1,429 @@ +// +using System; +using FictionArchive.Service.NovelService.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FictionArchive.Service.NovelService.Migrations +{ + [DbContext(typeof(NovelServiceDbContext))] + partial class NovelServiceDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("LocalizationKey"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Language") + .HasColumnType("integer"); + + b.Property("LocalizationKeyId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("TranslationEngineId") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LocalizationKeyId"); + + b.HasIndex("TranslationEngineId"); + + b.ToTable("LocalizationText"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BodyId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("NovelId") + .HasColumnType("bigint"); + + b.Property("Order") + .HasColumnType("bigint"); + + b.Property("Revision") + .HasColumnType("bigint"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("BodyId"); + + b.HasIndex("NameId"); + + b.HasIndex("NovelId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("bigint"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DescriptionId") + .HasColumnType("bigint"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameId") + .HasColumnType("bigint"); + + b.Property("RawLanguage") + .HasColumnType("integer"); + + b.Property("RawStatus") + .HasColumnType("integer"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("StatusOverride") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("DescriptionId"); + + b.HasIndex("NameId"); + + b.HasIndex("SourceId"); + + b.ToTable("Novels"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayNameId") + .HasColumnType("bigint"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceId") + .HasColumnType("bigint"); + + b.Property("TagType") + .HasColumnType("integer"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DisplayNameId"); + + b.HasIndex("SourceId"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalUrl") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("TranslationEngines"); + }); + + modelBuilder.Entity("NovelNovelTag", b => + { + b.Property("NovelsId") + .HasColumnType("bigint"); + + b.Property("TagsId") + .HasColumnType("bigint"); + + b.HasKey("NovelsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("NovelNovelTag"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationText", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", null) + .WithMany("Texts") + .HasForeignKey("LocalizationKeyId"); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.TranslationEngine", "TranslationEngine") + .WithMany() + .HasForeignKey("TranslationEngineId"); + + b.Navigation("TranslationEngine"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Chapter", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Body") + .WithMany() + .HasForeignKey("BodyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany("Chapters") + .HasForeignKey("NovelId"); + + b.Navigation("Body"); + + b.Navigation("Name"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Person", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Description") + .WithMany() + .HasForeignKey("DescriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "Name") + .WithMany() + .HasForeignKey("NameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Description"); + + b.Navigation("Name"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.NovelTag", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", "DisplayName") + .WithMany() + .HasForeignKey("DisplayNameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Source", "Source") + .WithMany() + .HasForeignKey("SourceId"); + + b.Navigation("DisplayName"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("NovelNovelTag", b => + { + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.Novel", null) + .WithMany() + .HasForeignKey("NovelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FictionArchive.Service.NovelService.Models.Novels.NovelTag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Localization.LocalizationKey", b => + { + b.Navigation("Texts"); + }); + + modelBuilder.Entity("FictionArchive.Service.NovelService.Models.Novels.Novel", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FictionArchive.Service.NovelService/Models/Enums/NovelStatus.cs b/FictionArchive.Service.NovelService/Models/Enums/NovelStatus.cs new file mode 100644 index 0000000..85ce6a3 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Enums/NovelStatus.cs @@ -0,0 +1,10 @@ +namespace FictionArchive.Service.NovelService.Models.Enums; + +public enum NovelStatus +{ + Unknown, + InProgress, + Completed, + Hiatus, + Abandoned +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Enums/TagType.cs b/FictionArchive.Service.NovelService/Models/Enums/TagType.cs new file mode 100644 index 0000000..a7e019a --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Enums/TagType.cs @@ -0,0 +1,9 @@ +namespace FictionArchive.Service.NovelService.Models.Enums; + +public enum TagType +{ + System, + External, + UserDefined, + Genre, +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Localization/LocalizationKey.cs b/FictionArchive.Service.NovelService/Models/Localization/LocalizationKey.cs new file mode 100644 index 0000000..fcb8b46 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Localization/LocalizationKey.cs @@ -0,0 +1,25 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.NovelService.Models.Novels; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.NovelService.Models.Localization; + +public class LocalizationKey : BaseEntity +{ + public List Texts { get; set; } + + public static LocalizationKey CreateFromText(string text, Language language) + { + return new LocalizationKey() + { + Texts = new List() + { + new LocalizationText() + { + Language = language, + Text = text + } + } + }; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Localization/LocalizationText.cs b/FictionArchive.Service.NovelService/Models/Localization/LocalizationText.cs new file mode 100644 index 0000000..216e20b --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Localization/LocalizationText.cs @@ -0,0 +1,11 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.NovelService.Models.Novels; + +namespace FictionArchive.Service.NovelService.Models.Localization; + +public class LocalizationText : BaseEntity +{ + public Language Language { get; set; } + public string Text { get; set; } + public TranslationEngine? TranslationEngine { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/BaseEntity.cs b/FictionArchive.Service.NovelService/Models/Novels/BaseEntity.cs new file mode 100644 index 0000000..626077e --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/BaseEntity.cs @@ -0,0 +1,8 @@ +namespace FictionArchive.Service.NovelService.Models.Novels; + +public abstract class BaseEntity +{ + public uint Id { get; set; } + public DateTime CreatedUtc { get; set; } + public DateTime UpdatedUtc { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/Chapter.cs b/FictionArchive.Service.NovelService/Models/Novels/Chapter.cs new file mode 100644 index 0000000..2dde365 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/Chapter.cs @@ -0,0 +1,13 @@ +using FictionArchive.Service.NovelService.Models.Localization; + +namespace FictionArchive.Service.NovelService.Models.Novels; + +public class Chapter : BaseEntity +{ + public uint Revision { get; set; } + public uint Order { get; set; } + public string? Url { get; set; } + + public LocalizationKey Name { get; set; } + public LocalizationKey Body { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/Novel.cs b/FictionArchive.Service.NovelService/Models/Novels/Novel.cs new file mode 100644 index 0000000..17aed48 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/Novel.cs @@ -0,0 +1,24 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.NovelService.Models.Localization; +using NovelStatus = FictionArchive.Service.NovelService.Models.Enums.NovelStatus; + +namespace FictionArchive.Service.NovelService.Models.Novels; + +public class Novel : BaseEntity +{ + public Person Author { get; set; } + public string Url { get; set; } + public Language RawLanguage { get; set; } + + public NovelStatus RawStatus { get; set; } + public NovelStatus? StatusOverride { get; set; } + + public Source Source { get; set; } + public string ExternalId { get; set; } + + public LocalizationKey Name { get; set; } + public LocalizationKey Description { get; set; } + + public List Chapters { get; set; } + public List Tags { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/NovelTag.cs b/FictionArchive.Service.NovelService/Models/Novels/NovelTag.cs new file mode 100644 index 0000000..48625fb --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/NovelTag.cs @@ -0,0 +1,14 @@ +using FictionArchive.Service.NovelService.Models.Enums; +using FictionArchive.Service.NovelService.Models.Localization; + +namespace FictionArchive.Service.NovelService.Models.Novels; + +public class NovelTag : BaseEntity +{ + public string Key { get; set; } + public LocalizationKey DisplayName { get; set; } + public TagType TagType { get; set; } + + public Source? Source { get; set; } + public List Novels { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/Person.cs b/FictionArchive.Service.NovelService/Models/Novels/Person.cs new file mode 100644 index 0000000..8e7d734 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/Person.cs @@ -0,0 +1,7 @@ +namespace FictionArchive.Service.NovelService.Models.Novels; + +public class Person : BaseEntity +{ + public string Name { get; set; } + public string? ExternalUrl { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/Source.cs b/FictionArchive.Service.NovelService/Models/Novels/Source.cs new file mode 100644 index 0000000..2dd82c6 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/Source.cs @@ -0,0 +1,8 @@ +namespace FictionArchive.Service.NovelService.Models.Novels; + +public class Source : BaseEntity +{ + public string Name { get; set; } + public string Key { get; set; } + public string Url { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/SourceConfiguration.cs b/FictionArchive.Service.NovelService/Models/Novels/SourceConfiguration.cs new file mode 100644 index 0000000..7bd1da7 --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/SourceConfiguration.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; +using FictionArchive.Service.NovelService.Models.Novels; + +namespace FictionArchive.Service.NovelService.Models; + +public class SourceConfiguration : BaseEntity +{ + public string Key { get; set; } + + [Column(TypeName = "jsonb")] + public string Configuration { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/Novels/TranslationEngine.cs b/FictionArchive.Service.NovelService/Models/Novels/TranslationEngine.cs new file mode 100644 index 0000000..8b450ab --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/Novels/TranslationEngine.cs @@ -0,0 +1,7 @@ +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/Models/SourceAdapters/ChapterMetadata.cs b/FictionArchive.Service.NovelService/Models/SourceAdapters/ChapterMetadata.cs new file mode 100644 index 0000000..353c9bd --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/SourceAdapters/ChapterMetadata.cs @@ -0,0 +1,10 @@ +namespace FictionArchive.Service.NovelService.Models.SourceAdapters; + +public class ChapterMetadata +{ + public uint Revision { get; set; } + public uint Order { get; set; } + public string? Url { get; set; } + public string Name { get; set; } + +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/SourceAdapters/NovelMetadata.cs b/FictionArchive.Service.NovelService/Models/SourceAdapters/NovelMetadata.cs new file mode 100644 index 0000000..af1467a --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/SourceAdapters/NovelMetadata.cs @@ -0,0 +1,22 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.NovelService.Models.Enums; + +namespace FictionArchive.Service.NovelService.Models.SourceAdapters; + +public class NovelMetadata +{ + public string Name { get; set; } + public string Description { get; set; } + public string AuthorName { get; set; } + public string AuthorUrl { get; set; } + public string Url { get; set; } + public string ExternalId { get; set; } + + public Language RawLanguage { get; set; } + public NovelStatus RawStatus { get; set; } + + public List Chapters { get; set; } + public List SourceTags { get; set; } + public List SystemTags { get; set; } + public SourceDescriptor SourceDescriptor { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Models/SourceAdapters/SourceDescriptor.cs b/FictionArchive.Service.NovelService/Models/SourceAdapters/SourceDescriptor.cs new file mode 100644 index 0000000..c84fe6a --- /dev/null +++ b/FictionArchive.Service.NovelService/Models/SourceAdapters/SourceDescriptor.cs @@ -0,0 +1,8 @@ +namespace FictionArchive.Service.NovelService.Models.SourceAdapters; + +public class SourceDescriptor +{ + public string Name { get; set; } + public string Key { get; set; } + public string Url { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Program.cs b/FictionArchive.Service.NovelService/Program.cs new file mode 100644 index 0000000..cd2a55c --- /dev/null +++ b/FictionArchive.Service.NovelService/Program.cs @@ -0,0 +1,74 @@ +using FictionArchive.Service.NovelService.GraphQL; +using FictionArchive.Service.NovelService.Services; +using FictionArchive.Service.NovelService.Services.SourceAdapters; +using FictionArchive.Service.NovelService.Services.SourceAdapters.Novelpia; +using FictionArchive.Service.Shared.Services.GraphQL; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.NovelService; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddMemoryCache(); + + #region GraphQL + + builder.Services.AddGraphQLServer() + .AddQueryType() + .AddMutationType() + .AddType() + .AddMutationConventions(applyToAllMutations: true) + .AddFiltering(opt => opt.AddDefaults().BindRuntimeType()) + .AddSorting() + .AddProjections(); + + #endregion + + #region Database + + builder.Services.AddDbContext(opt => + { + opt.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")); + }); + + #endregion + + #region Source Adapters + + builder.Services.Configure(builder.Configuration.GetSection("Novelpia")); + builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://novelpia.com"); + }); + builder.Services.AddHttpClient(client => + { + client.BaseAddress = new Uri("https://novelpia.com"); + }) + .AddHttpMessageHandler(); + + #endregion + + builder.Services.AddHealthChecks(); + + var app = builder.Build(); + + // Update database + using (var scope = app.Services.CreateScope()) + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.UpdateDatabase(); + } + + app.UseHttpsRedirection(); + + app.MapHealthChecks("/healthz"); + + app.MapGraphQL(); + + app.Run(); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Properties/launchSettings.json b/FictionArchive.Service.NovelService/Properties/launchSettings.json new file mode 100644 index 0000000..b687f10 --- /dev/null +++ b/FictionArchive.Service.NovelService/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:32130", + "sslPort": 44387 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5101", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "graphql", + "applicationUrl": "https://localhost:7208;http://localhost:5101", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/FictionArchive.Service.NovelService/Services/NovelServiceDbContext.cs b/FictionArchive.Service.NovelService/Services/NovelServiceDbContext.cs new file mode 100644 index 0000000..12f5be2 --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/NovelServiceDbContext.cs @@ -0,0 +1,33 @@ +using FictionArchive.Service.NovelService.Models.Novels; +using Microsoft.EntityFrameworkCore; + +namespace FictionArchive.Service.NovelService.Services; + +public class NovelServiceDbContext(DbContextOptions options, ILogger logger) + : DbContext(options) +{ + public DbSet Novels { get; set; } + public DbSet Sources { get; set; } + public DbSet TranslationEngines { get; set; } + public DbSet Tags { get; set; } + + private readonly ILogger _logger = logger; + + public void UpdateDatabase() + { + IEnumerable pendingMigrations = Database.GetPendingMigrations(); + if (!pendingMigrations.Any()) + { + _logger.LogDebug("No pending migrations found, continuing."); + return; + } + + foreach (string migration in pendingMigrations) + { + _logger.LogInformation("Found pending migration with name {migrationName}.", migration); + } + _logger.LogInformation("Attempting to apply pending migrations..."); + Database.Migrate(); + _logger.LogInformation("Migrations applied."); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/SourceAdapters/ISourceAdapter.cs b/FictionArchive.Service.NovelService/Services/SourceAdapters/ISourceAdapter.cs new file mode 100644 index 0000000..cd4ff0b --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/SourceAdapters/ISourceAdapter.cs @@ -0,0 +1,12 @@ +using FictionArchive.Service.NovelService.Models.Novels; +using FictionArchive.Service.NovelService.Models.SourceAdapters; + +namespace FictionArchive.Service.NovelService.Services.SourceAdapters; + +public interface ISourceAdapter +{ + public SourceDescriptor SourceDescriptor { get; } + public Task CanProcessNovel(string url); + public Task GetMetadata(string novelUrl); + public Task GetRawChapter(string chapterUrl); +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaAdapter.cs b/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaAdapter.cs new file mode 100644 index 0000000..b99e785 --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaAdapter.cs @@ -0,0 +1,200 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using FictionArchive.Common.Enums; +using FictionArchive.Service.NovelService.Constants; +using FictionArchive.Service.NovelService.Models.Enums; +using FictionArchive.Service.NovelService.Models.SourceAdapters; + +namespace FictionArchive.Service.NovelService.Services.SourceAdapters.Novelpia; + +public class NovelpiaAdapter : ISourceAdapter +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + private const string NovelIdRegex = @"novelpia.com\/novel\/(\d+)"; + private const string ChapterIdRegex = @"novelpia.com\/viewer\/(\d+)"; + private const string EpisodeListEndpoint = "/proc/episode_list"; + private const string ChapterDownloadEndpoint = "/proc/viewer_data/"; + + private const string SourceKey = "novelpia"; + private const string SourceName = "Novelpia"; + private const string SourceUrl = "https://novelpia.com"; + + private const string ChapterDownloadFailedMessage = "본인인증"; + + public NovelpiaAdapter(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public SourceDescriptor SourceDescriptor + { + get + { + return new SourceDescriptor() + { + Name = SourceName, + Key = SourceKey, + Url = SourceUrl + }; + } + } + + public async Task CanProcessNovel(string url) + { + return Regex.IsMatch(url, @"https://novelpia.com/novel/(\d+)"); + } + + public async Task GetMetadata(string novelUrl) + { + // PROCESS + // Get novelurl + // Title is
따먹히는 순애 금태양
+ // Author is 구다수 + // Chapters are gotten from the episode_list proc + + uint novelId = uint.Parse(Regex.Match(novelUrl, NovelIdRegex).Groups[1].Value); + + NovelMetadata novel = new NovelMetadata() + { + Url = novelUrl, + RawLanguage = Language.Kr, + ExternalId = novelId.ToString(), + SystemTags = new List(), + SourceTags = new List(), + Chapters = new List(), + SourceDescriptor = SourceDescriptor + }; + + // Novel metadata + var novelData = await _httpClient.GetStringAsync(novelUrl); + var novelNameMatch = Regex.Match(novelData, @"
(.+)<\/div>"); + var authorMatch = Regex.Match(novelData, @"(?s)\s*(.*?)\s*<\/a>"); + var descriptionMatch = Regex.Match(novelData, @"(?s)\s*(.*?)\s*<\/div>"); + + novel.Name = novelNameMatch.Groups[1].Value; + novel.Description = descriptionMatch.Groups[1].Value; + novel.AuthorName = authorMatch.Groups[2].Value; + novel.AuthorUrl = authorMatch.Groups[2].Value; + + // Some badge info + var badgeSet = Regex.Match(novelData, @"(?s)(.*?)<\/p>"); + var badgeMatches = Regex.Matches(badgeSet.Groups[1].Value, @"]*>(.*?)<\/span>"); + foreach (Match badge in badgeMatches) + { + var innerText = badge.Groups[1].Value; + if (innerText == "19") + { + novel.SystemTags.Add(SystemTags.Nsfw); + } + + if (innerText == "완결") + { + novel.RawStatus = NovelStatus.Completed; + } + else + { + novel.RawStatus = NovelStatus.InProgress; + } + } + + // Novel tags + HashSet tags = new HashSet(); + var tagSetMatch = Regex.Match(novelData, @"(?s)(.*?)<\/p>"); + var tagMatches = + Regex.Matches(tagSetMatch.Groups[1].Value, @"]*>#(.*?)<\/span>"); + foreach (Match tagMatch in tagMatches) + { + var tagText = tagMatch.Groups[1].Value; + tags.Add(tagText); + } + + foreach (string tag in tags) + { + novel.SourceTags.Add(tag); + } + + // Chapters + uint page = 0; + List chapters = new List(); + List seenChapterIds = new List(); + uint chapterOrder = 0; + while (true) + { + await Task.Delay(500); + _logger.LogInformation("Next chapter batch"); + var response = await _httpClient.PostAsync(EpisodeListEndpoint, new FormUrlEncodedContent(new Dictionary + { + {"novel_no", novelId.ToString()}, + {"sort", "DOWN"}, + {"page", page.ToString()} + })); + var responseContent = await response.Content.ReadAsStringAsync(); + var capturedChapters = Regex.Matches(responseContent, @"id=""bookmark_(\d+)"">(.+?)"); + if (seenChapterIds.Contains(uint.Parse(capturedChapters[0].Groups[1].Value))) + { + break; + } + foreach (Match chapter in capturedChapters) + { + string chapterId = chapter.Groups[1].Value; + string chapterName = chapter.Groups[2].Value; + chapters.Add(new ChapterMetadata + { + Revision = 0, + Order = chapterOrder, + Url = $"https://novelpia.com/viewer/{chapterId}", + Name = chapterName + }); + seenChapterIds.Add(uint.Parse(chapterId)); + chapterOrder++; + } + page++; + } + novel.Chapters = chapters; + + return novel; + } + + public async Task GetRawChapter(string chapterUrl) + { + var chapterId = uint.Parse(Regex.Match(chapterUrl, ChapterIdRegex).Groups[1].Value); + var endpoint = ChapterDownloadEndpoint + chapterId; + var result = await _httpClient.PostAsync(endpoint, null); + var responseContent = await result.Content.ReadAsStringAsync(); + + if (string.IsNullOrEmpty(responseContent) || responseContent.Contains(ChapterDownloadFailedMessage)) + { + throw new Exception(); + } + + StringBuilder builder = new StringBuilder(); + using var doc = JsonDocument.Parse(responseContent); + JsonElement root = doc.RootElement; + + // Get the "s" array + JsonElement sArray = root.GetProperty("s"); + + foreach (JsonElement item in sArray.EnumerateArray()) + { + string text = item.GetProperty("text").GetString(); + if (text.Contains("cover-wrapper")) + { + continue; + } + if (text.Contains("opacity: 0")) + { + continue; + } + + builder.Append(WebUtility.HtmlDecode(text)); + } + + return builder.ToString(); + + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaAuthMessageHandler.cs b/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaAuthMessageHandler.cs new file mode 100644 index 0000000..532985c --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaAuthMessageHandler.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace FictionArchive.Service.NovelService.Services.SourceAdapters.Novelpia; + +public class NovelpiaAuthMessageHandler : DelegatingHandler +{ + private readonly HttpClient _httpClient; + private readonly IMemoryCache _cache; + private readonly NovelpiaConfiguration _configuration; + private const string CacheKey = "novelpia_loginkey"; + private const string LoginUrl = "/proc/login"; + private const string LoginSuccessMessage = "감사합니다"; + private const string UserAgent = + "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"; + + public NovelpiaAuthMessageHandler(HttpClient httpClient, IOptions configuration, IMemoryCache cache) + { + _httpClient = httpClient; + _configuration = configuration.Value; + _cache = cache; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + string loginKey = await GetLoginKey(); + request.Headers.Add("cookie", $"LOGINKEY={loginKey}"); + request.Headers.UserAgent.ParseAdd(UserAgent); + return await base.SendAsync(request, cancellationToken); + } + + private async Task GetLoginKey() + { + if (!_cache.TryGetValue(CacheKey, out string? loginKey)) + { + var random = new Random(); + var characters = "0123456789abcdef"; + var firstPart = new string(Enumerable.Range(0, 32).Select(_ => characters[random.Next(characters.Length)]).ToArray()); + var secondPart = new string(Enumerable.Range(0, 32).Select(_ => characters[random.Next(characters.Length)]).ToArray()); + loginKey = firstPart + "_" + secondPart; + + HttpRequestMessage loginMessage = new HttpRequestMessage(HttpMethod.Post, LoginUrl); + loginMessage.Headers.Add("cookie", $"LOGINKEY={loginKey}"); + loginMessage.Headers.UserAgent.ParseAdd(UserAgent); + loginMessage.Content = new FormUrlEncodedContent(new Dictionary + { + { "redirecturl", string.Empty }, + { "email", _configuration.Username }, + { "wd", _configuration.Password } + }); + + var response = await _httpClient.SendAsync(loginMessage); + using (var streamReader = new StreamReader(response.Content.ReadAsStream())) + { + if (streamReader.ReadToEnd().Contains(LoginSuccessMessage)) + { + _cache.Set(CacheKey, loginKey); + } + } + } + + return loginKey; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaConfiguration.cs b/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaConfiguration.cs new file mode 100644 index 0000000..f6e6dc1 --- /dev/null +++ b/FictionArchive.Service.NovelService/Services/SourceAdapters/Novelpia/NovelpiaConfiguration.cs @@ -0,0 +1,7 @@ +namespace FictionArchive.Service.NovelService.Services.SourceAdapters.Novelpia; + +public class NovelpiaConfiguration +{ + public string Username { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.NovelService/appsettings.Development.json b/FictionArchive.Service.NovelService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/FictionArchive.Service.NovelService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/FictionArchive.Service.NovelService/appsettings.json b/FictionArchive.Service.NovelService/appsettings.json new file mode 100644 index 0000000..985ae9d --- /dev/null +++ b/FictionArchive.Service.NovelService/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Novelpia": { + "Username": "REPLACE_ME", + "Password": "REPLACE_ME" + }, + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Database=FictionArchive_NovelService;Username=postgres;password=postgres" + }, + "AllowedHosts": "*" +} diff --git a/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj b/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj new file mode 100644 index 0000000..413ef24 --- /dev/null +++ b/FictionArchive.Service.Shared/FictionArchive.Service.Shared.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/FictionArchive.Service.Shared/Services/FictionArchiveDbContext.cs b/FictionArchive.Service.Shared/Services/FictionArchiveDbContext.cs new file mode 100644 index 0000000..f5c3698 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/FictionArchiveDbContext.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace FictionArchive.Service.Shared.Services; + +/// +/// Abstract DbContext handling boilerplate shared between our contexts. Should not share actual data. +/// +public abstract class FictionArchiveDbContext : DbContext +{ + protected readonly ILogger _logger; + + protected FictionArchiveDbContext(DbContextOptions options, ILogger logger) : base(options) + { + _logger = logger; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.Shared/Services/GraphQL/UnsignedIntOperationFilterInputType.cs b/FictionArchive.Service.Shared/Services/GraphQL/UnsignedIntOperationFilterInputType.cs new file mode 100644 index 0000000..cbc3b84 --- /dev/null +++ b/FictionArchive.Service.Shared/Services/GraphQL/UnsignedIntOperationFilterInputType.cs @@ -0,0 +1,13 @@ +using HotChocolate.Data.Filters; + +namespace FictionArchive.Service.Shared.Services.GraphQL; + +public class UnsignedIntOperationFilterInputType + : ComparableOperationFilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Name("UnsignedIntOperationFilterInputType"); + base.Configure(descriptor); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Dockerfile b/FictionArchive.Service.TranslationService/Dockerfile new file mode 100644 index 0000000..5f82a9b --- /dev/null +++ b/FictionArchive.Service.TranslationService/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj", "FictionArchive.Service.TranslationService/"] +RUN dotnet restore "FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj" +COPY . . +WORKDIR "/src/FictionArchive.Service.TranslationService" +RUN dotnet build "./FictionArchive.Service.TranslationService.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./FictionArchive.Service.TranslationService.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "FictionArchive.Service.TranslationService.dll"] diff --git a/FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj b/FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj new file mode 100644 index 0000000..77a3c6f --- /dev/null +++ b/FictionArchive.Service.TranslationService/FictionArchive.Service.TranslationService.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + .dockerignore + + + + + + + + + diff --git a/FictionArchive.Service.TranslationService/GraphQL/Mutation.cs b/FictionArchive.Service.TranslationService/GraphQL/Mutation.cs new file mode 100644 index 0000000..fde7ac6 --- /dev/null +++ b/FictionArchive.Service.TranslationService/GraphQL/Mutation.cs @@ -0,0 +1,14 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.TranslationService.Services.TranslationEngines; + +namespace FictionArchive.Service.TranslationService.GraphQL; + +public class Mutation +{ + public async Task TranslateText(string text, Language from, Language to, string translationEngineKey, IEnumerable translationEngines) + { + var engine = translationEngines.FirstOrDefault(engine => engine.Descriptor.Key == translationEngineKey); + var translation = await engine.GetTranslation(text, from, to); + return translation; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/GraphQL/Query.cs b/FictionArchive.Service.TranslationService/GraphQL/Query.cs new file mode 100644 index 0000000..d4737bf --- /dev/null +++ b/FictionArchive.Service.TranslationService/GraphQL/Query.cs @@ -0,0 +1,14 @@ +using FictionArchive.Service.TranslationService.Models; +using FictionArchive.Service.TranslationService.Services.TranslationEngines; + +namespace FictionArchive.Service.TranslationService.GraphQL; + +public class Query +{ + [UseFiltering] + [UseSorting] + public IEnumerable GetTranslationEngines(IEnumerable engines) + { + return engines.Select(engine => engine.Descriptor); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Models/TranslationEngineDescriptor.cs b/FictionArchive.Service.TranslationService/Models/TranslationEngineDescriptor.cs new file mode 100644 index 0000000..9b29623 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Models/TranslationEngineDescriptor.cs @@ -0,0 +1,7 @@ +namespace FictionArchive.Service.TranslationService.Models; + +public class TranslationEngineDescriptor +{ + public string DisplayName { get; set; } + public string Key { get; set; } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Program.cs b/FictionArchive.Service.TranslationService/Program.cs new file mode 100644 index 0000000..fc20d89 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Program.cs @@ -0,0 +1,50 @@ +using DeepL; +using FictionArchive.Service.Shared.Services.GraphQL; +using FictionArchive.Service.TranslationService.GraphQL; +using FictionArchive.Service.TranslationService.Services.TranslationEngines; +using FictionArchive.Service.TranslationService.Services.TranslationEngines.DeepLTranslate; + +namespace FictionArchive.Service.TranslationService; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddHealthChecks(); + + #region GraphQL + + builder.Services.AddGraphQLServer() + .AddQueryType() + .AddMutationType() + .AddType() + .AddMutationConventions(applyToAllMutations: true) + .AddFiltering(opt => opt.AddDefaults().BindRuntimeType()) + .AddSorting() + .AddProjections(); + + #endregion + + #region Translation Adapter + + builder.Services.AddTransient(provider => + { + return new DeepLClient(builder.Configuration["DeepL:ApiKey"]); + }); + builder.Services.AddTransient(); + + #endregion + + var app = builder.Build(); + + app.UseHttpsRedirection(); + + app.MapHealthChecks("/healthz"); + + app.MapGraphQL(); + + app.Run(); + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Properties/launchSettings.json b/FictionArchive.Service.TranslationService/Properties/launchSettings.json new file mode 100644 index 0000000..4c07ce3 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36751", + "sslPort": 44335 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5134", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "graphql", + "applicationUrl": "https://localhost:7275;http://localhost:5134", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/FictionArchive.Service.TranslationService/Services/TranslationEngines/DeepLTranslate/DeepLTranslationAdapater.cs b/FictionArchive.Service.TranslationService/Services/TranslationEngines/DeepLTranslate/DeepLTranslationAdapater.cs new file mode 100644 index 0000000..af6b189 --- /dev/null +++ b/FictionArchive.Service.TranslationService/Services/TranslationEngines/DeepLTranslate/DeepLTranslationAdapater.cs @@ -0,0 +1,51 @@ +using DeepL; +using DeepL.Model; +using FictionArchive.Service.TranslationService.Models; +using Language = FictionArchive.Common.Enums.Language; + +namespace FictionArchive.Service.TranslationService.Services.TranslationEngines.DeepLTranslate; + +public class DeepLTranslationAdapater : ITranslationEngineAdapter +{ + private readonly DeepLClient _deepLClient; + private readonly ILogger _logger; + + private const string DisplayName = "DeepL"; + private const string Key = "deepl"; + + public DeepLTranslationAdapater(DeepLClient deepLClient, ILogger logger) + { + _deepLClient = deepLClient; + _logger = logger; + } + + public TranslationEngineDescriptor Descriptor + { + get + { + return new TranslationEngineDescriptor() + { + DisplayName = DisplayName, + Key = Key, + }; + } + } + + 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; + } + + private string GetLanguageCode(Language language) + { + return language switch + { + Language.En => LanguageCode.EnglishAmerican, + Language.Kr => LanguageCode.Korean, + Language.Ch => LanguageCode.Chinese, + Language.Ja => LanguageCode.Japanese + }; + } +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/Services/TranslationEngines/ITranslationEngineAdapter.cs b/FictionArchive.Service.TranslationService/Services/TranslationEngines/ITranslationEngineAdapter.cs new file mode 100644 index 0000000..af9d71c --- /dev/null +++ b/FictionArchive.Service.TranslationService/Services/TranslationEngines/ITranslationEngineAdapter.cs @@ -0,0 +1,10 @@ +using FictionArchive.Common.Enums; +using FictionArchive.Service.TranslationService.Models; + +namespace FictionArchive.Service.TranslationService.Services.TranslationEngines; + +public interface ITranslationEngineAdapter +{ + public TranslationEngineDescriptor Descriptor { get; } + public Task GetTranslation(string body, Language from, Language to); +} \ No newline at end of file diff --git a/FictionArchive.Service.TranslationService/appsettings.Development.json b/FictionArchive.Service.TranslationService/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/FictionArchive.Service.TranslationService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/FictionArchive.Service.TranslationService/appsettings.json b/FictionArchive.Service.TranslationService/appsettings.json new file mode 100644 index 0000000..138752d --- /dev/null +++ b/FictionArchive.Service.TranslationService/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DeepL": { + "ApiKey": "REPLACE_ME" + }, + "AllowedHosts": "*" +} diff --git a/FictionArchive.sln b/FictionArchive.sln new file mode 100644 index 0000000..a22fd12 --- /dev/null +++ b/FictionArchive.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Common", "FictionArchive.Common\FictionArchive.Common.csproj", "{ABF1BA10-9E76-45BE-9947-E20445A68147}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.API", "FictionArchive.API\FictionArchive.API.csproj", "{420CC1A1-9DBC-40EC-B9E3-D4B25D71B9A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.NovelService", "FictionArchive.Service.NovelService\FictionArchive.Service.NovelService.csproj", "{546231B6-CE6C-4600-A089-A25FE0F61006}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.TranslationService", "FictionArchive.Service.TranslationService\FictionArchive.Service.TranslationService.csproj", "{BE858DD7-C2A8-44D7-B4DB-9668E5BC9A26}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FictionArchive.Service.Shared", "FictionArchive.Service.Shared\FictionArchive.Service.Shared.csproj", "{82638874-304C-43E6-8EFA-8AD4C41C4435}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ABF1BA10-9E76-45BE-9947-E20445A68147}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABF1BA10-9E76-45BE-9947-E20445A68147}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABF1BA10-9E76-45BE-9947-E20445A68147}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABF1BA10-9E76-45BE-9947-E20445A68147}.Release|Any CPU.Build.0 = Release|Any CPU + {420CC1A1-9DBC-40EC-B9E3-D4B25D71B9A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {420CC1A1-9DBC-40EC-B9E3-D4B25D71B9A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {420CC1A1-9DBC-40EC-B9E3-D4B25D71B9A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {420CC1A1-9DBC-40EC-B9E3-D4B25D71B9A9}.Release|Any CPU.Build.0 = Release|Any CPU + {546231B6-CE6C-4600-A089-A25FE0F61006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {546231B6-CE6C-4600-A089-A25FE0F61006}.Debug|Any CPU.Build.0 = Debug|Any CPU + {546231B6-CE6C-4600-A089-A25FE0F61006}.Release|Any CPU.ActiveCfg = Release|Any CPU + {546231B6-CE6C-4600-A089-A25FE0F61006}.Release|Any CPU.Build.0 = Release|Any CPU + {BE858DD7-C2A8-44D7-B4DB-9668E5BC9A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE858DD7-C2A8-44D7-B4DB-9668E5BC9A26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE858DD7-C2A8-44D7-B4DB-9668E5BC9A26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE858DD7-C2A8-44D7-B4DB-9668E5BC9A26}.Release|Any CPU.Build.0 = Release|Any CPU + {82638874-304C-43E6-8EFA-8AD4C41C4435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82638874-304C-43E6-8EFA-8AD4C41C4435}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82638874-304C-43E6-8EFA-8AD4C41C4435}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82638874-304C-43E6-8EFA-8AD4C41C4435}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal