diff --git a/API/API.csproj b/API/API.csproj index fa1bcdfec..55163ad78 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -33,7 +33,7 @@ - + diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 81ea6a6ff..cce0de2b5 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -10,9 +10,11 @@ using API.Entities; using API.Extensions; using API.Helpers; using API.Interfaces; +using API.SignalR; using Kavita.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace API.Controllers @@ -22,12 +24,14 @@ namespace API.Controllers private readonly ILogger _logger; private readonly ITaskScheduler _taskScheduler; private readonly IUnitOfWork _unitOfWork; + private readonly IHubContext _messageHub; - public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork) + public SeriesController(ILogger logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IHubContext messageHub) { _logger = logger; _taskScheduler = taskScheduler; _unitOfWork = unitOfWork; + _messageHub = messageHub; } [HttpPost] @@ -296,6 +300,12 @@ namespace API.Controllers if (await _unitOfWork.CommitAsync()) { + foreach (var tag in updateSeriesMetadataDto.Tags) + { + await _messageHub.Clients.All.SendAsync(SignalREvents.SeriesAddedToCollection, + MessageFactory.SeriesAddedToCollection(tag.Id, + updateSeriesMetadataDto.SeriesMetadata.SeriesId)); + } return Ok("Successfully updated"); } } diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index fb08a53e8..9289cfa21 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using API.Entities.Enums; namespace API.DTOs @@ -7,8 +8,11 @@ namespace API.DTOs { public int Id { get; init; } public string Name { get; init; } - public string CoverImage { get; init; } + /// + /// Last time Library was scanned + /// + public DateTime LastScanned { get; init; } public LibraryType Type { get; init; } public ICollection Folders { get; init; } } -} \ No newline at end of file +} diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs b/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs new file mode 100644 index 000000000..ad28c5839 --- /dev/null +++ b/API/Data/Migrations/20211001113608_LastScannedLibrary.Designer.cs @@ -0,0 +1,1045 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20211001113608_LastScannedLibrary")] + partial class LastScannedLibrary + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.8"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BookReaderDarkMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("SiteDarkMode") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.HasIndex("Name", "NormalizedName", "LocalizedName", "LibraryId", "Format") + .IsUnique(); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("Files"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Metadata"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20211001113608_LastScannedLibrary.cs b/API/Data/Migrations/20211001113608_LastScannedLibrary.cs new file mode 100644 index 000000000..eb1fdc5cb --- /dev/null +++ b/API/Data/Migrations/20211001113608_LastScannedLibrary.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class LastScannedLibrary : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastScanned", + table: "Library", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastScanned", + table: "Library"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 38a09633e..21a9d930a 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -397,6 +397,9 @@ namespace API.Data.Migrations b.Property("LastModified") .HasColumnType("TEXT"); + b.Property("LastScanned") + .HasColumnType("TEXT"); + b.Property("Name") .HasColumnType("TEXT"); diff --git a/API/Data/Repositories/UserRepository.cs b/API/Data/Repositories/UserRepository.cs index 4e20039c7..13642a210 100644 --- a/API/Data/Repositories/UserRepository.cs +++ b/API/Data/Repositories/UserRepository.cs @@ -237,8 +237,8 @@ namespace API.Data.Repositories Libraries = u.Libraries.Select(l => new LibraryDto { Name = l.Name, - CoverImage = l.CoverImage, Type = l.Type, + LastScanned = l.LastScanned, Folders = l.Folders.Select(x => x.Path).ToList() }).ToList() }) diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index faf95e149..c77fb68dd 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -13,9 +13,13 @@ namespace API.Entities public LibraryType Type { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } + /// + /// Last time Library was scanned + /// + public DateTime LastScanned { get; set; } public ICollection Folders { get; set; } public ICollection AppUsers { get; set; } public ICollection Series { get; set; } - + } -} \ No newline at end of file +} diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index 8e2751135..d29dc328a 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -21,32 +21,35 @@ namespace API.Parser public const string SupportedExtensions = ArchiveFileExtensions + "|" + ImageFileExtensions + "|" + BookFileExtensions; + private const RegexOptions MatchOptions = + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant; + public static readonly Regex FontSrcUrlRegex = new Regex(@"(src:url\(.{1})" + "([^\"']*)" + @"(.{1}\))", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout); public static readonly Regex CssImportUrlRegex = new Regex("(@import\\s[\"|'])(?[\\w\\d/\\._-]+)([\"|'];?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout); private static readonly string XmlRegexExtensions = @"\.xml"; private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout); private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions, - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout); private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout); private static readonly Regex BookFileRegex = new Regex(BookFileExtensions, - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout); private static readonly Regex CoverImageRegex = new Regex(@"(?.*)(\b|_)v(?\d+-?\d+)( |_)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // NEEDLESS_Vol.4_-Simeon_6_v2[SugoiSugoi].rar new Regex( @"(?.*)(\b|_)(?!\[)(vol\.?)(?\d+(-\d+)?)(?!\])", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17 new Regex( @"(?.*)(\b|_)(?!\[)v(?\d+(-\d+)?)(?!\])", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Kodomo no Jikan vol. 10 new Regex( @"(?.*)(\b|_)(vol\.? ?)(?\d+(-\d+)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) new Regex( @"(vol\.? ?)(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Tonikaku Cawaii [Volume 11].cbz new Regex( @"(volume )(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Tower Of God S01 014 (CBT) (digital).cbz new Regex( @"(?.*)(\b|_|)(S(?\d+))", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // vol_001-1.cbz for MangaPy default naming convention new Regex( @"(vol_)(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; @@ -99,166 +102,166 @@ namespace API.Parser // Grand Blue Dreaming - SP02 new Regex( @"(?.*)(\b|_|-|\s)(?:sp)\d", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // [SugoiSugoi]_NEEDLESS_Vol.2_-_Disk_The_Informant_5_[ENG].rar, Yuusha Ga Shinda! - Vol.tbd Chapter 27.001 V2 Infection ①.cbz new Regex( @"^(?.*)( |_)Vol\.?(\d+|tbd)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Mad Chimera World - Volume 005 - Chapter 026.cbz (couldn't figure out how to get Volume negative lookaround working on below regex), // The Duke of Death and His Black Maid - Vol. 04 Ch. 054.5 - V4 Omake new Regex( @"(?.+?)(\s|_|-)+(?:Vol(ume|\.)?(\s|_|-)+\d+)(\s|_|-)+(?:(Ch|Chapter|Ch)\.?)(\s|_|-)+(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Ichiban_Ushiro_no_Daimaou_v04_ch34_[VISCANS].zip, VanDread-v01-c01.zip new Regex( @"(?.*)(\b|_)v(?\d+-?\d*)(\s|_|-)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Gokukoku no Brynhildr - c001-008 (v01) [TrinityBAKumA], Black Bullet - v4 c17 [batoto] new Regex( @"(?.*)( - )(?:v|vo|c)\d", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Kedouin Makoto - Corpse Party Musume, Chapter 19 [Dametrans].zip new Regex( @"(?.*)(?:, Chapter )(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Please Go Home, Akutsu-San! - Chapter 038.5 - Volume Announcement.cbz new Regex( @"(?.*)(\s|_|-)(?!Vol)(\s|_|-)(?:Chapter)(\s|_|-)(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // [dmntsf.net] One Piece - Digital Colored Comics Vol. 20 Ch. 177 - 30 Million vs 81 Million.cbz new Regex( @"(?.*) (\b|_|-)(vol)\.?(\s|-|_)?\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // [xPearse] Kyochuu Rettou Volume 1 [English] [Manga] [Volume Scans] new Regex( @"(?.*) (\b|_|-)(vol)(ume)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), //Knights of Sidonia c000 (S2 LE BD Omake - BLAME!) [Habanero Scans] new Regex( @"(?.*)(\bc\d+\b)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), //Tonikaku Cawaii [Volume 11], Darling in the FranXX - Volume 01.cbz new Regex( @"(?.*)(?: _|-|\[|\()\s?vol(ume)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Momo The Blood Taker - Chapter 027 Violent Emotion.cbz, Grand Blue Dreaming - SP02 Extra (2019) (Digital) (danke-Empire).cbz new Regex( @"^(?(?!Vol).+?)(?:(ch(apter|\.)(\b|_|-|\s))|sp)\d", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Historys Strongest Disciple Kenichi_v11_c90-98.zip, Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb) new Regex( @"(?.*) (\b|_|-)(v|ch\.?|c)\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), //Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip must be before [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip // due to duplicate version identifiers in file. new Regex( @"(?.*)(v|s)\d+(-\d+)?(_|\s)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), //[Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip new Regex( @"(?.*)(v|s)\d+(-\d+)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz new Regex( @"(?.*) (?\d+) (?:\(\d{4}\)) ", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Goblin Slayer - Brand New Day 006.5 (2019) (Digital) (danke-Empire) new Regex( @"(?.*) (?\d+(?:.\d+|-\d+)?) \(\d{4}\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Noblesse - Episode 429 (74 Pages).7z new Regex( @"(?.*)(\s|_)(?:Episode|Ep\.?)(\s|_)(?\d+(?:.\d+|-\d+)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Akame ga KILL! ZERO (2016-2019) (Digital) (LuCaZ) new Regex( @"(?.*)\(\d", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Tonikaku Kawaii (Ch 59-67) (Ongoing) new Regex( @"(?.*)(\s|_)\((c\s|ch\s|chapter\s)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Black Bullet (This is very loose, keep towards bottom) new Regex( @"(?.*)(_)(v|vo|c|volume)( |_)\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar new Regex( @"(?.*)( |_)(vol\d+)?( |_)(?:Chp\.? ?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Mahoutsukai to Deshi no Futekisetsu na Kankei Chp. 1 new Regex( @"(?.*)( |_)(?:Chp.? ?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Corpse Party -The Anthology- Sachikos game of love Hysteric Birthday 2U Chapter 01 new Regex( @"^(?!Vol)(?.*)( |_)Chapter( |_)(\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Fullmetal Alchemist chapters 101-108.cbz new Regex( @"^(?!vol)(?.*)( |_)(chapters( |_)?)\d+-?\d*", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Umineko no Naku Koro ni - Episode 1 - Legend of the Golden Witch #1 new Regex( @"^(?!Vol\.?)(?.*)( |_|-)(?.*)ch\d+-?\d?", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Magi - Ch.252-005.cbz new Regex( @"(?.*)( ?- ?)Ch\.\d+-?\d*", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // [BAA]_Darker_than_Black_Omake-1.zip new Regex( @"^(?!Vol)(?.*)(-)\d+-?\d*", // This catches a lot of stuff ^(?!Vol)(?.*)( |_)(\d+) - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Kodoja #001 (March 2016) new Regex( @"(?.*)(\s|_|-)#", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Baketeriya ch01-05.zip, Akiiro Bousou Biyori - 01.jpg, Beelzebub_172_RHS.zip, Cynthia the Mission 29.rar, A Compendium of Ghosts - 031 - The Third Story_ Part 12 (Digital) (Cobalt001) new Regex( @"^(?!Vol\.?)(?.+?)( |_|-)(?.*)( |_|-)(ch?)\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; @@ -267,67 +270,67 @@ namespace API.Parser // Invincible Vol 01 Family matters (2005) (Digital) new Regex( @"(?.*)(\b|_)(vol\.?)( |_)(?\d+(-\d+)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS) new Regex( @"^(?\d+) (- |_)?(?.*(\d{4})?)( |_)(\(|\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // 01 Spider-Man & Wolverine 01.cbr new Regex( @"^(?\d+) (?:- )?(?.*) (\d+)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Batman & Wildcat (1 of 3) new Regex( @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.*)(?: |_)v\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Amazing Man Comics chapter 25 new Regex( @"^(?.*)(?: |_)c(hapter) \d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Amazing Man Comics issue #25 new Regex( @"^(?.*)(?: |_)i(ssue) #\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Batman Wayne Family Adventures - Ep. 001 - Moving In new Regex( @"^(?.+?)(\s|_|-)?(?:Ep\.?)(\s|_|-)+\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.+?)(?: \d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Batman & Robin the Teen Wonder #0 new Regex( @"^(?.*)(?: |_)#\d+", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005) new Regex( @"^(?.*)(?: |_)(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // The First Asterix Frieze (WebP by Doc MaKS) new Regex( @"^(?.*)(?: |_)(?!\(\d{4}|\d{4}-\d{2}\))\(", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // MUST BE LAST: Batman & Daredevil - King of New York new Regex( @"^(?.*)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; @@ -336,40 +339,40 @@ namespace API.Parser // // 04 - Asterix the Gladiator (1964) (Digital-Empire) (WebP by Doc MaKS) // new Regex( // @"^(?\d+) (- |_)?(?.*(\d{4})?)( |_)(\(|\d+)", - // RegexOptions.IgnoreCase | RegexOptions.Compiled, + // MatchOptions, // RegexTimeout), // // 01 Spider-Man & Wolverine 01.cbr // new Regex( // @"^(?\d+) (?:- )?(?.*) (\d+)?", - // RegexOptions.IgnoreCase | RegexOptions.Compiled, + // MatchOptions, // RegexTimeout), // // Batman & Wildcat (1 of 3) // new Regex( // @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", - // RegexOptions.IgnoreCase | RegexOptions.Compiled, + // MatchOptions, // RegexTimeout), // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.*)(?: |_)v(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Scott Pilgrim 02 - Scott Pilgrim vs. The World (2005) // BUG: Negative lookbehind has to be fixed width // NOTE: The case this is built for does not make much sense. // new Regex( // @"^(?.+?)(?\d+)", - // RegexOptions.IgnoreCase | RegexOptions.Compiled, + // MatchOptions, // RegexTimeout), // Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) // new Regex( // @"^(?.+?)(?\d+))", - // RegexOptions.IgnoreCase | RegexOptions.Compiled, + // MatchOptions, // RegexTimeout), // // Batman & Robin the Teen Wonder #0 // new Regex( // @"^(?.*)(?: |_)#(?\d+)", - // RegexOptions.IgnoreCase | RegexOptions.Compiled, + // MatchOptions, // RegexTimeout), }; @@ -378,47 +381,47 @@ namespace API.Parser // Batman & Wildcat (1 of 3) new Regex( @"(?.*(\d{4})?)( |_)(?:\((?\d+) of \d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Batman Beyond 04 (of 6) (1999) new Regex( @"(?.+?)(?\d+)(\s|_|-)?\(of", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.+?)(?: |_)v(?\d+)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Invincible 070.5 - Invincible Returns 1 (2010) (digital) (Minutemen-InnerDemons).cbr new Regex( @"^(?.+?)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)(c? ?)-", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Batman & Catwoman - Trail of the Gun 01, Batman & Grendel (1996) 01 - Devil's Bones, Teen Titans v1 001 (1966-02) (digital) (OkC.O.M.P.U.T.O.-Novus) new Regex( @"^(?.+?)(?: (?\d+))", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Batman & Robin the Teen Wonder #0 new Regex( @"^(?.+?)(?:\s|_)#(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Saga 001 (2012) (Digital) (Empire-Zone) new Regex( @"(?.+?)(?: |_)(c? ?)(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)\s\(\d{4}", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Amazing Man Comics chapter 25 new Regex( @"^(?!Vol)(?.+?)( |_)c(hapter)( |_)(?\d*)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Amazing Man Comics issue #25 new Regex( @"^(?!Vol)(?.+?)( |_)i(ssue)( |_) #(?\d*)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; @@ -426,11 +429,11 @@ namespace API.Parser { // [TrinityBAKumA Finella&anon], [BAA]_, [SlowManga&OverloadScans], [batoto] new Regex(@"(?:\[(?(?!\s).+?(?(?!\s).+?(?(\d+(\.\d)?)-?(\d+(\.\d)?)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // [Suihei Kiki]_Kasumi_Otoko_no_Ko_[Taruby]_v1.1.zip new Regex( @"v\d+\.(?\d+(?:.\d+|-\d+)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Umineko no Naku Koro ni - Episode 3 - Banquet of the Golden Witch #02.cbz (Rare case, if causes issue remove) new Regex( @"^(?.*)(?: |_)#(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Green Worldz - Chapter 027, Kimi no Koto ga Daidaidaidaidaisuki na 100-nin no Kanojo Chapter 11-10 new Regex( @"^(?!Vol)(?.*)\s?(?\d+(?:\.?[\d-]+)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Hinowa ga CRUSH! 018 (2019) (Digital) (LuCaZ).cbz, Hinowa ga CRUSH! 018.5 (2019) (Digital) (LuCaZ).cbz new Regex( @"^(?!Vol)(?.+?)\s(?\d+(?:.\d+|-\d+)?)(?:\s\(\d{4}\))?(\b|_|-)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Tower Of God S01 014 (CBT) (digital).cbz new Regex( @"(?.*)\sS(?\d+)\s(?\d+(?:.\d+|-\d+)?)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Beelzebub_01_[Noodles].zip, Beelzebub_153b_RHS.zip new Regex( @"^((?!v|vo|vol|Volume).)*(\s|_)(?\.?\d+(?:.\d+|-\d+)?)(?b)?(\s|_|\[|\()", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Yumekui-Merry_DKThias_Chapter21.zip new Regex( @"Chapter(?\d+(-\d+)?)", //(?:.\d+|-\d+)? - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // [Hidoi]_Amaenaideyo_MS_vol01_chp02.rar new Regex( @"(?.*)(\s|_)(vol\d+)?(\s|_)Chp\.? ?(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Vol 1 Chapter 2 new Regex( @"(?((vol|volume|v))?(\s|_)?\.?\d+)(\s|_)(Chp|Chapter)\.?(\s|_)?(?\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; @@ -491,22 +494,22 @@ namespace API.Parser // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz new Regex( @"(?({|\(|\[).* Edition(}|\)|\]))", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Tenjo Tenge {Full Contact Edition} v01 (2011) (Digital) (ASTC).cbz new Regex( @"(\b|_)(?Omnibus(( |_)?Edition)?)(\b|_)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // To Love Ru v01 Uncensored (Ch.001-007) new Regex( @"(\b|_)(?Uncensored)(\b|_)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // AKIRA - c003 (v01) [Full Color] [Darkhorse].cbz new Regex( @"(\b|_)(?Full(?: |_)Color)(\b|_)?", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; @@ -515,17 +518,17 @@ namespace API.Parser // (), {}, [] new Regex( @"(?(\{\}|\[\]|\(\)))", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // (Complete) new Regex( @"(?(\{Complete\}|\[Complete\]|\(Complete\)))", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), // Anything in parenthesis new Regex( @"\(.*\)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; @@ -534,14 +537,14 @@ namespace API.Parser // All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle. new Regex( @"(?Specials?|OneShot|One\-Shot|Omake|Extra( Chapter)?|Art Collection|Side( |_)Stories|Bonus)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout), }; // If SP\d+ is in the filename, we force treat it as a special regardless if volume or chapter might have been found. private static readonly Regex SpecialMarkerRegex = new Regex( @"(?SP\d+)", - RegexOptions.IgnoreCase | RegexOptions.Compiled, + MatchOptions, RegexTimeout ); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 5be532867..87da418ad 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -199,6 +199,7 @@ namespace API.Services.Tasks await UpdateLibrary(library, series); + library.LastScanned = DateTime.Now; _unitOfWork.LibraryRepository.Update(library); if (await _unitOfWork.CommitAsync()) { @@ -296,6 +297,9 @@ namespace API.Services.Tasks { await _messageHub.Clients.All.SendAsync(SignalREvents.SeriesRemoved, MessageFactory.SeriesRemovedEvent(missing.Id, missing.Name, library.Id)); } + + await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress, + MessageFactory.ScanLibraryProgressEvent(library.Id, ((chunk + 1F) * chunkInfo.ChunkSize) / chunkInfo.TotalSize, string.Empty)); } diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 4485392b0..ebc1b3e83 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -59,7 +59,7 @@ namespace API.SignalR }; } - public static SignalRMessage ScanLibraryProgressEvent(int libraryId, int progress, string seriesName) + public static SignalRMessage ScanLibraryProgressEvent(int libraryId, float progress, string seriesName) { return new SignalRMessage() { @@ -97,5 +97,17 @@ namespace API.SignalR }; } + public static SignalRMessage SeriesAddedToCollection(int tagId, int seriesId) + { + return new SignalRMessage + { + Name = SignalREvents.UpdateVersion, + Body = new + { + TagId = tagId, + SeriesId = seriesId + } + }; + } } } diff --git a/API/SignalR/PresenceHub.cs b/API/SignalR/PresenceHub.cs deleted file mode 100644 index bb700e88a..000000000 --- a/API/SignalR/PresenceHub.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Extensions; -using API.SignalR.Presence; -using Microsoft.AspNetCore.SignalR; - -namespace API.SignalR -{ - /// - /// Keeps track of who is logged into the app - /// - public class PresenceHub : Hub - { - private readonly IPresenceTracker _tracker; - - public PresenceHub(IPresenceTracker tracker) - { - _tracker = tracker; - } - - public override async Task OnConnectedAsync() - { - await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId); - - var currentUsers = await PresenceTracker.GetOnlineUsers(); - await Clients.All.SendAsync("GetOnlineUsers", currentUsers); - - - } - - public override async Task OnDisconnectedAsync(Exception exception) - { - await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId); - - var currentUsers = await PresenceTracker.GetOnlineUsers(); - await Clients.All.SendAsync("GetOnlineUsers", currentUsers); - - await base.OnDisconnectedAsync(exception); - } - } -} diff --git a/API/SignalR/SignalREvents.cs b/API/SignalR/SignalREvents.cs index 0d4c124ca..d0ce5102e 100644 --- a/API/SignalR/SignalREvents.cs +++ b/API/SignalR/SignalREvents.cs @@ -10,5 +10,6 @@ public const string SeriesRemoved = "SeriesRemoved"; public const string ScanLibraryProgress = "ScanLibraryProgress"; public const string OnlineUsers = "OnlineUsers"; + public const string SeriesAddedToCollection = "SeriesAddedToCollection"; } } diff --git a/UI/Web/src/app/_models/events/series-added-to-collection-event.ts b/UI/Web/src/app/_models/events/series-added-to-collection-event.ts new file mode 100644 index 000000000..d07add15f --- /dev/null +++ b/UI/Web/src/app/_models/events/series-added-to-collection-event.ts @@ -0,0 +1,4 @@ +export interface SeriesAddedToCollectionEvent { + tagId: number; + seriesId: number; +} \ No newline at end of file diff --git a/UI/Web/src/app/_models/library.ts b/UI/Web/src/app/_models/library.ts index 5b9f8ec28..01a5727ae 100644 --- a/UI/Web/src/app/_models/library.ts +++ b/UI/Web/src/app/_models/library.ts @@ -7,7 +7,7 @@ export enum LibraryType { export interface Library { id: number; name: string; - coverImage: string; + lastScanned: string; type: LibraryType; folders: string[]; } \ No newline at end of file diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index d49f5076b..011d53279 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -17,7 +17,8 @@ export enum EVENTS { RefreshMetadata = 'RefreshMetadata', SeriesAdded = 'SeriesAdded', ScanLibraryProgress = 'ScanLibraryProgress', - OnlineUsers = 'OnlineUsers' + OnlineUsers = 'OnlineUsers', + SeriesAddedToCollection = 'SeriesAddedToCollection' } export interface Message { @@ -85,6 +86,13 @@ export class MessageHubService { this.scanLibrary.emit(resp.body); }); + this.hubConnection.on(EVENTS.SeriesAddedToCollection, resp => { + this.messagesSource.next({ + event: EVENTS.SeriesAddedToCollection, + payload: resp.body + }); + }); + this.hubConnection.on(EVENTS.SeriesAdded, resp => { this.messagesSource.next({ event: EVENTS.SeriesAdded, diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.html b/UI/Web/src/app/admin/manage-library/manage-library.component.html index 476e6b161..b8eb0b116 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.html +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.html @@ -20,6 +20,13 @@
Type: {{libraryType(library.type)}}
Shared Folders: {{library.folders.length + ' folders'}}
+
+ Last Scanned: + Never + + {{library.lastScanned | date: 'MM/dd/yyyy'}} + +
  • diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index 70f137f2d..d6bacfd8e 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -2,12 +2,12 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { Subject } from 'rxjs'; -import { take, takeUntil } from 'rxjs/operators'; +import { take, takeUntil, takeWhile } from 'rxjs/operators'; import { ConfirmService } from 'src/app/shared/confirm.service'; import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event'; import { Library, LibraryType } from 'src/app/_models/library'; import { LibraryService } from 'src/app/_services/library.service'; -import { MessageHubService } from 'src/app/_services/message-hub.service'; +import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { LibraryEditorModalComponent } from '../_modals/library-editor-modal/library-editor-modal.component'; @Component({ @@ -35,9 +35,10 @@ export class ManageLibraryComponent implements OnInit, OnDestroy { ngOnInit(): void { this.getLibraries(); - this.hubService.scanLibrary.subscribe((event: ScanLibraryProgressEvent) => { - - this.scanInProgress[event.libraryId] = event.progress !== 100; + // when a progress event comes in, show it on the UI next to library + this.hubService.messages$.pipe(takeWhile(event => event.event === EVENTS.ScanLibraryProgress)).subscribe((event) => { + const scanEvent = event.payload as ScanLibraryProgressEvent; + this.scanInProgress[scanEvent.libraryId] = scanEvent.progress !== 100; }); } diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts index 5f3fa3145..086df24a3 100644 --- a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -1,14 +1,16 @@ -import { Component, HostListener, OnInit } from '@angular/core'; +import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { Router, ActivatedRoute } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; -import { take } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { debounceTime, take, takeUntil, takeWhile } from 'rxjs/operators'; import { BulkSelectionService } from 'src/app/cards/bulk-selection.service'; import { UpdateFilterEvent } from 'src/app/cards/card-detail-layout/card-detail-layout.component'; import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { CollectionTag } from 'src/app/_models/collection-tag'; +import { SeriesAddedToCollectionEvent } from 'src/app/_models/events/series-added-to-collection-event'; import { Pagination } from 'src/app/_models/pagination'; import { Series } from 'src/app/_models/series'; import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter'; @@ -17,6 +19,7 @@ import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/acti import { ActionService } from 'src/app/_services/action.service'; import { CollectionTagService } from 'src/app/_services/collection-tag.service'; import { ImageService } from 'src/app/_services/image.service'; +import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { SeriesService } from 'src/app/_services/series.service'; @Component({ @@ -24,7 +27,7 @@ import { SeriesService } from 'src/app/_services/series.service'; templateUrl: './collection-detail.component.html', styleUrls: ['./collection-detail.component.scss'] }) -export class CollectionDetailComponent implements OnInit { +export class CollectionDetailComponent implements OnInit, OnDestroy { collectionTag!: CollectionTag; tagImage: string = ''; @@ -40,6 +43,8 @@ export class CollectionDetailComponent implements OnInit { mangaFormat: null }; + private onDestory: Subject = new Subject(); + bulkActionCallback = (action: Action, data: any) => { const selectedSeriesIndexies = this.bulkSelectionService.getSelectedCardsForSource('series'); const selectedSeries = this.series.filter((series, index: number) => selectedSeriesIndexies.includes(index + '')); @@ -68,7 +73,7 @@ export class CollectionDetailComponent implements OnInit { constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, private modalService: NgbModal, private titleService: Title, private accountService: AccountService, - public bulkSelectionService: BulkSelectionService, private actionService: ActionService) { + public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.accountService.currentUser$.pipe(take(1)).subscribe(user => { @@ -88,6 +93,18 @@ export class CollectionDetailComponent implements OnInit { ngOnInit(): void { this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this)); + + this.messageHub.messages$.pipe(takeWhile(event => event.event === EVENTS.SeriesAddedToCollection), takeUntil(this.onDestory), debounceTime(2000)).subscribe(event => { + const collectionEvent = event.payload as SeriesAddedToCollectionEvent; + if (collectionEvent.tagId === this.collectionTag.id) { + this.loadPage(); + } + }); + } + + ngOnDestroy() { + this.onDestory.next(); + this.onDestory.complete(); } @HostListener('document:keydown.shift', ['$event']) diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index 208bbd363..65e99ebc0 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -259,7 +259,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { break; case(Action.IncognitoRead): if (volume.chapters != undefined && volume.chapters?.length >= 1) { - this.openChapter(volume.chapters[0], true); + this.openChapter(volume.chapters.sort(this.utilityService.sortChapters)[0], true); } break; default: