Initial efcore migration and updates to make sure upserting novels (mostly) works. still need to do chapter handling

This commit is contained in:
2022-07-14 23:12:12 -04:00
parent 5402923e9f
commit 5337e7ccb8
25 changed files with 962 additions and 64 deletions

View File

@@ -18,14 +18,14 @@ public static class BuilderExtensions
string dbConnectionString = config.GetConnectionString("DefaultConnection");
collection.AddDbContext<AppDbContext>(opt =>
{
opt.UseNpgsql(dbConnectionString);
opt.UseSqlite(dbConnectionString);
});
Type[] repositories = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && (t.Namespace?.Contains(nameof(DBConnection.Repositories)) ?? false) && typeof(IRepository).IsAssignableFrom(t)).ToArray();
foreach (var repo in repositories)
{
var repoInterface = repo.GetInterfaces()
.FirstOrDefault(repoInterface => typeof(IRepository).IsAssignableFrom(repoInterface) && repoInterface != typeof(IRepository));
.FirstOrDefault(repoInterface => typeof(IRepository).IsAssignableFrom(repoInterface) && repoInterface != typeof(IRepository) && !repoInterface.IsGenericType);
if (repoInterface != null)
{
collection.AddScoped(repoInterface, repo);

View File

@@ -0,0 +1,250 @@
// <auto-generated />
using System;
using DBConnection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DBConnection.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20220715030913_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("DBConnection.Models.Author", b =>
{
b.Property<string>("Url")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Url");
b.ToTable("Authors");
});
modelBuilder.Entity("DBConnection.Models.Chapter", b =>
{
b.Property<string>("Url")
.HasColumnType("TEXT");
b.Property<int>("ChapterNumber")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<DateTime>("DatePosted")
.HasColumnType("TEXT");
b.Property<DateTime>("DateUpdated")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("NovelUrl")
.HasColumnType("TEXT");
b.Property<string>("RawContent")
.HasColumnType("TEXT");
b.HasKey("Url");
b.HasIndex("NovelUrl");
b.ToTable("Chapters");
});
modelBuilder.Entity("DBConnection.Models.Novel", b =>
{
b.Property<string>("Url")
.HasColumnType("TEXT");
b.Property<string>("AuthorUrl")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<DateTime>("DatePosted")
.HasColumnType("TEXT");
b.Property<DateTime>("LastUpdated")
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Url");
b.HasIndex("AuthorUrl");
b.ToTable("Novels");
});
modelBuilder.Entity("DBConnection.Models.Tag", b =>
{
b.Property<string>("TagValue")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.HasKey("TagValue");
b.ToTable("Tags");
});
modelBuilder.Entity("DBConnection.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("DBConnection.Models.UserNovel", b =>
{
b.Property<string>("NovelUrl")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("LastChapterRead")
.HasColumnType("INTEGER");
b.HasKey("NovelUrl", "UserId");
b.HasIndex("UserId");
b.ToTable("UserNovels");
});
modelBuilder.Entity("NovelTag", b =>
{
b.Property<string>("NovelsUrl")
.HasColumnType("TEXT");
b.Property<string>("TagsTagValue")
.HasColumnType("TEXT");
b.HasKey("NovelsUrl", "TagsTagValue");
b.HasIndex("TagsTagValue");
b.ToTable("NovelTag");
});
modelBuilder.Entity("DBConnection.Models.Chapter", b =>
{
b.HasOne("DBConnection.Models.Novel", null)
.WithMany("Chapters")
.HasForeignKey("NovelUrl");
});
modelBuilder.Entity("DBConnection.Models.Novel", b =>
{
b.HasOne("DBConnection.Models.Author", "Author")
.WithMany("Novels")
.HasForeignKey("AuthorUrl");
b.Navigation("Author");
});
modelBuilder.Entity("DBConnection.Models.UserNovel", b =>
{
b.HasOne("DBConnection.Models.Novel", "Novel")
.WithMany()
.HasForeignKey("NovelUrl")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DBConnection.Models.User", "User")
.WithMany("WatchedNovels")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Novel");
b.Navigation("User");
});
modelBuilder.Entity("NovelTag", b =>
{
b.HasOne("DBConnection.Models.Novel", null)
.WithMany()
.HasForeignKey("NovelsUrl")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DBConnection.Models.Tag", null)
.WithMany()
.HasForeignKey("TagsTagValue")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DBConnection.Models.Author", b =>
{
b.Navigation("Novels");
});
modelBuilder.Entity("DBConnection.Models.Novel", b =>
{
b.Navigation("Chapters");
});
modelBuilder.Entity("DBConnection.Models.User", b =>
{
b.Navigation("WatchedNovels");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,195 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DBConnection.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Authors",
columns: table => new
{
Url = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Authors", x => x.Url);
});
migrationBuilder.CreateTable(
name: "Tags",
columns: table => new
{
TagValue = table.Column<string>(type: "TEXT", nullable: false),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tags", x => x.TagValue);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Email = table.Column<string>(type: "TEXT", nullable: false),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Novels",
columns: table => new
{
Url = table.Column<string>(type: "TEXT", nullable: false),
Title = table.Column<string>(type: "TEXT", nullable: false),
AuthorUrl = table.Column<string>(type: "TEXT", nullable: true),
LastUpdated = table.Column<DateTime>(type: "TEXT", nullable: false),
DatePosted = table.Column<DateTime>(type: "TEXT", nullable: false),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Novels", x => x.Url);
table.ForeignKey(
name: "FK_Novels_Authors_AuthorUrl",
column: x => x.AuthorUrl,
principalTable: "Authors",
principalColumn: "Url");
});
migrationBuilder.CreateTable(
name: "Chapters",
columns: table => new
{
Url = table.Column<string>(type: "TEXT", nullable: false),
ChapterNumber = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Content = table.Column<string>(type: "TEXT", nullable: true),
RawContent = table.Column<string>(type: "TEXT", nullable: true),
DatePosted = table.Column<DateTime>(type: "TEXT", nullable: false),
DateUpdated = table.Column<DateTime>(type: "TEXT", nullable: false),
NovelUrl = table.Column<string>(type: "TEXT", nullable: true),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Chapters", x => x.Url);
table.ForeignKey(
name: "FK_Chapters_Novels_NovelUrl",
column: x => x.NovelUrl,
principalTable: "Novels",
principalColumn: "Url");
});
migrationBuilder.CreateTable(
name: "NovelTag",
columns: table => new
{
NovelsUrl = table.Column<string>(type: "TEXT", nullable: false),
TagsTagValue = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_NovelTag", x => new { x.NovelsUrl, x.TagsTagValue });
table.ForeignKey(
name: "FK_NovelTag_Novels_NovelsUrl",
column: x => x.NovelsUrl,
principalTable: "Novels",
principalColumn: "Url",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_NovelTag_Tags_TagsTagValue",
column: x => x.TagsTagValue,
principalTable: "Tags",
principalColumn: "TagValue",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserNovels",
columns: table => new
{
UserId = table.Column<int>(type: "INTEGER", nullable: false),
NovelUrl = table.Column<string>(type: "TEXT", nullable: false),
LastChapterRead = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserNovels", x => new { x.NovelUrl, x.UserId });
table.ForeignKey(
name: "FK_UserNovels_Novels_NovelUrl",
column: x => x.NovelUrl,
principalTable: "Novels",
principalColumn: "Url",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserNovels_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Chapters_NovelUrl",
table: "Chapters",
column: "NovelUrl");
migrationBuilder.CreateIndex(
name: "IX_Novels_AuthorUrl",
table: "Novels",
column: "AuthorUrl");
migrationBuilder.CreateIndex(
name: "IX_NovelTag_TagsTagValue",
table: "NovelTag",
column: "TagsTagValue");
migrationBuilder.CreateIndex(
name: "IX_UserNovels_UserId",
table: "UserNovels",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Chapters");
migrationBuilder.DropTable(
name: "NovelTag");
migrationBuilder.DropTable(
name: "UserNovels");
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable(
name: "Novels");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "Authors");
}
}
}

View File

@@ -0,0 +1,248 @@
// <auto-generated />
using System;
using DBConnection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DBConnection.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("DBConnection.Models.Author", b =>
{
b.Property<string>("Url")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Url");
b.ToTable("Authors");
});
modelBuilder.Entity("DBConnection.Models.Chapter", b =>
{
b.Property<string>("Url")
.HasColumnType("TEXT");
b.Property<int>("ChapterNumber")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<DateTime>("DatePosted")
.HasColumnType("TEXT");
b.Property<DateTime>("DateUpdated")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("NovelUrl")
.HasColumnType("TEXT");
b.Property<string>("RawContent")
.HasColumnType("TEXT");
b.HasKey("Url");
b.HasIndex("NovelUrl");
b.ToTable("Chapters");
});
modelBuilder.Entity("DBConnection.Models.Novel", b =>
{
b.Property<string>("Url")
.HasColumnType("TEXT");
b.Property<string>("AuthorUrl")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<DateTime>("DatePosted")
.HasColumnType("TEXT");
b.Property<DateTime>("LastUpdated")
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Url");
b.HasIndex("AuthorUrl");
b.ToTable("Novels");
});
modelBuilder.Entity("DBConnection.Models.Tag", b =>
{
b.Property<string>("TagValue")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.HasKey("TagValue");
b.ToTable("Tags");
});
modelBuilder.Entity("DBConnection.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("DBConnection.Models.UserNovel", b =>
{
b.Property<string>("NovelUrl")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("LastChapterRead")
.HasColumnType("INTEGER");
b.HasKey("NovelUrl", "UserId");
b.HasIndex("UserId");
b.ToTable("UserNovels");
});
modelBuilder.Entity("NovelTag", b =>
{
b.Property<string>("NovelsUrl")
.HasColumnType("TEXT");
b.Property<string>("TagsTagValue")
.HasColumnType("TEXT");
b.HasKey("NovelsUrl", "TagsTagValue");
b.HasIndex("TagsTagValue");
b.ToTable("NovelTag");
});
modelBuilder.Entity("DBConnection.Models.Chapter", b =>
{
b.HasOne("DBConnection.Models.Novel", null)
.WithMany("Chapters")
.HasForeignKey("NovelUrl");
});
modelBuilder.Entity("DBConnection.Models.Novel", b =>
{
b.HasOne("DBConnection.Models.Author", "Author")
.WithMany("Novels")
.HasForeignKey("AuthorUrl");
b.Navigation("Author");
});
modelBuilder.Entity("DBConnection.Models.UserNovel", b =>
{
b.HasOne("DBConnection.Models.Novel", "Novel")
.WithMany()
.HasForeignKey("NovelUrl")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DBConnection.Models.User", "User")
.WithMany("WatchedNovels")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Novel");
b.Navigation("User");
});
modelBuilder.Entity("NovelTag", b =>
{
b.HasOne("DBConnection.Models.Novel", null)
.WithMany()
.HasForeignKey("NovelsUrl")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DBConnection.Models.Tag", null)
.WithMany()
.HasForeignKey("TagsTagValue")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DBConnection.Models.Author", b =>
{
b.Navigation("Novels");
});
modelBuilder.Entity("DBConnection.Models.Novel", b =>
{
b.Navigation("Chapters");
});
modelBuilder.Entity("DBConnection.Models.User", b =>
{
b.Navigation("WatchedNovels");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
namespace DBConnection.Models;
@@ -7,5 +8,6 @@ public class Author : BaseEntity
[Key]
public string Url { get; set; }
public string Name { get; set; }
[JsonIgnore]
public List<Novel> Novels { get; set; }
}

View File

@@ -4,11 +4,11 @@ namespace DBConnection.Models;
public class Chapter : BaseEntity
{
[Key]
public int ChapterNumber { get; set; }
public string Name { get; set; }
public string? Content { get; set; }
public string? RawContent { get; set; }
[Key]
public string Url { get; set; }
public DateTime DatePosted { get; set; }
public DateTime DateUpdated { get; set; }

View File

@@ -0,0 +1,17 @@
using DBConnection.Models;
using DBConnection.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace DBConnection.Repositories;
public class AuthorRepository : BaseRepository<Author>, IAuthorRepository
{
public AuthorRepository(AppDbContext dbContext) : base(dbContext)
{
}
protected override IQueryable<Author> GetAllIncludedQueryable()
{
return DbContext.Authors.Include(i => i.Novels);
}
}

View File

@@ -13,7 +13,8 @@ public abstract class BaseRepository<TEntityType> : IRepository<TEntityType> whe
private object?[]? GetPrimaryKey(TEntityType entity)
{
var keyProperties = DbContext.Model.FindEntityType(typeof(TEntityType))?.FindPrimaryKey()?.Properties.Select(p => p.Name);
return keyProperties?.Select(p => entity.GetType().GetProperty(p)?.GetValue(entity, null)).ToArray();
var ret = keyProperties?.Select(p => entity.GetType().GetProperty(p)?.GetValue(entity, null)).ToArray();
return ret;
}
protected abstract IQueryable<TEntityType> GetAllIncludedQueryable();
@@ -48,7 +49,7 @@ public abstract class BaseRepository<TEntityType> : IRepository<TEntityType> whe
public virtual async Task<TEntityType?> GetIncluded(TEntityType entity)
{
return await GetIncluded(dbEntity => GetPrimaryKey(dbEntity) == GetPrimaryKey(entity));
return await GetIncluded(dbEntity => GetPrimaryKey(dbEntity).SequenceEqual(GetPrimaryKey(entity)));
}
public virtual async Task<TEntityType?> GetIncluded(Func<TEntityType, bool> predicate)

View File

@@ -0,0 +1,8 @@
using DBConnection.Models;
namespace DBConnection.Repositories.Interfaces;
public interface IAuthorRepository : IRepository<Author>
{
}

View File

@@ -0,0 +1,8 @@
using DBConnection.Models;
namespace DBConnection.Repositories.Interfaces;
public interface ITagRepository : IRepository<Tag>
{
}

View File

@@ -6,9 +6,32 @@ namespace DBConnection.Repositories;
public class NovelRepository : BaseRepository<Novel>, INovelRepository
{
public NovelRepository(AppDbContext dbContext) : base(dbContext)
private readonly IAuthorRepository _authorRepository;
private readonly ITagRepository _tagRepository;
public NovelRepository(AppDbContext dbContext, IAuthorRepository authorRepository, ITagRepository tagRepository) : base(dbContext)
{
_authorRepository = authorRepository;
_tagRepository = tagRepository;
}
public override async Task<Novel> Upsert(Novel entity)
{
var dbEntity = await GetIncluded(entity) ?? entity;
dbEntity.Author = await _authorRepository.GetIncluded(entity.Author) ?? entity.Author;
List<Tag> newTags = new List<Tag>();
foreach (var tag in dbEntity.Tags)
{
newTags.Add(await _tagRepository.GetIncluded(tag) ?? tag);
}
dbEntity.Tags.Clear();
dbEntity.Tags = newTags;
if (DbContext.Entry(dbEntity).State == EntityState.Detached)
{
DbContext.Add(dbEntity);
}
await DbContext.SaveChangesAsync();
return dbEntity;
}
protected override IQueryable<Novel> GetAllIncludedQueryable()

View File

@@ -0,0 +1,17 @@
using DBConnection.Models;
using DBConnection.Repositories.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace DBConnection.Repositories;
public class TagRepository : BaseRepository<Tag>, ITagRepository
{
public TagRepository(AppDbContext dbContext) : base(dbContext)
{
}
protected override IQueryable<Tag> GetAllIncludedQueryable()
{
return DbContext.Tags.Include(i => i.Novels);
}
}

View File

@@ -70,7 +70,7 @@ public abstract class ApiAccessLayer
return await SendRequest(message);
}
protected async Task<HttpResponseWrapper<T>> SendGet<T>(string endpoint, HttpMethod method, Dictionary<string, string>? queryParams = null, object? data = null)
protected async Task<HttpResponseWrapper<T>> SendRequest<T>(string endpoint, HttpMethod method, Dictionary<string, string>? queryParams = null, object? data = null)
{
HttpRequestMessage message = CreateRequestMessage(endpoint, method, queryParams, data);
return await SendRequest<T>(message);

View File

@@ -3,8 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DBConnection;
using DBConnection.Models;
using DBConnection.Repositories;
using DBConnection.Repositories.Interfaces;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WebNovelPortalAPI.DTO;
using WebNovelPortalAPI.Scrapers;
namespace WebNovelPortalAPI.Controllers
{
@@ -12,11 +17,42 @@ namespace WebNovelPortalAPI.Controllers
[ApiController]
public class NovelController : ControllerBase
{
private readonly AppDbContext _context;
private readonly INovelRepository _novelRepository;
private readonly IEnumerable<IScraper> _scrapers;
public NovelController(AppDbContext context)
public NovelController(IEnumerable<IScraper> scrapers, INovelRepository novelRepository)
{
_context = context;
_scrapers = scrapers;
_novelRepository = novelRepository;
}
private IScraper? MatchScraper(string novelUrl)
{
return _scrapers.FirstOrDefault(i => i.MatchesUrl(novelUrl));
}
[HttpPost]
[Route("scrapeNovel")]
public async Task<IActionResult> ScrapeNovel(ScrapeNovelRequest request)
{
var scraper = MatchScraper(request.NovelUrl);
if (scraper == null)
{
return BadRequest("Invalid url, no valid scraper configured");
}
Novel novel;
try
{
novel = scraper.ScrapeNovel(request.NovelUrl);
}
catch (Exception e)
{
return StatusCode(500, e);
}
var novelUpload = await _novelRepository.Upsert(novel);
return Ok(novelUpload);
}
}
}

View File

@@ -1,32 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace WebNovelPortalAPI.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

View File

@@ -0,0 +1,6 @@
namespace WebNovelPortalAPI.DTO;
public class ScrapeNovelRequest
{
public string NovelUrl { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System.Reflection;
using WebNovelPortalAPI.Scrapers;
namespace WebNovelPortalAPI.Extensions;
public static class ScraperExtensions
{
public static void AddScrapers(this IServiceCollection services)
{
Type[] types = Assembly.GetExecutingAssembly().GetTypes().Where(t =>
t.IsClass && typeof(IScraper).IsAssignableFrom(t) && (t.Namespace?.Contains(nameof(Scrapers)) ?? false))
.ToArray();
foreach (var t in types)
{
services.AddScoped(typeof(IScraper), t);
}
}
}

View File

@@ -1,12 +1,19 @@
using DBConnection;
using DBConnection.Extensions;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using WebNovelPortalAPI.Extensions;
using WebNovelPortalAPI.Scrapers;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddDbServices(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddScrapers();
builder.Services.AddControllers().AddNewtonsoftJson(opt =>
{
opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

View File

@@ -1,6 +0,0 @@
namespace WebNovelPortalAPI.Scrapers;
public class AbstractScraper
{
}

View File

@@ -4,6 +4,8 @@ namespace WebNovelPortalAPI.Scrapers;
public interface IScraper
{
public bool MatchesUrl(string url);
public Novel ScrapeNovel(string url);
public string? ScrapeChapterContent(string chapterUrl);
}

View File

@@ -0,0 +1,102 @@
using System.Reflection.Metadata;
using System.Text.RegularExpressions;
using DBConnection.Models;
using HtmlAgilityPack;
namespace WebNovelPortalAPI.Scrapers;
public class KakuyomuScraper : IScraper
{
private const string UrlPattern = @"https?:\/\/kakuyomu\.jp\/works\/\d+\/?";
private const string BaseUrl = "https://kakuyomu.jp";
public bool MatchesUrl(string url)
{
var regex = new Regex(UrlPattern, RegexOptions.IgnoreCase);
return regex.IsMatch(url);
}
private string GetNovelTitle(HtmlDocument document)
{
var xpath = @"//*[@id='workTitle']/a";
return document.DocumentNode.SelectSingleNode(xpath).InnerText;
}
private Author GetAuthor(HtmlDocument document)
{
var nameXPath = @"//*[@id='workAuthor-activityName']/a";
var urlXPath = @"//*[@id='workAuthor-activityName']/a";
var authorName = document.DocumentNode.SelectSingleNode(nameXPath).InnerText;
var authorUrl = document.DocumentNode.SelectSingleNode(urlXPath).Attributes["href"].Value;
Author author = new Author
{
Name = authorName,
Url = $"{BaseUrl + authorUrl}"
};
return author;
}
private List<Chapter> GetChapters(HtmlDocument document)
{
var urlxpath = @"//a[@class='widget-toc-episode-episodeTitle']";
var namexpath = @"span";
var urlnodes = document.DocumentNode.SelectNodes(urlxpath);
var chapters = urlnodes.Select((node, i) => new Chapter
{
ChapterNumber = i + 1,
Url = $"{BaseUrl}{node.Attributes["href"].Value}",
Name = node.SelectSingleNode(namexpath).InnerText
});
return chapters.ToList();
}
private List<Tag> GetTags(HtmlDocument document)
{
var xpath = @"//span[@itemprop='keywords']/a";
var nodes = document.DocumentNode.SelectNodes(xpath);
return nodes.Select(node => new Tag
{
TagValue = node.InnerText
}).ToList();
}
private DateTime GetPostedDate(HtmlDocument document)
{
var xpath = @"//time[@itemprop='datePublished']";
return DateTime.Parse(document.DocumentNode.SelectSingleNode(xpath).InnerText);
}
private DateTime GetLastUpdatedDate(HtmlDocument document)
{
var xpath = @"//time[@itemprop='dateModified']";
return DateTime.Parse(document.DocumentNode.SelectSingleNode(xpath).InnerText);
}
public Novel ScrapeNovel(string url)
{
Novel novel = new Novel();
var web = new HtmlWeb();
var doc = web.Load(url);
if (doc == null)
{
throw new Exception("Error parsing document");
}
return new Novel
{
Author = GetAuthor(doc),
Chapters = GetChapters(doc),
DatePosted = GetPostedDate(doc),
LastUpdated = GetLastUpdatedDate(doc),
Tags = GetTags(doc),
Title = GetNovelTitle(doc),
Url = url
};
}
public string? ScrapeChapterContent(string chapterUrl)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,6 @@
namespace WebNovelPortalAPI.TranslationEngines;
public interface ITranslationEngine
{
public string Translate(string text);
}

View File

@@ -1,12 +0,0 @@
namespace WebNovelPortalAPI;
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int) (TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View File

@@ -7,6 +7,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">

View File

@@ -6,7 +6,7 @@
}
},
"ConnectionStrings": {
"DefaultConnection": "null"
"DefaultConnection": "Data Source=/home/m/Documents/WebNovelPortal/WebNovelPortalAPI/test_db"
},
"AllowedHosts": "*"
}