Initial efcore migration and updates to make sure upserting novels (mostly) works. still need to do chapter handling
This commit is contained in:
@@ -18,14 +18,14 @@ public static class BuilderExtensions
|
|||||||
string dbConnectionString = config.GetConnectionString("DefaultConnection");
|
string dbConnectionString = config.GetConnectionString("DefaultConnection");
|
||||||
collection.AddDbContext<AppDbContext>(opt =>
|
collection.AddDbContext<AppDbContext>(opt =>
|
||||||
{
|
{
|
||||||
opt.UseNpgsql(dbConnectionString);
|
opt.UseSqlite(dbConnectionString);
|
||||||
});
|
});
|
||||||
Type[] repositories = Assembly.GetExecutingAssembly().GetTypes()
|
Type[] repositories = Assembly.GetExecutingAssembly().GetTypes()
|
||||||
.Where(t => t.IsClass && !t.IsAbstract && (t.Namespace?.Contains(nameof(DBConnection.Repositories)) ?? false) && typeof(IRepository).IsAssignableFrom(t)).ToArray();
|
.Where(t => t.IsClass && !t.IsAbstract && (t.Namespace?.Contains(nameof(DBConnection.Repositories)) ?? false) && typeof(IRepository).IsAssignableFrom(t)).ToArray();
|
||||||
foreach (var repo in repositories)
|
foreach (var repo in repositories)
|
||||||
{
|
{
|
||||||
var repoInterface = repo.GetInterfaces()
|
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)
|
if (repoInterface != null)
|
||||||
{
|
{
|
||||||
collection.AddScoped(repoInterface, repo);
|
collection.AddScoped(repoInterface, repo);
|
||||||
|
|||||||
250
DBConnection/Migrations/20220715030913_Initial.Designer.cs
generated
Normal file
250
DBConnection/Migrations/20220715030913_Initial.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
DBConnection/Migrations/20220715030913_Initial.cs
Normal file
195
DBConnection/Migrations/20220715030913_Initial.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
248
DBConnection/Migrations/AppDbContextModelSnapshot.cs
Normal file
248
DBConnection/Migrations/AppDbContextModelSnapshot.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace DBConnection.Models;
|
namespace DBConnection.Models;
|
||||||
|
|
||||||
@@ -7,5 +8,6 @@ public class Author : BaseEntity
|
|||||||
[Key]
|
[Key]
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
[JsonIgnore]
|
||||||
public List<Novel> Novels { get; set; }
|
public List<Novel> Novels { get; set; }
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,11 @@ namespace DBConnection.Models;
|
|||||||
|
|
||||||
public class Chapter : BaseEntity
|
public class Chapter : BaseEntity
|
||||||
{
|
{
|
||||||
[Key]
|
|
||||||
public int ChapterNumber { get; set; }
|
public int ChapterNumber { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string? Content { get; set; }
|
public string? Content { get; set; }
|
||||||
public string? RawContent { get; set; }
|
public string? RawContent { get; set; }
|
||||||
|
[Key]
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public DateTime DatePosted { get; set; }
|
public DateTime DatePosted { get; set; }
|
||||||
public DateTime DateUpdated { get; set; }
|
public DateTime DateUpdated { get; set; }
|
||||||
|
|||||||
17
DBConnection/Repositories/AuthorRepository.cs
Normal file
17
DBConnection/Repositories/AuthorRepository.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,8 @@ public abstract class BaseRepository<TEntityType> : IRepository<TEntityType> whe
|
|||||||
private object?[]? GetPrimaryKey(TEntityType entity)
|
private object?[]? GetPrimaryKey(TEntityType entity)
|
||||||
{
|
{
|
||||||
var keyProperties = DbContext.Model.FindEntityType(typeof(TEntityType))?.FindPrimaryKey()?.Properties.Select(p => p.Name);
|
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();
|
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)
|
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)
|
public virtual async Task<TEntityType?> GetIncluded(Func<TEntityType, bool> predicate)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using DBConnection.Models;
|
||||||
|
|
||||||
|
namespace DBConnection.Repositories.Interfaces;
|
||||||
|
|
||||||
|
public interface IAuthorRepository : IRepository<Author>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
8
DBConnection/Repositories/Interfaces/ITagRepository.cs
Normal file
8
DBConnection/Repositories/Interfaces/ITagRepository.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using DBConnection.Models;
|
||||||
|
|
||||||
|
namespace DBConnection.Repositories.Interfaces;
|
||||||
|
|
||||||
|
public interface ITagRepository : IRepository<Tag>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,9 +6,32 @@ namespace DBConnection.Repositories;
|
|||||||
|
|
||||||
public class NovelRepository : BaseRepository<Novel>, INovelRepository
|
public class NovelRepository : BaseRepository<Novel>, INovelRepository
|
||||||
{
|
{
|
||||||
|
private readonly IAuthorRepository _authorRepository;
|
||||||
public NovelRepository(AppDbContext dbContext) : base(dbContext)
|
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()
|
protected override IQueryable<Novel> GetAllIncludedQueryable()
|
||||||
|
|||||||
17
DBConnection/Repositories/TagRepository.cs
Normal file
17
DBConnection/Repositories/TagRepository.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,7 +70,7 @@ public abstract class ApiAccessLayer
|
|||||||
return await SendRequest(message);
|
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);
|
HttpRequestMessage message = CreateRequestMessage(endpoint, method, queryParams, data);
|
||||||
return await SendRequest<T>(message);
|
return await SendRequest<T>(message);
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DBConnection;
|
using DBConnection;
|
||||||
|
using DBConnection.Models;
|
||||||
|
using DBConnection.Repositories;
|
||||||
|
using DBConnection.Repositories.Interfaces;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using WebNovelPortalAPI.DTO;
|
||||||
|
using WebNovelPortalAPI.Scrapers;
|
||||||
|
|
||||||
namespace WebNovelPortalAPI.Controllers
|
namespace WebNovelPortalAPI.Controllers
|
||||||
{
|
{
|
||||||
@@ -12,11 +17,42 @@ namespace WebNovelPortalAPI.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
public class NovelController : ControllerBase
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
WebNovelPortalAPI/DTO/ScrapeNovelRequest.cs
Normal file
6
WebNovelPortalAPI/DTO/ScrapeNovelRequest.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace WebNovelPortalAPI.DTO;
|
||||||
|
|
||||||
|
public class ScrapeNovelRequest
|
||||||
|
{
|
||||||
|
public string NovelUrl { get; set; }
|
||||||
|
}
|
||||||
18
WebNovelPortalAPI/Extensions/ScraperExtensions.cs
Normal file
18
WebNovelPortalAPI/Extensions/ScraperExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
using DBConnection;
|
using DBConnection;
|
||||||
using DBConnection.Extensions;
|
using DBConnection.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using WebNovelPortalAPI.Extensions;
|
||||||
|
using WebNovelPortalAPI.Scrapers;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddDbServices(builder.Configuration);
|
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
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace WebNovelPortalAPI.Scrapers;
|
|
||||||
|
|
||||||
public class AbstractScraper
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,8 @@ namespace WebNovelPortalAPI.Scrapers;
|
|||||||
|
|
||||||
public interface IScraper
|
public interface IScraper
|
||||||
{
|
{
|
||||||
|
public bool MatchesUrl(string url);
|
||||||
public Novel ScrapeNovel(string url);
|
public Novel ScrapeNovel(string url);
|
||||||
|
public string? ScrapeChapterContent(string chapterUrl);
|
||||||
|
|
||||||
}
|
}
|
||||||
102
WebNovelPortalAPI/Scrapers/KakuyomuScraper.cs
Normal file
102
WebNovelPortalAPI/Scrapers/KakuyomuScraper.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace WebNovelPortalAPI.TranslationEngines;
|
||||||
|
|
||||||
|
public interface ITranslationEngine
|
||||||
|
{
|
||||||
|
public string Translate(string text);
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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.Sqlite" Version="6.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "null"
|
"DefaultConnection": "Data Source=/home/m/Documents/WebNovelPortal/WebNovelPortalAPI/test_db"
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user