diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj
index ec4a37c81..efe321347 100644
--- a/API.Tests/API.Tests.csproj
+++ b/API.Tests/API.Tests.csproj
@@ -10,7 +10,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/API.Tests/BasicTest.cs b/API.Tests/BasicTest.cs
new file mode 100644
index 000000000..c40b3706d
--- /dev/null
+++ b/API.Tests/BasicTest.cs
@@ -0,0 +1,118 @@
+using System.Collections.Generic;
+using System.Data.Common;
+using System.IO.Abstractions.TestingHelpers;
+using System.Linq;
+using System.Threading.Tasks;
+using API.Data;
+using API.Entities;
+using API.Entities.Enums;
+using API.Helpers;
+using API.Services;
+using AutoMapper;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+
+namespace API.Tests;
+
+public abstract class BasicTest
+{
+ private readonly DbConnection _connection;
+ protected readonly DataContext _context;
+ protected readonly IUnitOfWork _unitOfWork;
+
+
+ protected const string CacheDirectory = "C:/kavita/config/cache/";
+ protected const string CoverImageDirectory = "C:/kavita/config/covers/";
+ protected const string BackupDirectory = "C:/kavita/config/backups/";
+ protected const string LogDirectory = "C:/kavita/config/logs/";
+ protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
+ protected const string TempDirectory = "C:/kavita/config/temp/";
+
+ protected BasicTest()
+ {
+ var contextOptions = new DbContextOptionsBuilder()
+ .UseSqlite(CreateInMemoryDatabase())
+ .Options;
+ _connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
+
+ _context = new DataContext(contextOptions);
+ Task.Run(SeedDb).GetAwaiter().GetResult();
+
+ var config = new MapperConfiguration(cfg => cfg.AddProfile());
+ var mapper = config.CreateMapper();
+
+ _unitOfWork = new UnitOfWork(_context, mapper, null);
+ }
+
+ private static DbConnection CreateInMemoryDatabase()
+ {
+ var connection = new SqliteConnection("Filename=:memory:");
+
+ connection.Open();
+
+ return connection;
+ }
+
+ private async Task SeedDb()
+ {
+ await _context.Database.MigrateAsync();
+ var filesystem = CreateFileSystem();
+
+ await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem));
+
+ var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();
+ setting.Value = CacheDirectory;
+
+ setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync();
+ setting.Value = BackupDirectory;
+
+ setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
+ setting.Value = BookmarkDirectory;
+
+ setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync();
+ setting.Value = "10";
+
+ _context.ServerSetting.Update(setting);
+
+ _context.Library.Add(new Library()
+ {
+ Name = "Manga",
+ Folders = new List()
+ {
+ new FolderPath()
+ {
+ Path = "C:/data/"
+ }
+ }
+ });
+ return await _context.SaveChangesAsync() > 0;
+ }
+
+ protected async Task ResetDB()
+ {
+ _context.Series.RemoveRange(_context.Series.ToList());
+ _context.Users.RemoveRange(_context.Users.ToList());
+ _context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.ToList());
+
+ await _context.SaveChangesAsync();
+ }
+
+ protected static MockFileSystem CreateFileSystem()
+ {
+ var fileSystem = new MockFileSystem();
+ fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
+ fileSystem.AddDirectory("C:/kavita/config/");
+ fileSystem.AddDirectory(CacheDirectory);
+ fileSystem.AddDirectory(CoverImageDirectory);
+ fileSystem.AddDirectory(BackupDirectory);
+ fileSystem.AddDirectory(BookmarkDirectory);
+ fileSystem.AddDirectory(LogDirectory);
+ fileSystem.AddDirectory(TempDirectory);
+ fileSystem.AddDirectory("C:/data/");
+
+ return fileSystem;
+ }
+}
diff --git a/API.Tests/Helpers/CacheHelperTests.cs b/API.Tests/Helpers/CacheHelperTests.cs
index 723742bc6..d78ed1601 100644
--- a/API.Tests/Helpers/CacheHelperTests.cs
+++ b/API.Tests/Helpers/CacheHelperTests.cs
@@ -165,7 +165,7 @@ public class CacheHelperTests
FilePath = TestCoverArchive,
LastModified = filesystemFile.LastWriteTime.DateTime
};
- Assert.True(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
+ Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
}
[Fact]
@@ -195,7 +195,7 @@ public class CacheHelperTests
FilePath = TestCoverArchive,
LastModified = filesystemFile.LastWriteTime.DateTime
};
- Assert.True(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
+ Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
}
[Fact]
@@ -225,15 +225,16 @@ public class CacheHelperTests
FilePath = TestCoverArchive,
LastModified = filesystemFile.LastWriteTime.DateTime
};
- Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, true, file));
+ Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, true, file));
}
[Fact]
- public void HasFileNotChangedSinceCreationOrLastScan_ModifiedSinceLastScan()
+ public void IsFileUnmodifiedSinceCreationOrLastScan_ModifiedSinceLastScan()
{
var filesystemFile = new MockFileData("")
{
- LastWriteTime = DateTimeOffset.Now
+ LastWriteTime = DateTimeOffset.Now,
+ CreationTime = DateTimeOffset.Now
};
var fileSystem = new MockFileSystem(new Dictionary
{
@@ -246,8 +247,8 @@ public class CacheHelperTests
var chapter = new Chapter()
{
- Created = filesystemFile.LastWriteTime.DateTime.Subtract(TimeSpan.FromMinutes(10)),
- LastModified = filesystemFile.LastWriteTime.DateTime.Subtract(TimeSpan.FromMinutes(10))
+ Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)),
+ LastModified = DateTime.Now.Subtract(TimeSpan.FromMinutes(10))
};
var file = new MangaFile()
@@ -255,7 +256,7 @@ public class CacheHelperTests
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
LastModified = filesystemFile.LastWriteTime.DateTime
};
- Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
+ Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
}
[Fact]
@@ -276,8 +277,8 @@ public class CacheHelperTests
var chapter = new Chapter()
{
- Created = filesystemFile.LastWriteTime.DateTime.Subtract(TimeSpan.FromMinutes(10)),
- LastModified = filesystemFile.LastWriteTime.DateTime
+ Created = DateTime.Now.Subtract(TimeSpan.FromMinutes(10)),
+ LastModified = DateTime.Now
};
var file = new MangaFile()
@@ -285,7 +286,7 @@ public class CacheHelperTests
FilePath = Path.Join(TestCoverImageDirectory, TestCoverArchive),
LastModified = filesystemFile.LastWriteTime.DateTime
};
- Assert.False(cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, false, file));
+ Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
}
}
diff --git a/API.Tests/Services/DeviceServiceTests.cs b/API.Tests/Services/DeviceServiceTests.cs
new file mode 100644
index 000000000..a03f1e715
--- /dev/null
+++ b/API.Tests/Services/DeviceServiceTests.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using API.DTOs.Device;
+using API.Entities;
+using API.Entities.Enums.Device;
+using API.Services;
+using API.Services.Tasks;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+using Xunit;
+
+namespace API.Tests.Services;
+
+public class DeviceServiceTests : BasicTest
+{
+ private readonly ILogger _logger = Substitute.For>();
+ private readonly IDeviceService _deviceService;
+
+ public DeviceServiceTests() : base()
+ {
+ _deviceService = new DeviceService(_unitOfWork, _logger, Substitute.For());
+ }
+
+ protected void ResetDB()
+ {
+ _context.Users.RemoveRange(_context.Users.ToList());
+ }
+
+
+
+ [Fact]
+ public async Task CreateDevice_Succeeds()
+ {
+
+ var user = new AppUser()
+ {
+ UserName = "majora2007",
+ Devices = new List()
+ };
+
+ _context.Users.Add(user);
+ await _unitOfWork.CommitAsync();
+
+ var device = await _deviceService.Create(new CreateDeviceDto()
+ {
+ EmailAddress = "fake@kindle.com",
+ Name = "Test Kindle",
+ Platform = DevicePlatform.Kindle
+ }, user);
+
+ Assert.NotNull(device);
+
+ }
+
+ [Fact]
+ public async Task CreateDevice_ThrowsErrorWhenEmailDoesntMatchRules()
+ {
+
+ var user = new AppUser()
+ {
+ UserName = "majora2007",
+ Devices = new List()
+ };
+
+ _context.Users.Add(user);
+ await _unitOfWork.CommitAsync();
+
+ var device = await _deviceService.Create(new CreateDeviceDto()
+ {
+ EmailAddress = "fake@gmail.com",
+ Name = "Test Kindle",
+ Platform = DevicePlatform.Kindle
+ }, user);
+
+ Assert.NotNull(device);
+
+ }
+}
diff --git a/API/Controllers/DeviceController.cs b/API/Controllers/DeviceController.cs
new file mode 100644
index 000000000..ff96ea2b2
--- /dev/null
+++ b/API/Controllers/DeviceController.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using API.Data;
+using API.Data.Repositories;
+using API.DTOs.Device;
+using API.Extensions;
+using API.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace API.Controllers;
+
+///
+/// Responsible interacting and creating Devices
+///
+public class DeviceController : BaseApiController
+{
+ private readonly IUnitOfWork _unitOfWork;
+ private readonly IDeviceService _deviceService;
+ private readonly IEmailService _emailService;
+
+ public DeviceController(IUnitOfWork unitOfWork, IDeviceService deviceService, IEmailService emailService)
+ {
+ _unitOfWork = unitOfWork;
+ _deviceService = deviceService;
+ _emailService = emailService;
+ }
+
+
+ [HttpPost("create")]
+ public async Task CreateOrUpdateDevice(CreateDeviceDto dto)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
+ var device = await _deviceService.Create(dto, user);
+
+ if (device == null) return BadRequest("There was an error when creating the device");
+
+ return Ok();
+ }
+
+ [HttpPost("update")]
+ public async Task UpdateDevice(UpdateDeviceDto dto)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
+ var device = await _deviceService.Update(dto, user);
+
+ if (device == null) return BadRequest("There was an error when updating the device");
+
+ return Ok();
+ }
+
+ ///
+ /// Deletes the device from the user
+ ///
+ ///
+ ///
+ [HttpDelete]
+ public async Task DeleteDevice(int deviceId)
+ {
+ if (deviceId <= 0) return BadRequest("Not a valid deviceId");
+ var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Devices);
+ if (await _deviceService.Delete(user, deviceId)) return Ok();
+
+ return BadRequest("Could not delete device");
+ }
+
+ [HttpGet]
+ public async Task>> GetDevices()
+ {
+ var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
+ return Ok(await _unitOfWork.DeviceRepository.GetDevicesForUserAsync(userId));
+ }
+
+ [HttpPost("send-to")]
+ public async Task SendToDevice(SendToDeviceDto dto)
+ {
+ if (dto.ChapterId < 0) return BadRequest("ChapterId must be greater than 0");
+ if (dto.DeviceId < 0) return BadRequest("DeviceId must be greater than 0");
+
+ if (await _emailService.IsDefaultEmailService())
+ return BadRequest("Send to device cannot be used with Kavita's email service. Please configure your own.");
+
+ if (await _deviceService.SendTo(dto.ChapterId, dto.DeviceId)) return Ok();
+
+ return BadRequest("There was an error sending the file to the device");
+ }
+
+
+}
+
+
diff --git a/API/Controllers/FallbackController.cs b/API/Controllers/FallbackController.cs
index a765269b8..2f5d7fceb 100644
--- a/API/Controllers/FallbackController.cs
+++ b/API/Controllers/FallbackController.cs
@@ -1,7 +1,9 @@
-using System.IO;
+using System;
+using System.IO;
using API.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
namespace API.Controllers;
diff --git a/API/DTOs/Device/CreateDeviceDto.cs b/API/DTOs/Device/CreateDeviceDto.cs
new file mode 100644
index 000000000..bdcdde194
--- /dev/null
+++ b/API/DTOs/Device/CreateDeviceDto.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.InteropServices;
+using API.Entities.Enums.Device;
+
+namespace API.DTOs.Device;
+
+public class CreateDeviceDto
+{
+ [Required]
+ public string Name { get; set; }
+ ///
+ /// Platform of the device. If not know, defaults to "Custom"
+ ///
+ [Required]
+ public DevicePlatform Platform { get; set; }
+ [Required]
+ public string EmailAddress { get; set; }
+
+
+}
diff --git a/API/DTOs/Device/DeviceDto.cs b/API/DTOs/Device/DeviceDto.cs
new file mode 100644
index 000000000..e5344f31e
--- /dev/null
+++ b/API/DTOs/Device/DeviceDto.cs
@@ -0,0 +1,33 @@
+using System;
+using API.Entities.Enums.Device;
+
+namespace API.DTOs.Device;
+
+///
+/// A Device is an entity that can receive data from Kavita (kindle)
+///
+public class DeviceDto
+{
+ ///
+ /// The device Id
+ ///
+ public int Id { get; set; }
+ ///
+ /// A name given to this device
+ ///
+ /// If this device is web, this will be the browser name
+ /// Pixel 3a, John's Kindle
+ public string Name { get; set; }
+ ///
+ /// An email address associated with the device (ie Kindle). Will be used with Send to functionality
+ ///
+ public string EmailAddress { get; set; }
+ ///
+ /// Platform (ie) Windows 10
+ ///
+ public DevicePlatform Platform { get; set; }
+ ///
+ /// Last time this device was used to send a file
+ ///
+ public DateTime LastUsed { get; set; }
+}
diff --git a/API/DTOs/Device/SendToDeviceDto.cs b/API/DTOs/Device/SendToDeviceDto.cs
new file mode 100644
index 000000000..20d8cf311
--- /dev/null
+++ b/API/DTOs/Device/SendToDeviceDto.cs
@@ -0,0 +1,7 @@
+namespace API.DTOs.Device;
+
+public class SendToDeviceDto
+{
+ public int DeviceId { get; set; }
+ public int ChapterId { get; set; }
+}
diff --git a/API/DTOs/Device/UpdateDeviceDto.cs b/API/DTOs/Device/UpdateDeviceDto.cs
new file mode 100644
index 000000000..201adcb5d
--- /dev/null
+++ b/API/DTOs/Device/UpdateDeviceDto.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations;
+using API.Entities.Enums.Device;
+
+namespace API.DTOs.Device;
+
+public class UpdateDeviceDto
+{
+ [Required]
+ public int Id { get; set; }
+ [Required]
+ public string Name { get; set; }
+ ///
+ /// Platform of the device. If not know, defaults to "Custom"
+ ///
+ [Required]
+ public DevicePlatform Platform { get; set; }
+ [Required]
+ public string EmailAddress { get; set; }
+}
diff --git a/API/DTOs/Email/SendToDto.cs b/API/DTOs/Email/SendToDto.cs
new file mode 100644
index 000000000..254f7fd09
--- /dev/null
+++ b/API/DTOs/Email/SendToDto.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace API.DTOs.Email;
+
+public class SendToDto
+{
+ public string DestinationEmail { get; set; }
+ public IEnumerable FilePaths { get; set; }
+}
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index 7b1ce8d36..c00289227 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -44,6 +44,7 @@ public sealed class DataContext : IdentityDbContext SiteTheme { get; set; }
public DbSet SeriesRelation { get; set; }
public DbSet FolderPath { get; set; }
+ public DbSet Device { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
diff --git a/API/Data/DbFactory.cs b/API/Data/DbFactory.cs
index 8151030dd..891c10843 100644
--- a/API/Data/DbFactory.cs
+++ b/API/Data/DbFactory.cs
@@ -162,7 +162,15 @@ public static class DbFactory
FilePath = filePath,
Format = format,
Pages = pages,
- LastModified = File.GetLastWriteTime(filePath) // NOTE: Changed this from DateTime.Now
+ LastModified = File.GetLastWriteTime(filePath)
+ };
+ }
+
+ public static Device Device(string name)
+ {
+ return new Device()
+ {
+ Name = name,
};
}
diff --git a/API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs b/API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs
new file mode 100644
index 000000000..dbf4a0af6
--- /dev/null
+++ b/API/Data/Migrations/20220921023455_DeviceSupport.Designer.cs
@@ -0,0 +1,1658 @@
+//
+using System;
+using API.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace API.Data.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20220921023455_DeviceSupport")]
+ partial class DeviceSupport
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
+
+ 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", (string)null);
+ });
+
+ 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("ConfirmationToken")
+ .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", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserBookmark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ 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("BackgroundColor")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("#000000");
+
+ b.Property("BlurUnreadSummaries")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderFontFamily")
+ .HasColumnType("TEXT");
+
+ b.Property("BookReaderFontSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderImmersiveMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderLineSpacing")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderMargin")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookReaderTapToPaginate")
+ .HasColumnType("INTEGER");
+
+ b.Property("BookThemeName")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("Dark");
+
+ b.Property("GlobalPageLayoutMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageSplitOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("PromptForDownloadSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReaderMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReadingDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScalingOption")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowScreenHints")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThemeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId")
+ .IsUnique();
+
+ b.HasIndex("ThemeId");
+
+ 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.HasIndex("SeriesId");
+
+ 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.HasIndex("SeriesId");
+
+ 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", (string)null);
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("MaxHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("Range")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("TEXT");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("TitleName")
+ .HasColumnType("TEXT");
+
+ b.Property("TotalCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("WordCount")
+ .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.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("EmailAddress")
+ .HasColumnType("TEXT");
+
+ b.Property("IpAddress")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastUsed")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Platform")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("Device");
+ });
+
+ 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.Genre", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalTag")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle", "ExternalTag")
+ .IsUnique();
+
+ b.ToTable("Genre");
+ });
+
+ 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("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("FilePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastFileAnalysis")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.ToTable("MangaFile");
+ });
+
+ modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRatingLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("CharacterLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ColoristLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverArtistLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("EditorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("GenresLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("InkerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LanguageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("LettererLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("PencillerLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublicationStatus")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublicationStatusLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("PublisherLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ReleaseYear")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Summary")
+ .HasColumnType("TEXT");
+
+ b.Property("SummaryLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TagsLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("TotalCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TranslatorLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("WriterLocked")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId")
+ .IsUnique();
+
+ b.HasIndex("Id", "SeriesId")
+ .IsUnique();
+
+ b.ToTable("SeriesMetadata");
+ });
+
+ modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("RelationKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TargetSeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SeriesId");
+
+ b.HasIndex("TargetSeriesId");
+
+ b.ToTable("SeriesRelation");
+ });
+
+ modelBuilder.Entity("API.Entities.Person", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Role")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Person");
+ });
+
+ modelBuilder.Entity("API.Entities.ReadingList", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedTitle")
+ .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("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("CoverImageLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("FolderPath")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastChapterAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("LastFolderScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LocalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("LocalizedNameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedLocalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginalName")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortName")
+ .HasColumnType("TEXT");
+
+ b.Property("SortNameLocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("WordCount")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("Series");
+ });
+
+ 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.SiteTheme", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("IsDefault")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.Property("Provider")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("SiteTheme");
+ });
+
+ modelBuilder.Entity("API.Entities.Tag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ExternalTag")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle", "ExternalTag")
+ .IsUnique();
+
+ b.ToTable("Tag");
+ });
+
+ modelBuilder.Entity("API.Entities.Volume", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AvgHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("MaxHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("MinHoursToRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("INTEGER");
+
+ b.Property