diff --git a/API.Tests/Helpers/SmartFilterHelperTests.cs b/API.Tests/Helpers/SmartFilterHelperTests.cs
new file mode 100644
index 000000000..3d9fbc3ca
--- /dev/null
+++ b/API.Tests/Helpers/SmartFilterHelperTests.cs
@@ -0,0 +1,40 @@
+using System.Linq;
+using API.DTOs.Filtering;
+using API.DTOs.Filtering.v2;
+using API.Entities.Enums;
+using API.Helpers;
+using Xunit;
+
+namespace API.Tests.Helpers;
+
+public class SmartFilterHelperTests
+{
+ [Fact]
+ public void Test_Decode()
+ {
+ var encoded = """
+ stmts=comparison%3D5%26field%3D18%26value%3D6%2Ccomparison%3D0%26field%3D4%26value%3D0%2Ccomparison%3D7%26field%3D1%26value%3Da&sortOptions=sortField=1&isAscending=true&limitTo=0&combination=1
+ """;
+
+ var filter = SmartFilterHelper.Decode(encoded);
+
+ Assert.Equal(0, filter.LimitTo);
+ Assert.Equal(SortField.SortName, filter.SortOptions.SortField);
+ Assert.True(filter.SortOptions.IsAscending);
+ Assert.Null(filter.Name);
+
+ var list = filter.Statements.ToList();
+ AssertStatementSame(list[2], FilterField.SeriesName, FilterComparison.Matches, "a");
+ AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + "");
+ AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Contains, "6");
+
+ }
+
+ private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value)
+ {
+ Assert.Equal(statement.Field, field);
+ Assert.Equal(statement.Comparison, combination);
+ Assert.Equal(statement.Value, value);
+ }
+
+}
diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs
index 0ff5882d5..e1742a519 100644
--- a/API/Controllers/AccountController.cs
+++ b/API/Controllers/AccountController.cs
@@ -8,6 +8,7 @@ using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.Account;
+using API.DTOs.Dashboard;
using API.DTOs.Email;
using API.Entities;
using API.Entities.Enums;
@@ -1035,4 +1036,123 @@ public class AccountController : BaseApiController
return Ok(origin + "/" + baseUrl + "api/opds/" + user!.ApiKey);
}
+
+ ///
+ /// Returns the layout of the user's dashboard
+ ///
+ ///
+ [HttpGet("dashboard")]
+ public async Task>> GetDashboardLayout(bool visibleOnly = true)
+ {
+ var streams = await _unitOfWork.UserRepository.GetDashboardStreams(User.GetUserId(), visibleOnly);
+ return Ok(streams);
+ }
+
+ ///
+ /// Creates a Dashboard Stream from a SmartFilter and adds it to the user's dashboard as visible
+ ///
+ ///
+ ///
+ [HttpPost("add-dashboard-stream")]
+ public async Task> AddDashboard([FromQuery] int smartFilterId)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.DashboardStreams);
+ if (user == null) return Unauthorized();
+
+ var smartFilter = await _unitOfWork.AppUserSmartFilterRepository.GetById(smartFilterId);
+ if (smartFilter == null) return NoContent();
+
+ var stream = user?.DashboardStreams.FirstOrDefault(d => d.SmartFilter?.Id == smartFilterId);
+ if (stream != null) return BadRequest("There is an existing stream with this Filter");
+
+ var maxOrder = user!.DashboardStreams.Max(d => d.Order);
+ var createdStream = new AppUserDashboardStream()
+ {
+ Name = smartFilter.Name,
+ IsProvided = false,
+ StreamType = DashboardStreamType.SmartFilter,
+ Visible = true,
+ Order = maxOrder + 1,
+ SmartFilter = smartFilter
+ };
+
+ user.DashboardStreams.Add(createdStream);
+ _unitOfWork.UserRepository.Update(user);
+ await _unitOfWork.CommitAsync();
+
+ var ret = new DashboardStreamDto()
+ {
+ Name = createdStream.Name,
+ IsProvided = createdStream.IsProvided,
+ Visible = createdStream.Visible,
+ Order = createdStream.Order,
+ SmartFilterEncoded = smartFilter.Filter,
+ StreamType = createdStream.StreamType
+ };
+
+
+ await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id),
+ User.GetUserId());
+ return Ok(ret);
+ }
+
+ ///
+ /// Updates the visibility of a dashboard stream
+ ///
+ ///
+ ///
+ [HttpPost("update-dashboard-stream")]
+ public async Task UpdateDashboardStream(DashboardStreamDto dto)
+ {
+ var stream = await _unitOfWork.UserRepository.GetDashboardStream(dto.Id);
+ if (stream == null) return BadRequest();
+ stream.Visible = dto.Visible;
+
+ _unitOfWork.UserRepository.Update(stream);
+ await _unitOfWork.CommitAsync();
+ var userId = User.GetUserId();
+ await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(userId),
+ userId);
+ return Ok();
+ }
+
+ ///
+ /// Updates the position of a dashboard stream
+ ///
+ ///
+ ///
+ [HttpPost("update-dashboard-position")]
+ public async Task UpdateDashboardStreamPosition(UpdateDashboardStreamPositionDto dto)
+ {
+ var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(),
+ AppUserIncludes.DashboardStreams);
+ var stream = user?.DashboardStreams.FirstOrDefault(d => d.Id == dto.DashboardStreamId);
+ if (stream == null) return BadRequest();
+ if (stream.Order == dto.ToPosition) return Ok();
+
+ var list = user!.DashboardStreams.ToList();
+ ReorderItems(list, stream.Id, dto.ToPosition);
+ user.DashboardStreams = list;
+
+ _unitOfWork.UserRepository.Update(user);
+ await _unitOfWork.CommitAsync();
+ await _eventHub.SendMessageToAsync(MessageFactory.DashboardUpdate, MessageFactory.DashboardUpdateEvent(user.Id),
+ user.Id);
+ return Ok();
+ }
+
+ private static void ReorderItems(List items, int itemId, int toPosition)
+ {
+ var item = items.Find(r => r.Id == itemId);
+ if (item != null)
+ {
+ items.Remove(item);
+ items.Insert(toPosition, item);
+ }
+
+ for (var i = 0; i < items.Count; i++)
+ {
+ items[i].Order = i;
+ }
+ }
}
diff --git a/API/Controllers/FilterController.cs b/API/Controllers/FilterController.cs
index 7b6e41ef8..6a2d06ee5 100644
--- a/API/Controllers/FilterController.cs
+++ b/API/Controllers/FilterController.cs
@@ -1,8 +1,15 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
+using API.Data.Repositories;
+using API.DTOs.Dashboard;
using API.DTOs.Filtering.v2;
+using API.Entities;
+using API.Extensions;
+using API.Helpers;
using EasyCaching.Core;
using Microsoft.AspNetCore.Mvc;
@@ -22,38 +29,66 @@ public class FilterController : BaseApiController
_cacheFactory = cacheFactory;
}
- [HttpGet]
- public async Task> GetFilter(string name)
+ ///
+ /// Creates or Updates the filter
+ ///
+ ///
+ ///
+ [HttpPost("update")]
+ public async Task CreateOrUpdateSmartFilter(FilterV2Dto dto)
{
- var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Filter);
- if (string.IsNullOrEmpty(name)) return Ok(null);
- var filter = await provider.GetAsync(name);
- if (filter.HasValue)
+ var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.SmartFilters);
+ if (user == null) return Unauthorized();
+
+ if (string.IsNullOrWhiteSpace(dto.Name)) return BadRequest("Name must be set");
+ if (Seed.DefaultStreams.Any(s => s.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase)))
{
- filter.Value.Name = name;
- return Ok(filter.Value);
+ return BadRequest("You cannot use the name of a system provided stream");
}
- return Ok(null);
+ // I might just want to use DashboardStream instead of a separate entity. It will drastically simplify implementation
+
+ var existingFilter =
+ user.SmartFilters.FirstOrDefault(f => f.Name.Equals(dto.Name, StringComparison.InvariantCultureIgnoreCase));
+ if (existingFilter != null)
+ {
+ // Update the filter
+ existingFilter.Filter = SmartFilterHelper.Encode(dto);
+ _unitOfWork.AppUserSmartFilterRepository.Update(existingFilter);
+ }
+ else
+ {
+ existingFilter = new AppUserSmartFilter()
+ {
+ Name = dto.Name,
+ Filter = SmartFilterHelper.Encode(dto)
+ };
+ user.SmartFilters.Add(existingFilter);
+ _unitOfWork.UserRepository.Update(user);
+ }
+
+ if (!_unitOfWork.HasChanges()) return Ok();
+ await _unitOfWork.CommitAsync();
+
+ return Ok();
}
- ///
- /// Caches the filter in the backend and returns a temp string for retrieving.
- ///
- /// The cache line lives for only 1 hour
- ///
- ///
- [HttpPost("create-temp")]
- public async Task> CreateTempFilter(FilterV2Dto filterDto)
+ [HttpGet]
+ public ActionResult> GetFilters()
{
- var provider = _cacheFactory.GetCachingProvider(EasyCacheProfiles.Filter);
- var name = filterDto.Name;
- if (string.IsNullOrEmpty(filterDto.Name))
- {
- name = Guid.NewGuid().ToString();
- }
+ return Ok(_unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(User.GetUserId()));
+ }
- await provider.SetAsync(name, filterDto, TimeSpan.FromHours(1));
- return name;
+ [HttpDelete]
+ public async Task DeleteFilter(int filterId)
+ {
+ var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId);
+ if (filter == null) return Ok();
+ // This needs to delete any dashboard filters that have it too
+ var streams = await _unitOfWork.UserRepository.GetDashboardStreamWithFilter(filter.Id);
+ _unitOfWork.UserRepository.Delete(streams);
+ _unitOfWork.AppUserSmartFilterRepository.Delete(filter);
+ await _unitOfWork.CommitAsync();
+ return Ok();
}
}
diff --git a/API/Controllers/LocaleController.cs b/API/Controllers/LocaleController.cs
index dde8b0d03..de1c0d16c 100644
--- a/API/Controllers/LocaleController.cs
+++ b/API/Controllers/LocaleController.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using API.DTOs.Filtering;
@@ -19,11 +20,26 @@ public class LocaleController : BaseApiController
[HttpGet]
public ActionResult> GetAllLocales()
{
- var languages = _localizationService.GetLocales().Select(c => new CultureInfo(c)).Select(c =>
- new LanguageDto()
+ var languages = _localizationService.GetLocales().Select(c =>
{
- Title = c.DisplayName,
- IsoCode = c.IetfLanguageTag
+ try
+ {
+ var cult = new CultureInfo(c);
+ return new LanguageDto()
+ {
+ Title = cult.DisplayName,
+ IsoCode = cult.IetfLanguageTag
+ };
+ }
+ catch (Exception ex)
+ {
+ // Some OS' don't have all culture codes supported like PT_BR, thus we need to default
+ return new LanguageDto()
+ {
+ Title = c,
+ IsoCode = c
+ };
+ }
})
.Where(l => !string.IsNullOrEmpty(l.IsoCode))
.OrderBy(d => d.Title);
diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs
index 79380a4ea..d8fd48efd 100644
--- a/API/Controllers/OPDSController.cs
+++ b/API/Controllers/OPDSController.cs
@@ -102,32 +102,68 @@ public class OpdsController : BaseApiController
var feed = CreateFeed("Kavita", string.Empty, apiKey, prefix);
SetFeedId(feed, "root");
- feed.Entries.Add(new FeedEntry()
+
+ // Get the user's customized dashboard
+ var streams = await _unitOfWork.UserRepository.GetDashboardStreams(userId, true);
+ foreach (var stream in streams)
{
- Id = "onDeck",
- Title = await _localizationService.Translate(userId, "on-deck"),
- Content = new FeedEntryContent()
+ switch (stream.StreamType)
{
- Text = await _localizationService.Translate(userId, "browse-on-deck")
- },
- Links = new List()
- {
- CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/on-deck"),
+ case DashboardStreamType.OnDeck:
+ feed.Entries.Add(new FeedEntry()
+ {
+ Id = "onDeck",
+ Title = await _localizationService.Translate(userId, "on-deck"),
+ Content = new FeedEntryContent()
+ {
+ Text = await _localizationService.Translate(userId, "browse-on-deck")
+ },
+ Links = new List()
+ {
+ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/on-deck"),
+ }
+ });
+ break;
+ case DashboardStreamType.NewlyAdded:
+ feed.Entries.Add(new FeedEntry()
+ {
+ Id = "recentlyAdded",
+ Title = await _localizationService.Translate(userId, "recently-added"),
+ Content = new FeedEntryContent()
+ {
+ Text = await _localizationService.Translate(userId, "browse-recently-added")
+ },
+ Links = new List()
+ {
+ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-added"),
+ }
+ });
+ break;
+ case DashboardStreamType.RecentlyUpdated:
+ // TODO: See if we can implement this and use (count) on series name for number of updates
+ break;
+ case DashboardStreamType.MoreInGenre:
+ // TODO: See if we can implement this
+ break;
+ case DashboardStreamType.SmartFilter:
+
+ feed.Entries.Add(new FeedEntry()
+ {
+ Id = "smartFilter-" + stream.Id,
+ Title = stream.Name,
+ Content = new FeedEntryContent()
+ {
+ Text = stream.Name
+ },
+ Links = new List()
+ {
+ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filter/{stream.SmartFilterId}/"),
+ }
+ });
+ break;
}
- });
- feed.Entries.Add(new FeedEntry()
- {
- Id = "recentlyAdded",
- Title = await _localizationService.Translate(userId, "recently-added"),
- Content = new FeedEntryContent()
- {
- Text = await _localizationService.Translate(userId, "browse-recently-added")
- },
- Links = new List()
- {
- CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/recently-added"),
- }
- });
+ }
+
feed.Entries.Add(new FeedEntry()
{
Id = "readingList",
@@ -180,6 +216,19 @@ public class OpdsController : BaseApiController
CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/collections"),
}
});
+ feed.Entries.Add(new FeedEntry()
+ {
+ Id = "allSmartFilters",
+ Title = await _localizationService.Translate(userId, "smart-filters"),
+ Content = new FeedEntryContent()
+ {
+ Text = await _localizationService.Translate(userId, "browse-smart-filters")
+ },
+ Links = new List()
+ {
+ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filters"),
+ }
+ });
return CreateXmlResult(SerializeXml(feed));
}
@@ -196,6 +245,67 @@ public class OpdsController : BaseApiController
return new Tuple(baseUrl, prefix);
}
+ ///
+ /// Returns the Series matching this smart filter. If FromDashboard, will only return 20 records.
+ ///
+ ///
+ [HttpGet("{apiKey}/smart-filter/{filterId}")]
+ [Produces("application/xml")]
+ public async Task GetSmartFilter(string apiKey, int filterId)
+ {
+ var userId = await GetUser(apiKey);
+ if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
+ return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
+ var (baseUrl, prefix) = await GetPrefix();
+
+
+ var filter = await _unitOfWork.AppUserSmartFilterRepository.GetById(filterId);
+ if (filter == null) return BadRequest(_localizationService.Translate(userId, "smart-filter-doesnt-exist"));
+ var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilter-" + filter.Id), $"{prefix}{apiKey}/smart-filter/{filter.Id}/", apiKey, prefix);
+ SetFeedId(feed, "smartFilter-" + filter.Id);
+
+ var decodedFilter = SmartFilterHelper.Decode(filter.Filter);
+ var series = await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdV2Async(userId, UserParams.Default,
+ decodedFilter);
+ var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(series.Select(s => s.Id));
+
+ foreach (var seriesDto in series)
+ {
+ feed.Entries.Add(CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl));
+ }
+
+ AddPagination(feed, series, $"{prefix}{apiKey}/smart-filter/{filterId}/");
+ return CreateXmlResult(SerializeXml(feed));
+ }
+
+ [HttpGet("{apiKey}/smart-filters")]
+ [Produces("application/xml")]
+ public async Task GetSmartFilters(string apiKey)
+ {
+ var userId = await GetUser(apiKey);
+ if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
+ return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
+ var (baseUrl, prefix) = await GetPrefix();
+
+ var filters = _unitOfWork.AppUserSmartFilterRepository.GetAllDtosByUserId(userId);
+ var feed = CreateFeed(await _localizationService.Translate(userId, "smartFilters"), $"{prefix}{apiKey}/smart-filters", apiKey, prefix);
+ SetFeedId(feed, "smartFilters");
+ foreach (var filter in filters)
+ {
+ feed.Entries.Add(new FeedEntry()
+ {
+ Id = filter.Id.ToString(),
+ Title = filter.Name,
+ Links = new List()
+ {
+ CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/smart-filter/{filter.Id}")
+ }
+ });
+ }
+
+ return CreateXmlResult(SerializeXml(feed));
+ }
+
[HttpGet("{apiKey}/libraries")]
[Produces("application/xml")]
diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs
index a86d9626a..97a340ecf 100644
--- a/API/Controllers/SeriesController.cs
+++ b/API/Controllers/SeriesController.cs
@@ -6,6 +6,7 @@ using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
+using API.DTOs.Dashboard;
using API.DTOs.Filtering;
using API.DTOs.Filtering.v2;
using API.DTOs.Metadata;
diff --git a/API/DTOs/Dashboard/DashboardStreamDto.cs b/API/DTOs/Dashboard/DashboardStreamDto.cs
new file mode 100644
index 000000000..59e5f4f7d
--- /dev/null
+++ b/API/DTOs/Dashboard/DashboardStreamDto.cs
@@ -0,0 +1,30 @@
+using API.DTOs.Filtering.v2;
+using API.Entities;
+using API.Entities.Enums;
+
+namespace API.DTOs.Dashboard;
+
+public class DashboardStreamDto
+{
+ public int Id { get; set; }
+ public required string Name { get; set; }
+ ///
+ /// Is System Provided
+ ///
+ public bool IsProvided { get; set; }
+ ///
+ /// Sort Order on the Dashboard
+ ///
+ public int Order { get; set; }
+ ///
+ /// If Not IsProvided, the appropriate smart filter
+ ///
+ /// Encoded filter
+ public string? SmartFilterEncoded { get; set; }
+ public int? SmartFilterId { get; set; }
+ ///
+ /// For system provided
+ ///
+ public DashboardStreamType StreamType { get; set; }
+ public bool Visible { get; set; }
+}
diff --git a/API/DTOs/GroupedSeriesDto.cs b/API/DTOs/Dashboard/GroupedSeriesDto.cs
similarity index 97%
rename from API/DTOs/GroupedSeriesDto.cs
rename to API/DTOs/Dashboard/GroupedSeriesDto.cs
index 697ae3a53..3b283de34 100644
--- a/API/DTOs/GroupedSeriesDto.cs
+++ b/API/DTOs/Dashboard/GroupedSeriesDto.cs
@@ -1,7 +1,7 @@
using System;
using API.Entities.Enums;
-namespace API.DTOs;
+namespace API.DTOs.Dashboard;
///
/// This is a representation of a Series with some amount of underlying files within it. This is used for Recently Updated Series section
///
diff --git a/API/DTOs/RecentlyAddedItemDto.cs b/API/DTOs/Dashboard/RecentlyAddedItemDto.cs
similarity index 97%
rename from API/DTOs/RecentlyAddedItemDto.cs
rename to API/DTOs/Dashboard/RecentlyAddedItemDto.cs
index 93ef9ac9a..2e5658e2e 100644
--- a/API/DTOs/RecentlyAddedItemDto.cs
+++ b/API/DTOs/Dashboard/RecentlyAddedItemDto.cs
@@ -1,7 +1,7 @@
using System;
using API.Entities.Enums;
-namespace API.DTOs;
+namespace API.DTOs.Dashboard;
///
/// A mesh of data for Recently added volume/chapters
diff --git a/API/DTOs/Dashboard/SmartFilterDto.cs b/API/DTOs/Dashboard/SmartFilterDto.cs
new file mode 100644
index 000000000..b23a74c69
--- /dev/null
+++ b/API/DTOs/Dashboard/SmartFilterDto.cs
@@ -0,0 +1,13 @@
+using API.DTOs.Filtering.v2;
+
+namespace API.DTOs.Dashboard;
+
+public class SmartFilterDto
+{
+ public int Id { get; set; }
+ public required string Name { get; set; }
+ ///
+ /// This is the Filter url encoded. It is decoded and reconstructed into a
+ ///
+ public required string Filter { get; set; }
+}
diff --git a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs b/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs
new file mode 100644
index 000000000..c2320f1a9
--- /dev/null
+++ b/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs
@@ -0,0 +1,9 @@
+namespace API.DTOs.Dashboard;
+
+public class UpdateDashboardStreamPositionDto
+{
+ public int FromPosition { get; set; }
+ public int ToPosition { get; set; }
+ public int DashboardStreamId { get; set; }
+ public string StreamName { get; set; }
+}
diff --git a/API/DTOs/Filtering/SortField.cs b/API/DTOs/Filtering/SortField.cs
index 918b74279..f30b617df 100644
--- a/API/DTOs/Filtering/SortField.cs
+++ b/API/DTOs/Filtering/SortField.cs
@@ -25,5 +25,9 @@ public enum SortField
///
/// Release Year of the Series
///
- ReleaseYear = 6
+ ReleaseYear = 6,
+ ///
+ /// Last time the user had any reading progress
+ ///
+ ReadProgress = 7,
}
diff --git a/API/DTOs/Filtering/v2/FilterField.cs b/API/DTOs/Filtering/v2/FilterField.cs
index 73fef1c37..776bc0e26 100644
--- a/API/DTOs/Filtering/v2/FilterField.cs
+++ b/API/DTOs/Filtering/v2/FilterField.cs
@@ -36,5 +36,10 @@ public enum FilterField
///
/// File path
///
- FilePath = 25
+ FilePath = 25,
+ ///
+ /// On Want To Read or Not
+ ///
+ WantToRead = 26
+
}
diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/API/DTOs/Filtering/v2/FilterV2Dto.cs
index 2dff500f7..e25f1e21d 100644
--- a/API/DTOs/Filtering/v2/FilterV2Dto.cs
+++ b/API/DTOs/Filtering/v2/FilterV2Dto.cs
@@ -10,6 +10,10 @@ namespace API.DTOs.Filtering.v2;
///
public class FilterV2Dto
{
+ ///
+ /// Not used in the UI.
+ ///
+ public int Id { get; set; }
///
/// The name of the filter
///
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index e63549c6c..b5e6abaa0 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -54,6 +54,8 @@ public sealed class DataContext : IdentityDbContext ScrobbleHold { get; set; } = null!;
public DbSet AppUserOnDeckRemoval { get; set; } = null!;
public DbSet AppUserTableOfContent { get; set; } = null!;
+ public DbSet AppUserSmartFilter { get; set; } = null!;
+ public DbSet AppUserDashboardStream { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
@@ -119,6 +121,13 @@ public sealed class DataContext : IdentityDbContext()
.Property(b => b.ISBN)
.HasDefaultValue(string.Empty);
+
+ builder.Entity()
+ .Property(b => b.StreamType)
+ .HasDefaultValue(DashboardStreamType.SmartFilter);
+ builder.Entity()
+ .HasIndex(e => e.Visible)
+ .IsUnique(false);
}
diff --git a/API/Data/Migrations/20230904184205_SmartFilters.Designer.cs b/API/Data/Migrations/20230904184205_SmartFilters.Designer.cs
new file mode 100644
index 000000000..2379ec2ad
--- /dev/null
+++ b/API/Data/Migrations/20230904184205_SmartFilters.Designer.cs
@@ -0,0 +1,2310 @@
+//
+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("20230904184205_SmartFilters")]
+ partial class SmartFilters
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.10");
+
+ 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("AgeRestriction")
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRestrictionIncludeUnknowns")
+ .HasColumnType("INTEGER");
+
+ b.Property("AniListAccessToken")
+ .HasColumnType("TEXT");
+
+ b.Property("ApiKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("ConfirmationToken")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActive")
+ .HasColumnType("TEXT");
+
+ b.Property("LastActiveUtc")
+ .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("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .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.AppUserOnDeckRemoval", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserOnDeckRemoval");
+ });
+
+ 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("BookReaderWritingStyle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("BookThemeName")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("Dark");
+
+ b.Property("CollapseSeriesRelationships")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmulateBook")
+ .HasColumnType("INTEGER");
+
+ b.Property("GlobalPageLayoutMode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(0);
+
+ b.Property("LayoutMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("Locale")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("en");
+
+ b.Property("NoTransitions")
+ .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("ShareReviews")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowScreenHints")
+ .HasColumnType("INTEGER");
+
+ b.Property("SwipeToPaginate")
+ .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("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PagesRead")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ 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("HasBeenRated")
+ .HasColumnType("INTEGER");
+
+ b.Property("Rating")
+ .HasColumnType("REAL");
+
+ b.Property("Review")
+ .HasColumnType("TEXT");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Tagline")
+ .HasColumnType("TEXT");
+
+ 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.AppUserSmartFilter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AppUserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Filter")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.ToTable("AppUserSmartFilter");
+ });
+
+ modelBuilder.Entity("API.Entities.AppUserTableOfContent", 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("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PageNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeriesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("VolumeId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AppUserId");
+
+ b.HasIndex("ChapterId");
+
+ b.HasIndex("SeriesId");
+
+ b.ToTable("AppUserTableOfContent");
+ });
+
+ modelBuilder.Entity("API.Entities.Chapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternateCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternateNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("AlternateSeries")
+ .HasColumnType("TEXT");
+
+ 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("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("ISBN")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("");
+
+ b.Property("IsSpecial")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .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("SeriesGroup")
+ .HasColumnType("TEXT");
+
+ b.Property("StoryArc")
+ .HasColumnType("TEXT");
+
+ b.Property("StoryArcNumber")
+ .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("WebLinks")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("");
+
+ 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("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("EmailAddress")
+ .HasColumnType("TEXT");
+
+ b.Property("IpAddress")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastUsed")
+ .HasColumnType("TEXT");
+
+ b.Property("LastUsedUtc")
+ .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("NormalizedTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedTitle")
+ .IsUnique();
+
+ b.ToTable("Genre");
+ });
+
+ modelBuilder.Entity("API.Entities.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AllowScrobbling")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true);
+
+ b.Property("CoverImage")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("FolderWatching")
+ .HasColumnType("INTEGER");
+
+ b.Property("IncludeInDashboard")
+ .HasColumnType("INTEGER");
+
+ b.Property("IncludeInRecommended")
+ .HasColumnType("INTEGER");
+
+ b.Property("IncludeInSearch")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastScanned")
+ .HasColumnType("TEXT");
+
+ b.Property("ManageCollections")
+ .HasColumnType("INTEGER");
+
+ b.Property("ManageReadingLists")
+ .HasColumnType("INTEGER");
+
+ 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("Bytes")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Extension")
+ .HasColumnType("TEXT");
+
+ b.Property("FilePath")
+ .HasColumnType("TEXT");
+
+ b.Property("Format")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastFileAnalysis")
+ .HasColumnType("TEXT");
+
+ b.Property("LastFileAnalysisUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Pages")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChapterId");
+
+ b.ToTable("MangaFile");
+ });
+
+ modelBuilder.Entity("API.Entities.MediaError", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Comment")
+ .HasColumnType("TEXT");
+
+ b.Property("Created")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedUtc")
+ .HasColumnType("TEXT");
+
+ b.Property("Details")
+ .HasColumnType("TEXT");
+
+ b.Property("Extension")
+ .HasColumnType("TEXT");
+
+ b.Property("FilePath")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("LastModifiedUtc")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaError");
+ });
+
+ 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