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");
|
||||
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);
|
||||
|
||||
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 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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 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()
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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();
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace WebNovelPortalAPI.Scrapers;
|
||||
|
||||
public class AbstractScraper
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
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>
|
||||
|
||||
<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">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "null"
|
||||
"DefaultConnection": "Data Source=/home/m/Documents/WebNovelPortal/WebNovelPortalAPI/test_db"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user