Logging Enhancements (#1521)

* Recreated Kavita Logging with Serilog instead of Default. This needs to be move out of the appsettings now, to allow auto updater to patch.

* Refactored the code to be completely configured via Code rather than appsettings.json. This is a required step for Auto Updating.

* Added in the ability to send logs directly to the UI only for users on the log route. Stopping implementation as Alerts page will handle the rest of the implementation.

* Fixed up the backup service to not rely on Config from appsettings.json

* Tweaked the Logging levels available

* Moved everything over to File-scoped namespaces

* Moved everything over to File-scoped namespaces

* Code cleanup, removed an old migration and changed so debug logging doesn't print sensitive db data

* Removed dead code
This commit is contained in:
Joseph Milazzo 2022-09-12 19:25:48 -05:00 committed by GitHub
parent 9f715cc35f
commit d1a14f7e68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
212 changed files with 16599 additions and 16834 deletions

View file

@ -73,6 +73,14 @@
<PackageReference Include="NetVips" Version="2.2.0" />
<PackageReference Include="NetVips.Native" Version="8.13.0" />
<PackageReference Include="NReco.Logging.File" Version="1.1.5" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
<PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.43.0.51858">

View file

@ -1,21 +1,20 @@
namespace API.Archive
namespace API.Archive;
/// <summary>
/// Represents which library should handle opening this library
/// </summary>
public enum ArchiveLibrary
{
/// <summary>
/// Represents which library should handle opening this library
/// The underlying archive cannot be opened
/// </summary>
public enum ArchiveLibrary
{
/// <summary>
/// The underlying archive cannot be opened
/// </summary>
NotSupported = 0,
/// <summary>
/// The underlying archive can be opened by SharpCompress
/// </summary>
SharpCompress = 1,
/// <summary>
/// The underlying archive can be opened by default .NET
/// </summary>
Default = 2
}
NotSupported = 0,
/// <summary>
/// The underlying archive can be opened by SharpCompress
/// </summary>
SharpCompress = 1,
/// <summary>
/// The underlying archive can be opened by default .NET
/// </summary>
Default = 2
}

View file

@ -1,66 +1,65 @@
using System.Collections.Generic;
namespace API.Comparators
namespace API.Comparators;
/// <summary>
/// Sorts chapters based on their Number. Uses natural ordering of doubles.
/// </summary>
public class ChapterSortComparer : IComparer<double>
{
/// <summary>
/// Sorts chapters based on their Number. Uses natural ordering of doubles.
/// Normal sort for 2 doubles. 0 always comes last
/// </summary>
public class ChapterSortComparer : IComparer<double>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public int Compare(double x, double y)
{
/// <summary>
/// Normal sort for 2 doubles. 0 always comes last
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public int Compare(double x, double y)
{
if (x == 0.0 && y == 0.0) return 0;
// if x is 0, it comes second
if (x == 0.0) return 1;
// if y is 0, it comes second
if (y == 0.0) return -1;
if (x == 0.0 && y == 0.0) return 0;
// if x is 0, it comes second
if (x == 0.0) return 1;
// if y is 0, it comes second
if (y == 0.0) return -1;
return x.CompareTo(y);
}
public static readonly ChapterSortComparer Default = new ChapterSortComparer();
return x.CompareTo(y);
}
/// <summary>
/// This is a special case comparer used exclusively for sorting chapters within a single Volume for reading order.
/// <example>
/// Volume 10 has "Series - Vol 10" and "Series - Vol 10 Chapter 81". In this case, for reading order, the order is Vol 10, Vol 10 Chapter 81.
/// This is represented by Chapter 0, Chapter 81.
/// </example>
/// </summary>
public class ChapterSortComparerZeroFirst : IComparer<double>
public static readonly ChapterSortComparer Default = new ChapterSortComparer();
}
/// <summary>
/// This is a special case comparer used exclusively for sorting chapters within a single Volume for reading order.
/// <example>
/// Volume 10 has "Series - Vol 10" and "Series - Vol 10 Chapter 81". In this case, for reading order, the order is Vol 10, Vol 10 Chapter 81.
/// This is represented by Chapter 0, Chapter 81.
/// </example>
/// </summary>
public class ChapterSortComparerZeroFirst : IComparer<double>
{
public int Compare(double x, double y)
{
public int Compare(double x, double y)
{
if (x == 0.0 && y == 0.0) return 0;
// if x is 0, it comes first
if (x == 0.0) return -1;
// if y is 0, it comes first
if (y == 0.0) return 1;
if (x == 0.0 && y == 0.0) return 0;
// if x is 0, it comes first
if (x == 0.0) return -1;
// if y is 0, it comes first
if (y == 0.0) return 1;
return x.CompareTo(y);
}
public static readonly ChapterSortComparerZeroFirst Default = new ChapterSortComparerZeroFirst();
return x.CompareTo(y);
}
public class SortComparerZeroLast : IComparer<double>
{
public int Compare(double x, double y)
{
if (x == 0.0 && y == 0.0) return 0;
// if x is 0, it comes last
if (x == 0.0) return 1;
// if y is 0, it comes last
if (y == 0.0) return -1;
public static readonly ChapterSortComparerZeroFirst Default = new ChapterSortComparerZeroFirst();
}
return x.CompareTo(y);
}
public class SortComparerZeroLast : IComparer<double>
{
public int Compare(double x, double y)
{
if (x == 0.0 && y == 0.0) return 0;
// if x is 0, it comes last
if (x == 0.0) return 1;
// if y is 0, it comes last
if (y == 0.0) return -1;
return x.CompareTo(y);
}
}

View file

@ -1,17 +1,16 @@
using System.Collections;
namespace API.Comparators
{
public class NumericComparer : IComparer
{
namespace API.Comparators;
public int Compare(object x, object y)
public class NumericComparer : IComparer
{
public int Compare(object x, object y)
{
if((x is string xs) && (y is string ys))
{
if((x is string xs) && (y is string ys))
{
return StringLogicalComparer.Compare(xs, ys);
}
return -1;
return StringLogicalComparer.Compare(xs, ys);
}
return -1;
}
}
}

View file

@ -4,127 +4,126 @@
using static System.Char;
namespace API.Comparators
namespace API.Comparators;
public static class StringLogicalComparer
{
public static class StringLogicalComparer
public static int Compare(string s1, string s2)
{
public static int Compare(string s1, string s2)
{
//get rid of special cases
if((s1 == null) && (s2 == null)) return 0;
if(s1 == null) return -1;
if(s2 == null) return 1;
//get rid of special cases
if((s1 == null) && (s2 == null)) return 0;
if(s1 == null) return -1;
if(s2 == null) return 1;
if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2)) return 0;
if (string.IsNullOrEmpty(s1)) return -1;
if (string.IsNullOrEmpty(s2)) return -1;
if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2)) return 0;
if (string.IsNullOrEmpty(s1)) return -1;
if (string.IsNullOrEmpty(s2)) return -1;
//WE style, special case
var sp1 = IsLetterOrDigit(s1, 0);
var sp2 = IsLetterOrDigit(s2, 0);
if(sp1 && !sp2) return 1;
if(!sp1 && sp2) return -1;
//WE style, special case
var sp1 = IsLetterOrDigit(s1, 0);
var sp2 = IsLetterOrDigit(s2, 0);
if(sp1 && !sp2) return 1;
if(!sp1 && sp2) return -1;
int i1 = 0, i2 = 0; //current index
while(true)
{
var c1 = IsDigit(s1, i1);
var c2 = IsDigit(s2, i2);
int r; // temp result
if(!c1 && !c2)
{
bool letter1 = IsLetter(s1, i1);
bool letter2 = IsLetter(s2, i2);
if((letter1 && letter2) || (!letter1 && !letter2))
{
if(letter1 && letter2)
{
r = ToLower(s1[i1]).CompareTo(ToLower(s2[i2]));
}
else
{
r = s1[i1].CompareTo(s2[i2]);
}
if(r != 0) return r;
}
else if(!letter1 && letter2) return -1;
else if(letter1 && !letter2) return 1;
}
else if(c1 && c2)
{
r = CompareNum(s1, ref i1, s2, ref i2);
if(r != 0) return r;
}
else if(c1)
{
return -1;
}
else if(c2)
{
return 1;
}
i1++;
i2++;
if((i1 >= s1.Length) && (i2 >= s2.Length))
{
return 0;
}
if(i1 >= s1.Length)
{
return -1;
}
if(i2 >= s2.Length)
{
return -1;
}
}
}
private static int CompareNum(string s1, ref int i1, string s2, ref int i2)
{
int nzStart1 = i1, nzStart2 = i2; // nz = non zero
int end1 = i1, end2 = i2;
ScanNumEnd(s1, i1, ref end1, ref nzStart1);
ScanNumEnd(s2, i2, ref end2, ref nzStart2);
var start1 = i1; i1 = end1 - 1;
var start2 = i2; i2 = end2 - 1;
var nzLength1 = end1 - nzStart1;
var nzLength2 = end2 - nzStart2;
if(nzLength1 < nzLength2) return -1;
if(nzLength1 > nzLength2) return 1;
for(int j1 = nzStart1,j2 = nzStart2; j1 <= i1; j1++,j2++)
{
var r = s1[j1].CompareTo(s2[j2]);
if(r != 0) return r;
}
// the nz parts are equal
var length1 = end1 - start1;
var length2 = end2 - start2;
if(length1 == length2) return 0;
if(length1 > length2) return -1;
return 1;
}
//lookahead
private static void ScanNumEnd(string s, int start, ref int end, ref int nzStart)
{
nzStart = start;
end = start;
var countZeros = true;
while(IsDigit(s, end))
{
if(countZeros && s[end].Equals('0'))
{
nzStart++;
}
else countZeros = false;
end++;
if(end >= s.Length) break;
}
}
int i1 = 0, i2 = 0; //current index
while(true)
{
var c1 = IsDigit(s1, i1);
var c2 = IsDigit(s2, i2);
int r; // temp result
if(!c1 && !c2)
{
bool letter1 = IsLetter(s1, i1);
bool letter2 = IsLetter(s2, i2);
if((letter1 && letter2) || (!letter1 && !letter2))
{
if(letter1 && letter2)
{
r = ToLower(s1[i1]).CompareTo(ToLower(s2[i2]));
}
else
{
r = s1[i1].CompareTo(s2[i2]);
}
if(r != 0) return r;
}
else if(!letter1 && letter2) return -1;
else if(letter1 && !letter2) return 1;
}
else if(c1 && c2)
{
r = CompareNum(s1, ref i1, s2, ref i2);
if(r != 0) return r;
}
else if(c1)
{
return -1;
}
else if(c2)
{
return 1;
}
i1++;
i2++;
if((i1 >= s1.Length) && (i2 >= s2.Length))
{
return 0;
}
if(i1 >= s1.Length)
{
return -1;
}
if(i2 >= s2.Length)
{
return -1;
}
}
}
}
private static int CompareNum(string s1, ref int i1, string s2, ref int i2)
{
int nzStart1 = i1, nzStart2 = i2; // nz = non zero
int end1 = i1, end2 = i2;
ScanNumEnd(s1, i1, ref end1, ref nzStart1);
ScanNumEnd(s2, i2, ref end2, ref nzStart2);
var start1 = i1; i1 = end1 - 1;
var start2 = i2; i2 = end2 - 1;
var nzLength1 = end1 - nzStart1;
var nzLength2 = end2 - nzStart2;
if(nzLength1 < nzLength2) return -1;
if(nzLength1 > nzLength2) return 1;
for(int j1 = nzStart1,j2 = nzStart2; j1 <= i1; j1++,j2++)
{
var r = s1[j1].CompareTo(s2[j2]);
if(r != 0) return r;
}
// the nz parts are equal
var length1 = end1 - start1;
var length2 = end2 - start2;
if(length1 == length2) return 0;
if(length1 > length2) return -1;
return 1;
}
//lookahead
private static void ScanNumEnd(string s, int start, ref int end, ref int nzStart)
{
nzStart = start;
end = start;
var countZeros = true;
while(IsDigit(s, end))
{
if(countZeros && s[end].Equals('0'))
{
nzStart++;
}
else countZeros = false;
end++;
if(end >= s.Length) break;
}
}
}

View file

@ -1,34 +1,33 @@
using System.Collections.Immutable;
namespace API.Constants
namespace API.Constants;
/// <summary>
/// Role-based Security
/// </summary>
public static class PolicyConstants
{
/// <summary>
/// Role-based Security
/// Admin User. Has all privileges
/// </summary>
public static class PolicyConstants
{
/// <summary>
/// Admin User. Has all privileges
/// </summary>
public const string AdminRole = "Admin";
/// <summary>
/// Non-Admin User. Must be granted privileges by an Admin.
/// </summary>
public const string PlebRole = "Pleb";
/// <summary>
/// Used to give a user ability to download files from the server
/// </summary>
public const string DownloadRole = "Download";
/// <summary>
/// Used to give a user ability to change their own password
/// </summary>
public const string ChangePasswordRole = "Change Password";
/// <summary>
/// Used to give a user ability to bookmark files on the server
/// </summary>
public const string BookmarkRole = "Bookmark";
public const string AdminRole = "Admin";
/// <summary>
/// Non-Admin User. Must be granted privileges by an Admin.
/// </summary>
public const string PlebRole = "Pleb";
/// <summary>
/// Used to give a user ability to download files from the server
/// </summary>
public const string DownloadRole = "Download";
/// <summary>
/// Used to give a user ability to change their own password
/// </summary>
public const string ChangePasswordRole = "Change Password";
/// <summary>
/// Used to give a user ability to bookmark files on the server
/// </summary>
public const string BookmarkRole = "Bookmark";
public static readonly ImmutableArray<string> ValidRoles =
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole);
}
public static readonly ImmutableArray<string> ValidRoles =
ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole);
}

File diff suppressed because it is too large Load diff

View file

@ -4,27 +4,26 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
namespace API.Controllers;
public class AdminController : BaseApiController
{
public class AdminController : BaseApiController
private readonly UserManager<AppUser> _userManager;
public AdminController(UserManager<AppUser> userManager)
{
private readonly UserManager<AppUser> _userManager;
_userManager = userManager;
}
public AdminController(UserManager<AppUser> userManager)
{
_userManager = userManager;
}
/// <summary>
/// Checks if an admin exists on the system. This is essentially a check to validate if the system has been setup.
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("exists")]
public async Task<ActionResult<bool>> AdminExists()
{
var users = await _userManager.GetUsersInRoleAsync("Admin");
return users.Count > 0;
}
/// <summary>
/// Checks if an admin exists on the system. This is essentially a check to validate if the system has been setup.
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("exists")]
public async Task<ActionResult<bool>> AdminExists()
{
var users = await _userManager.GetUsersInRoleAsync("Admin");
return users.Count > 0;
}
}

View file

@ -1,12 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
namespace API.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class BaseApiController : ControllerBase
{
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class BaseApiController : ControllerBase
{
}
}

View file

@ -13,151 +13,150 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using VersOne.Epub;
namespace API.Controllers
namespace API.Controllers;
public class BookController : BaseApiController
{
public class BookController : BaseApiController
private readonly IBookService _bookService;
private readonly IUnitOfWork _unitOfWork;
private readonly ICacheService _cacheService;
public BookController(IBookService bookService,
IUnitOfWork unitOfWork, ICacheService cacheService)
{
private readonly IBookService _bookService;
private readonly IUnitOfWork _unitOfWork;
private readonly ICacheService _cacheService;
public BookController(IBookService bookService,
IUnitOfWork unitOfWork, ICacheService cacheService)
{
_bookService = bookService;
_unitOfWork = unitOfWork;
_cacheService = cacheService;
}
/// <summary>
/// Retrieves information for the PDF and Epub reader
/// </summary>
/// <remarks>This only applies to Epub or PDF files</remarks>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("{chapterId}/book-info")]
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
{
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
var bookTitle = string.Empty;
switch (dto.SeriesFormat)
{
case MangaFormat.Epub:
{
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
bookTitle = book.Title;
break;
}
case MangaFormat.Pdf:
{
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
if (string.IsNullOrEmpty(bookTitle))
{
// Override with filename
bookTitle = Path.GetFileNameWithoutExtension(mangaFile.FilePath);
}
break;
}
case MangaFormat.Image:
break;
case MangaFormat.Archive:
break;
case MangaFormat.Unknown:
break;
default:
throw new ArgumentOutOfRangeException();
}
return Ok(new BookInfoDto()
{
ChapterNumber = dto.ChapterNumber,
VolumeNumber = dto.VolumeNumber,
VolumeId = dto.VolumeId,
BookTitle = bookTitle,
SeriesName = dto.SeriesName,
SeriesFormat = dto.SeriesFormat,
SeriesId = dto.SeriesId,
LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
});
}
/// <summary>
/// This is an entry point to fetch resources from within an epub chapter/book.
/// </summary>
/// <param name="chapterId"></param>
/// <param name="file"></param>
/// <returns></returns>
[HttpGet("{chapterId}/book-resources")]
[ResponseCache(Duration = 60 * 1, Location = ResponseCacheLocation.Client, NoStore = false)]
[AllowAnonymous]
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
{
if (chapterId <= 0) return BadRequest("Chapter is not valid");
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
var key = BookService.CleanContentKeys(file);
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
var bookFile = book.Content.AllFiles[key];
var content = await bookFile.ReadContentAsBytesAsync();
var contentType = BookService.GetContentType(bookFile.ContentType);
return File(content, contentType, $"{chapterId}-{file}");
}
/// <summary>
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
/// this is used to rewrite anchors in the book text so that we always load properly in our reader.
/// </summary>
/// <remarks>This is essentially building the table of contents</remarks>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("{chapterId}/chapters")]
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
{
if (chapterId <= 0) return BadRequest("Chapter is not valid");
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
try
{
return Ok(await _bookService.GenerateTableOfContents(chapter));
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
/// all css is scoped, etc.
/// </summary>
/// <param name="chapterId"></param>
/// <param name="page"></param>
/// <returns></returns>
[HttpGet("{chapterId}/book-page")]
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
{
var chapter = await _cacheService.Ensure(chapterId);
var path = _cacheService.GetCachedFile(chapter);
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
try
{
return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl));
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
_bookService = bookService;
_unitOfWork = unitOfWork;
_cacheService = cacheService;
}
/// <summary>
/// Retrieves information for the PDF and Epub reader
/// </summary>
/// <remarks>This only applies to Epub or PDF files</remarks>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("{chapterId}/book-info")]
public async Task<ActionResult<BookInfoDto>> GetBookInfo(int chapterId)
{
var dto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(chapterId);
var bookTitle = string.Empty;
switch (dto.SeriesFormat)
{
case MangaFormat.Epub:
{
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
bookTitle = book.Title;
break;
}
case MangaFormat.Pdf:
{
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId)).First();
if (string.IsNullOrEmpty(bookTitle))
{
// Override with filename
bookTitle = Path.GetFileNameWithoutExtension(mangaFile.FilePath);
}
break;
}
case MangaFormat.Image:
break;
case MangaFormat.Archive:
break;
case MangaFormat.Unknown:
break;
default:
throw new ArgumentOutOfRangeException();
}
return Ok(new BookInfoDto()
{
ChapterNumber = dto.ChapterNumber,
VolumeNumber = dto.VolumeNumber,
VolumeId = dto.VolumeId,
BookTitle = bookTitle,
SeriesName = dto.SeriesName,
SeriesFormat = dto.SeriesFormat,
SeriesId = dto.SeriesId,
LibraryId = dto.LibraryId,
IsSpecial = dto.IsSpecial,
Pages = dto.Pages,
});
}
/// <summary>
/// This is an entry point to fetch resources from within an epub chapter/book.
/// </summary>
/// <param name="chapterId"></param>
/// <param name="file"></param>
/// <returns></returns>
[HttpGet("{chapterId}/book-resources")]
[ResponseCache(Duration = 60 * 1, Location = ResponseCacheLocation.Client, NoStore = false)]
[AllowAnonymous]
public async Task<ActionResult> GetBookPageResources(int chapterId, [FromQuery] string file)
{
if (chapterId <= 0) return BadRequest("Chapter is not valid");
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
var key = BookService.CleanContentKeys(file);
if (!book.Content.AllFiles.ContainsKey(key)) return BadRequest("File was not found in book");
var bookFile = book.Content.AllFiles[key];
var content = await bookFile.ReadContentAsBytesAsync();
var contentType = BookService.GetContentType(bookFile.ContentType);
return File(content, contentType, $"{chapterId}-{file}");
}
/// <summary>
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
/// this is used to rewrite anchors in the book text so that we always load properly in our reader.
/// </summary>
/// <remarks>This is essentially building the table of contents</remarks>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("{chapterId}/chapters")]
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
{
if (chapterId <= 0) return BadRequest("Chapter is not valid");
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
try
{
return Ok(await _bookService.GenerateTableOfContents(chapter));
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// This returns a single page within the epub book. All html will be rewritten to be scoped within our reader,
/// all css is scoped, etc.
/// </summary>
/// <param name="chapterId"></param>
/// <param name="page"></param>
/// <returns></returns>
[HttpGet("{chapterId}/book-page")]
public async Task<ActionResult<string>> GetBookPage(int chapterId, [FromQuery] int page)
{
var chapter = await _cacheService.Ensure(chapterId);
var path = _cacheService.GetCachedFile(chapter);
var baseUrl = "//" + Request.Host + Request.PathBase + "/api/";
try
{
return Ok(await _bookService.GetBookPage(page, chapterId, path, baseUrl));
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
}

View file

@ -11,182 +11,181 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
namespace API.Controllers
namespace API.Controllers;
/// <summary>
/// APIs for Collections
/// </summary>
public class CollectionController : BaseApiController
{
/// <summary>
/// APIs for Collections
/// </summary>
public class CollectionController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
/// <inheritdoc />
public CollectionController(IUnitOfWork unitOfWork, IEventHub eventHub)
{
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
_unitOfWork = unitOfWork;
_eventHub = eventHub;
}
/// <inheritdoc />
public CollectionController(IUnitOfWork unitOfWork, IEventHub eventHub)
/// <summary>
/// Return a list of all collection tags on the server
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
if (isAdmin)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
}
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
}
/// <summary>
/// Return a list of all collection tags on the server
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
/// <summary>
/// Searches against the collection tags on the DB and returns matches that meet the search criteria.
/// <remarks>Search strings will be cleaned of certain fields, like %</remarks>
/// </summary>
/// <param name="queryString">Search term</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("search")]
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
{
queryString ??= "";
queryString = queryString.Replace(@"%", string.Empty);
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
}
/// <summary>
/// Updates an existing tag with a new title, promotion status, and summary.
/// <remarks>UI does not contain controls to update title</remarks>
/// </summary>
/// <param name="updatedTag"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update")]
public async Task<ActionResult> UpdateTagPromotion(CollectionTagDto updatedTag)
{
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
if (existingTag == null) return BadRequest("This tag does not exist");
existingTag.Promoted = updatedTag.Promoted;
existingTag.Title = updatedTag.Title.Trim();
existingTag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(updatedTag.Title).ToUpper();
existingTag.Summary = updatedTag.Summary.Trim();
if (_unitOfWork.HasChanges())
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
if (isAdmin)
{
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
}
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
}
/// <summary>
/// Searches against the collection tags on the DB and returns matches that meet the search criteria.
/// <remarks>Search strings will be cleaned of certain fields, like %</remarks>
/// </summary>
/// <param name="queryString">Search term</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("search")]
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
{
queryString ??= "";
queryString = queryString.Replace(@"%", string.Empty);
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
}
/// <summary>
/// Updates an existing tag with a new title, promotion status, and summary.
/// <remarks>UI does not contain controls to update title</remarks>
/// </summary>
/// <param name="updatedTag"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update")]
public async Task<ActionResult> UpdateTagPromotion(CollectionTagDto updatedTag)
{
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
if (existingTag == null) return BadRequest("This tag does not exist");
existingTag.Promoted = updatedTag.Promoted;
existingTag.Title = updatedTag.Title.Trim();
existingTag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(updatedTag.Title).ToUpper();
existingTag.Summary = updatedTag.Summary.Trim();
if (_unitOfWork.HasChanges())
{
if (await _unitOfWork.CommitAsync())
{
return Ok("Tag updated successfully");
}
}
else
if (await _unitOfWork.CommitAsync())
{
return Ok("Tag updated successfully");
}
return BadRequest("Something went wrong, please try again");
}
else
{
return Ok("Tag updated successfully");
}
/// <summary>
/// Adds a collection tag onto multiple Series. If tag id is 0, this will create a new tag.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update-for-series")]
public async Task<ActionResult> AddToMultipleSeries(CollectionTagBulkAddDto dto)
return BadRequest("Something went wrong, please try again");
}
/// <summary>
/// Adds a collection tag onto multiple Series. If tag id is 0, this will create a new tag.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update-for-series")]
public async Task<ActionResult> AddToMultipleSeries(CollectionTagBulkAddDto dto)
{
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(dto.CollectionTagId);
if (tag == null)
{
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(dto.CollectionTagId);
if (tag == null)
tag = DbFactory.CollectionTag(0, dto.CollectionTagTitle, String.Empty, false);
_unitOfWork.CollectionTagRepository.Add(tag);
}
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(dto.SeriesIds);
foreach (var metadata in seriesMetadatas)
{
if (!metadata.CollectionTags.Any(t => t.Title.Equals(tag.Title, StringComparison.InvariantCulture)))
{
tag = DbFactory.CollectionTag(0, dto.CollectionTagTitle, String.Empty, false);
_unitOfWork.CollectionTagRepository.Add(tag);
metadata.CollectionTags.Add(tag);
_unitOfWork.SeriesMetadataRepository.Update(metadata);
}
}
if (!_unitOfWork.HasChanges()) return Ok();
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
return BadRequest("There was an issue updating series with collection tag");
}
/// <summary>
/// For a given tag, update the summary if summary has changed and remove a set of series from the tag.
/// </summary>
/// <param name="updateSeriesForTagDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update-series")]
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
{
try
{
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id);
if (tag == null) return BadRequest("Not a valid Tag");
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
// Check if Tag has updated (Summary)
if (tag.Summary == null || !tag.Summary.Equals(updateSeriesForTagDto.Tag.Summary))
{
tag.Summary = updateSeriesForTagDto.Tag.Summary;
_unitOfWork.CollectionTagRepository.Update(tag);
}
tag.CoverImageLocked = updateSeriesForTagDto.Tag.CoverImageLocked;
if (!updateSeriesForTagDto.Tag.CoverImageLocked)
{
tag.CoverImageLocked = false;
tag.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
_unitOfWork.CollectionTagRepository.Update(tag);
}
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
{
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
}
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(dto.SeriesIds);
foreach (var metadata in seriesMetadatas)
if (tag.SeriesMetadatas.Count == 0)
{
if (!metadata.CollectionTags.Any(t => t.Title.Equals(tag.Title, StringComparison.InvariantCulture)))
{
metadata.CollectionTags.Add(tag);
_unitOfWork.SeriesMetadataRepository.Update(metadata);
}
_unitOfWork.CollectionTagRepository.Remove(tag);
}
if (!_unitOfWork.HasChanges()) return Ok();
if (!_unitOfWork.HasChanges()) return Ok("No updates");
if (await _unitOfWork.CommitAsync())
{
return Ok();
return Ok("Tag updated");
}
return BadRequest("There was an issue updating series with collection tag");
}
/// <summary>
/// For a given tag, update the summary if summary has changed and remove a set of series from the tag.
/// </summary>
/// <param name="updateSeriesForTagDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update-series")]
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
catch (Exception)
{
try
{
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id);
if (tag == null) return BadRequest("Not a valid Tag");
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
// Check if Tag has updated (Summary)
if (tag.Summary == null || !tag.Summary.Equals(updateSeriesForTagDto.Tag.Summary))
{
tag.Summary = updateSeriesForTagDto.Tag.Summary;
_unitOfWork.CollectionTagRepository.Update(tag);
}
tag.CoverImageLocked = updateSeriesForTagDto.Tag.CoverImageLocked;
if (!updateSeriesForTagDto.Tag.CoverImageLocked)
{
tag.CoverImageLocked = false;
tag.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
_unitOfWork.CollectionTagRepository.Update(tag);
}
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
{
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
}
if (tag.SeriesMetadatas.Count == 0)
{
_unitOfWork.CollectionTagRepository.Remove(tag);
}
if (!_unitOfWork.HasChanges()) return Ok("No updates");
if (await _unitOfWork.CommitAsync())
{
return Ok("Tag updated");
}
}
catch (Exception)
{
await _unitOfWork.RollbackAsync();
}
return BadRequest("Something went wrong. Please try again.");
await _unitOfWork.RollbackAsync();
}
return BadRequest("Something went wrong. Please try again.");
}
}

View file

@ -16,210 +16,209 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers
namespace API.Controllers;
/// <summary>
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
/// </summary>
[Authorize(Policy="RequireDownloadRole")]
public class DownloadController : BaseApiController
{
/// <summary>
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
/// </summary>
[Authorize(Policy="RequireDownloadRole")]
public class DownloadController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IArchiveService _archiveService;
private readonly IDirectoryService _directoryService;
private readonly IDownloadService _downloadService;
private readonly IEventHub _eventHub;
private readonly ILogger<DownloadController> _logger;
private readonly IBookmarkService _bookmarkService;
private readonly IAccountService _accountService;
private const string DefaultContentType = "application/octet-stream";
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
IAccountService accountService)
{
private readonly IUnitOfWork _unitOfWork;
private readonly IArchiveService _archiveService;
private readonly IDirectoryService _directoryService;
private readonly IDownloadService _downloadService;
private readonly IEventHub _eventHub;
private readonly ILogger<DownloadController> _logger;
private readonly IBookmarkService _bookmarkService;
private readonly IAccountService _accountService;
private const string DefaultContentType = "application/octet-stream";
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService,
IAccountService accountService)
{
_unitOfWork = unitOfWork;
_archiveService = archiveService;
_directoryService = directoryService;
_downloadService = downloadService;
_eventHub = eventHub;
_logger = logger;
_bookmarkService = bookmarkService;
_accountService = accountService;
}
/// <summary>
/// For a given volume, return the size in bytes
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
[HttpGet("volume-size")]
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
{
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
/// <summary>
/// For a given chapter, return the size in bytes
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("chapter-size")]
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
{
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
/// <summary>
/// For a series, return the size in bytes
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("series-size")]
public async Task<ActionResult<long>> GetSeriesSize(int seriesId)
{
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
/// <summary>
/// Downloads all chapters within a volume. If the chapters are multiple zips, they will all be zipped up.
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
[Authorize(Policy="RequireDownloadRole")]
[HttpGet("volume")]
public async Task<ActionResult> DownloadVolume(int volumeId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try
{
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip");
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
private async Task<bool> HasDownloadPermission()
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return await _accountService.HasDownloadPermission(user);
}
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
{
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
return PhysicalFile(zipFile, contentType, fileDownloadName, true);
}
/// <summary>
/// Returns the zip for a single chapter. If the chapter contains multiple files, they will be zipped.
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("chapter")]
public async Task<ActionResult> DownloadChapter(int chapterId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try
{
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip");
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
private async Task<ActionResult> DownloadFiles(ICollection<MangaFile> files, string tempFolder, string downloadName)
{
try
{
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 0F, "started"));
if (files.Count == 1)
{
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
return GetFirstFileDownload(files);
}
var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder);
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
return PhysicalFile(filePath, DefaultContentType, downloadName, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception when trying to download files");
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
throw;
}
}
[HttpGet("series")]
public async Task<ActionResult> DownloadSeries(int seriesId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
try
{
return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip");
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Downloads all bookmarks in a zip for
/// </summary>
/// <param name="downloadBookmarkDto"></param>
/// <returns></returns>
[HttpPost("bookmarks")]
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
// We know that all bookmarks will be for one single seriesId
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
var filename = $"{series.Name} - Bookmarks.zip";
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 0F));
var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
var filePath = _archiveService.CreateZipForDownload(files,
$"download_{user.Id}_{seriesIds}_bookmarks");
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F));
return PhysicalFile(filePath, DefaultContentType, filename, true);
}
_unitOfWork = unitOfWork;
_archiveService = archiveService;
_directoryService = directoryService;
_downloadService = downloadService;
_eventHub = eventHub;
_logger = logger;
_bookmarkService = bookmarkService;
_accountService = accountService;
}
/// <summary>
/// For a given volume, return the size in bytes
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
[HttpGet("volume-size")]
public async Task<ActionResult<long>> GetVolumeSize(int volumeId)
{
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
/// <summary>
/// For a given chapter, return the size in bytes
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("chapter-size")]
public async Task<ActionResult<long>> GetChapterSize(int chapterId)
{
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
/// <summary>
/// For a series, return the size in bytes
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("series-size")]
public async Task<ActionResult<long>> GetSeriesSize(int seriesId)
{
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
return Ok(_directoryService.GetTotalSize(files.Select(c => c.FilePath)));
}
/// <summary>
/// Downloads all chapters within a volume. If the chapters are multiple zips, they will all be zipped up.
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
[Authorize(Policy="RequireDownloadRole")]
[HttpGet("volume")]
public async Task<ActionResult> DownloadVolume(int volumeId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.VolumeRepository.GetFilesForVolume(volumeId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(volumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try
{
return await DownloadFiles(files, $"download_{User.GetUsername()}_v{volumeId}", $"{series.Name} - Volume {volume.Number}.zip");
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
private async Task<bool> HasDownloadPermission()
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return await _accountService.HasDownloadPermission(user);
}
private ActionResult GetFirstFileDownload(IEnumerable<MangaFile> files)
{
var (zipFile, contentType, fileDownloadName) = _downloadService.GetFirstFileDownload(files);
return PhysicalFile(zipFile, contentType, fileDownloadName, true);
}
/// <summary>
/// Returns the zip for a single chapter. If the chapter contains multiple files, they will be zipped.
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("chapter")]
public async Task<ActionResult> DownloadChapter(int chapterId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId);
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
var volume = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(chapter.VolumeId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
try
{
return await DownloadFiles(files, $"download_{User.GetUsername()}_c{chapterId}", $"{series.Name} - Chapter {chapter.Number}.zip");
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
private async Task<ActionResult> DownloadFiles(ICollection<MangaFile> files, string tempFolder, string downloadName)
{
try
{
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 0F, "started"));
if (files.Count == 1)
{
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
return GetFirstFileDownload(files);
}
var filePath = _archiveService.CreateZipForDownload(files.Select(c => c.FilePath), tempFolder);
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
return PhysicalFile(filePath, DefaultContentType, downloadName, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception when trying to download files");
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(),
Path.GetFileNameWithoutExtension(downloadName), 1F, "ended"));
throw;
}
}
[HttpGet("series")]
public async Task<ActionResult> DownloadSeries(int seriesId)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
try
{
return await DownloadFiles(files, $"download_{User.GetUsername()}_s{seriesId}", $"{series.Name}.zip");
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Downloads all bookmarks in a zip for
/// </summary>
/// <param name="downloadBookmarkDto"></param>
/// <returns></returns>
[HttpPost("bookmarks")]
public async Task<ActionResult> DownloadBookmarkPages(DownloadBookmarkDto downloadBookmarkDto)
{
if (!await HasDownloadPermission()) return BadRequest("You do not have permission");
if (!downloadBookmarkDto.Bookmarks.Any()) return BadRequest("Bookmarks cannot be empty");
// We know that all bookmarks will be for one single seriesId
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
var files = await _bookmarkService.GetBookmarkFilesById(downloadBookmarkDto.Bookmarks.Select(b => b.Id));
var filename = $"{series.Name} - Bookmarks.zip";
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 0F));
var seriesIds = string.Join("_", downloadBookmarkDto.Bookmarks.Select(b => b.SeriesId).Distinct());
var filePath = _archiveService.CreateZipForDownload(files,
$"download_{user.Id}_{seriesIds}_bookmarks");
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.DownloadProgressEvent(User.GetUsername(), Path.GetFileNameWithoutExtension(filename), 1F));
return PhysicalFile(filePath, DefaultContentType, filename, true);
}
}

View file

@ -7,147 +7,146 @@ using API.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
namespace API.Controllers;
/// <summary>
/// Responsible for servicing up images stored in Kavita for entities
/// </summary>
[AllowAnonymous]
public class ImageController : BaseApiController
{
/// <summary>
/// Responsible for servicing up images stored in Kavita for entities
/// </summary>
[AllowAnonymous]
public class ImageController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
/// <inheritdoc />
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService)
{
private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
_unitOfWork = unitOfWork;
_directoryService = directoryService;
}
/// <inheritdoc />
public ImageController(IUnitOfWork unitOfWork, IDirectoryService directoryService)
{
_unitOfWork = unitOfWork;
_directoryService = directoryService;
}
/// <summary>
/// Returns cover image for Chapter
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("chapter-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
/// <summary>
/// Returns cover image for Chapter
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("chapter-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetChapterCoverImage(int chapterId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ChapterRepository.GetChapterCoverImageAsync(chapterId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
/// Returns cover image for Volume
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
[HttpGet("volume-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
/// <summary>
/// Returns cover image for Volume
/// </summary>
/// <param name="volumeId"></param>
/// <returns></returns>
[HttpGet("volume-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetVolumeCoverImage(int volumeId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.VolumeRepository.GetVolumeCoverImageAsync(volumeId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
/// Returns cover image for Series
/// </summary>
/// <param name="seriesId">Id of Series</param>
/// <returns></returns>
[ResponseCache(CacheProfileName = "Images")]
[HttpGet("series-cover")]
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
/// <summary>
/// Returns cover image for Series
/// </summary>
/// <param name="seriesId">Id of Series</param>
/// <returns></returns>
[ResponseCache(CacheProfileName = "Images")]
[HttpGet("series-cover")]
public async Task<ActionResult> GetSeriesCoverImage(int seriesId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.SeriesRepository.GetSeriesCoverImageAsync(seriesId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
Response.AddCacheHeader(path);
Response.AddCacheHeader(path);
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
/// Returns cover image for Collection Tag
/// </summary>
/// <param name="collectionTagId"></param>
/// <returns></returns>
[HttpGet("collection-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
/// <summary>
/// Returns cover image for Collection Tag
/// </summary>
/// <param name="collectionTagId"></param>
/// <returns></returns>
[HttpGet("collection-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetCollectionCoverImage(int collectionTagId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.CollectionTagRepository.GetCoverImageAsync(collectionTagId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
/// Returns cover image for a Reading List
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet("readinglist-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
/// <summary>
/// Returns cover image for a Reading List
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet("readinglist-cover")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetReadingListCoverImage(int readingListId)
{
var path = Path.Join(_directoryService.CoverImageDirectory, await _unitOfWork.ReadingListRepository.GetCoverImageAsync(readingListId));
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"No cover image");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
/// <summary>
/// Returns image for a given bookmark page
/// </summary>
/// <remarks>This request is served unauthenticated, but user must be passed via api key to validate</remarks>
/// <param name="chapterId"></param>
/// <param name="pageNum">Starts at 0</param>
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
/// <returns></returns>
[HttpGet("bookmark")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
if (bookmark == null) return BadRequest("Bookmark does not exist");
/// <summary>
/// Returns image for a given bookmark page
/// </summary>
/// <remarks>This request is served unauthenticated, but user must be passed via api key to validate</remarks>
/// <param name="chapterId"></param>
/// <param name="pageNum">Starts at 0</param>
/// <param name="apiKey">API Key for user. Needed to authenticate request</param>
/// <returns></returns>
[HttpGet("bookmark")]
[ResponseCache(CacheProfileName = "Images")]
public async Task<ActionResult> GetBookmarkImage(int chapterId, int pageNum, string apiKey)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
var bookmark = await _unitOfWork.UserRepository.GetBookmarkForPage(pageNum, chapterId, userId);
if (bookmark == null) return BadRequest("Bookmark does not exist");
var bookmarkDirectory =
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName));
var format = Path.GetExtension(file.FullName).Replace(".", "");
var bookmarkDirectory =
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
var file = new FileInfo(Path.Join(bookmarkDirectory, bookmark.FileName));
var format = Path.GetExtension(file.FullName).Replace(".", "");
return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName));
}
return PhysicalFile(file.FullName, "image/" + format, Path.GetFileName(file.FullName));
}
/// <summary>
/// Returns a temp coverupload image
/// </summary>
/// <param name="filename">Filename of file. This is used with upload/upload-by-url</param>
/// <returns></returns>
[Authorize(Policy="RequireAdminRole")]
[HttpGet("cover-upload")]
[ResponseCache(CacheProfileName = "Images")]
public ActionResult GetCoverUploadImage(string filename)
{
if (filename.Contains("..")) return BadRequest("Invalid Filename");
/// <summary>
/// Returns a temp coverupload image
/// </summary>
/// <param name="filename">Filename of file. This is used with upload/upload-by-url</param>
/// <returns></returns>
[Authorize(Policy="RequireAdminRole")]
[HttpGet("cover-upload")]
[ResponseCache(CacheProfileName = "Images")]
public ActionResult GetCoverUploadImage(string filename)
{
if (filename.Contains("..")) return BadRequest("Invalid Filename");
var path = Path.Join(_directoryService.TempDirectory, filename);
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
var path = Path.Join(_directoryService.TempDirectory, filename);
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path)) return BadRequest($"File does not exist");
var format = _directoryService.FileSystem.Path.GetExtension(path).Replace(".", "");
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
return PhysicalFile(path, "image/" + format, _directoryService.FileSystem.Path.GetFileName(path));
}
}

View file

@ -22,323 +22,322 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using TaskScheduler = API.Services.TaskScheduler;
namespace API.Controllers
namespace API.Controllers;
[Authorize]
public class LibraryController : BaseApiController
{
[Authorize]
public class LibraryController : BaseApiController
private readonly IDirectoryService _directoryService;
private readonly ILogger<LibraryController> _logger;
private readonly IMapper _mapper;
private readonly ITaskScheduler _taskScheduler;
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly ILibraryWatcher _libraryWatcher;
public LibraryController(IDirectoryService directoryService,
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher)
{
private readonly IDirectoryService _directoryService;
private readonly ILogger<LibraryController> _logger;
private readonly IMapper _mapper;
private readonly ITaskScheduler _taskScheduler;
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly ILibraryWatcher _libraryWatcher;
_directoryService = directoryService;
_logger = logger;
_mapper = mapper;
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
_eventHub = eventHub;
_libraryWatcher = libraryWatcher;
}
public LibraryController(IDirectoryService directoryService,
ILogger<LibraryController> logger, IMapper mapper, ITaskScheduler taskScheduler,
IUnitOfWork unitOfWork, IEventHub eventHub, ILibraryWatcher libraryWatcher)
/// <summary>
/// Creates a new Library. Upon library creation, adds new library to all Admin accounts.
/// </summary>
/// <param name="createLibraryDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("create")]
public async Task<ActionResult> AddLibrary(CreateLibraryDto createLibraryDto)
{
if (await _unitOfWork.LibraryRepository.LibraryExists(createLibraryDto.Name))
{
_directoryService = directoryService;
_logger = logger;
_mapper = mapper;
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
_eventHub = eventHub;
_libraryWatcher = libraryWatcher;
return BadRequest("Library name already exists. Please choose a unique name to the server.");
}
/// <summary>
/// Creates a new Library. Upon library creation, adds new library to all Admin accounts.
/// </summary>
/// <param name="createLibraryDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("create")]
public async Task<ActionResult> AddLibrary(CreateLibraryDto createLibraryDto)
var library = new Library
{
if (await _unitOfWork.LibraryRepository.LibraryExists(createLibraryDto.Name))
Name = createLibraryDto.Name,
Type = createLibraryDto.Type,
Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList()
};
_unitOfWork.LibraryRepository.Add(library);
var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList();
foreach (var admin in admins)
{
admin.Libraries ??= new List<Library>();
admin.Libraries.Add(library);
}
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
await _libraryWatcher.RestartWatching();
_taskScheduler.ScanLibrary(library.Id);
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
return Ok();
}
/// <summary>
/// Returns a list of directories for a given path. If path is empty, returns root drives.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("list")]
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string path)
{
if (string.IsNullOrEmpty(path))
{
return Ok(Directory.GetLogicalDrives().Select(d => new DirectoryDto()
{
return BadRequest("Library name already exists. Please choose a unique name to the server.");
Name = d,
FullPath = d
}));
}
if (!Directory.Exists(path)) return BadRequest("This is not a valid path");
return Ok(_directoryService.ListDirectory(path));
}
[HttpGet]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
}
[HttpGet("jump-bar")]
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("grant-access")]
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
if (user == null) return BadRequest("Could not validate user");
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
foreach (var library in allLibraries)
{
library.AppUsers ??= new List<AppUser>();
var libraryContainsUser = library.AppUsers.Any(u => u.UserName == user.UserName);
var libraryIsSelected = updateLibraryForUserDto.SelectedLibraries.Any(l => l.Id == library.Id);
if (libraryContainsUser && !libraryIsSelected)
{
// Remove
library.AppUsers.Remove(user);
}
else if (!libraryContainsUser && libraryIsSelected)
{
library.AppUsers.Add(user);
}
var library = new Library
{
Name = createLibraryDto.Name,
Type = createLibraryDto.Type,
Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList()
};
}
_unitOfWork.LibraryRepository.Add(library);
if (!_unitOfWork.HasChanges())
{
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
return Ok(_mapper.Map<MemberDto>(user));
}
var admins = (await _unitOfWork.UserRepository.GetAdminUsersAsync()).ToList();
foreach (var admin in admins)
if (await _unitOfWork.CommitAsync())
{
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
return Ok(_mapper.Map<MemberDto>(user));
}
return BadRequest("There was a critical issue. Please try again.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")]
public ActionResult Scan(int libraryId, bool force = false)
{
_taskScheduler.ScanLibrary(libraryId, force);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshMetadata(int libraryId, bool force = true)
{
_taskScheduler.RefreshMetadata(libraryId, force);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("analyze")]
public ActionResult Analyze(int libraryId)
{
_taskScheduler.AnalyzeFilesForLibrary(libraryId, true);
return Ok();
}
[HttpGet("libraries")]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
}
/// <summary>
/// Given a valid path, will invoke either a Scan Series or Scan Library. If the folder does not exist within Kavita, the request will be ignored
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("scan-folder")]
public async Task<ActionResult> ScanFolder(ScanFolderDto dto)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey);
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
// Validate user has Admin privileges
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
if (!isAdmin) return BadRequest("API key must belong to an admin");
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
var libraryFolder = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
.SelectMany(l => l.Folders)
.Distinct()
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
new List<string>() {dto.FolderPath});
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete")]
public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
{
var username = User.GetUsername();
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username);
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
var seriesIds = series.Select(x => x.Id).ToArray();
var chapterIds =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds);
try
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
{
admin.Libraries ??= new List<Library>();
admin.Libraries.Add(library);
// TODO: Figure out how to cancel a job
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
return BadRequest(
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
}
_unitOfWork.LibraryRepository.Delete(library);
await _unitOfWork.CommitAsync();
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
_logger.LogInformation("Created a new library: {LibraryName}", library.Name);
await _libraryWatcher.RestartWatching();
_taskScheduler.ScanLibrary(library.Id);
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(library.Id, "create"), false);
return Ok();
}
/// <summary>
/// Returns a list of directories for a given path. If path is empty, returns root drives.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("list")]
public ActionResult<IEnumerable<DirectoryDto>> GetDirectories(string path)
{
if (string.IsNullOrEmpty(path))
if (chapterIds.Any())
{
return Ok(Directory.GetLogicalDrives().Select(d => new DirectoryDto()
{
Name = d,
FullPath = d
}));
}
if (!Directory.Exists(path)) return BadRequest("This is not a valid path");
return Ok(_directoryService.ListDirectory(path));
}
[HttpGet]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
}
[HttpGet("jump-bar")]
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("grant-access")]
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
if (user == null) return BadRequest("Could not validate user");
var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
foreach (var library in allLibraries)
{
library.AppUsers ??= new List<AppUser>();
var libraryContainsUser = library.AppUsers.Any(u => u.UserName == user.UserName);
var libraryIsSelected = updateLibraryForUserDto.SelectedLibraries.Any(l => l.Id == library.Id);
if (libraryContainsUser && !libraryIsSelected)
{
// Remove
library.AppUsers.Remove(user);
}
else if (!libraryContainsUser && libraryIsSelected)
{
library.AppUsers.Add(user);
}
}
if (!_unitOfWork.HasChanges())
{
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
return Ok(_mapper.Map<MemberDto>(user));
}
if (await _unitOfWork.CommitAsync())
{
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
return Ok(_mapper.Map<MemberDto>(user));
}
return BadRequest("There was a critical issue. Please try again.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")]
public ActionResult Scan(int libraryId, bool force = false)
{
_taskScheduler.ScanLibrary(libraryId, force);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshMetadata(int libraryId, bool force = true)
{
_taskScheduler.RefreshMetadata(libraryId, force);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("analyze")]
public ActionResult Analyze(int libraryId)
{
_taskScheduler.AnalyzeFilesForLibrary(libraryId, true);
return Ok();
}
[HttpGet("libraries")]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
}
/// <summary>
/// Given a valid path, will invoke either a Scan Series or Scan Library. If the folder does not exist within Kavita, the request will be ignored
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("scan-folder")]
public async Task<ActionResult> ScanFolder(ScanFolderDto dto)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(dto.ApiKey);
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
// Validate user has Admin privileges
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
if (!isAdmin) return BadRequest("API key must belong to an admin");
if (dto.FolderPath.Contains("..")) return BadRequest("Invalid Path");
dto.FolderPath = Services.Tasks.Scanner.Parser.Parser.NormalizePath(dto.FolderPath);
var libraryFolder = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
.SelectMany(l => l.Folders)
.Distinct()
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
new List<string>() {dto.FolderPath});
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete")]
public async Task<ActionResult<bool>> DeleteLibrary(int libraryId)
{
var username = User.GetUsername();
_logger.LogInformation("Library {LibraryId} is being deleted by {UserName}", libraryId, username);
var series = await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(libraryId);
var seriesIds = series.Select(x => x.Id).ToArray();
var chapterIds =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds);
try
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
{
// TODO: Figure out how to cancel a job
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
return BadRequest(
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
}
_unitOfWork.LibraryRepository.Delete(library);
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
await _unitOfWork.CommitAsync();
if (chapterIds.Any())
{
await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
await _unitOfWork.CommitAsync();
_taskScheduler.CleanupChapters(chapterIds);
}
await _libraryWatcher.RestartWatching();
foreach (var seriesId in seriesIds)
{
await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,
MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, libraryId), false);
}
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
return Ok(true);
_taskScheduler.CleanupChapters(chapterIds);
}
catch (Exception ex)
await _libraryWatcher.RestartWatching();
foreach (var seriesId in seriesIds)
{
_logger.LogError(ex, "There was a critical error trying to delete the library");
await _unitOfWork.RollbackAsync();
return Ok(false);
}
}
/// <summary>
/// Updates an existing Library with new name, folders, and/or type.
/// </summary>
/// <remarks>Any folder or type change will invoke a scan.</remarks>
/// <param name="libraryForUserDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update")]
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id, LibraryIncludes.Folders);
var originalFolders = library.Folders.Select(x => x.Path).ToList();
library.Name = libraryForUserDto.Name;
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
var typeUpdate = library.Type != libraryForUserDto.Type;
library.Type = libraryForUserDto.Type;
_unitOfWork.LibraryRepository.Update(library);
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
if (originalFolders.Count != libraryForUserDto.Folders.Count() || typeUpdate)
{
await _libraryWatcher.RestartWatching();
_taskScheduler.ScanLibrary(library.Id);
await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved,
MessageFactory.SeriesRemovedEvent(seriesId, string.Empty, libraryId), false);
}
return Ok();
await _eventHub.SendMessageAsync(MessageFactory.LibraryModified,
MessageFactory.LibraryModifiedEvent(libraryId, "delete"), false);
return Ok(true);
}
[HttpGet("search")]
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
catch (Exception ex)
{
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
// Get libraries user has access to
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
return Ok(series);
}
[HttpGet("type")]
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId));
_logger.LogError(ex, "There was a critical error trying to delete the library");
await _unitOfWork.RollbackAsync();
return Ok(false);
}
}
/// <summary>
/// Updates an existing Library with new name, folders, and/or type.
/// </summary>
/// <remarks>Any folder or type change will invoke a scan.</remarks>
/// <param name="libraryForUserDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update")]
public async Task<ActionResult> UpdateLibrary(UpdateLibraryDto libraryForUserDto)
{
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryForUserDto.Id, LibraryIncludes.Folders);
var originalFolders = library.Folders.Select(x => x.Path).ToList();
library.Name = libraryForUserDto.Name;
library.Folders = libraryForUserDto.Folders.Select(s => new FolderPath() {Path = s}).ToList();
var typeUpdate = library.Type != libraryForUserDto.Type;
library.Type = libraryForUserDto.Type;
_unitOfWork.LibraryRepository.Update(library);
if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
if (originalFolders.Count != libraryForUserDto.Folders.Count() || typeUpdate)
{
await _libraryWatcher.RestartWatching();
_taskScheduler.ScanLibrary(library.Id);
}
return Ok();
}
[HttpGet("search")]
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
{
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
// Get libraries user has access to
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
return Ok(series);
}
[HttpGet("type")]
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(libraryId));
}
}

View file

@ -7,44 +7,43 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers
namespace API.Controllers;
public class PluginController : BaseApiController
{
public class PluginController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly ITokenService _tokenService;
private readonly ILogger<PluginController> _logger;
public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
{
private readonly IUnitOfWork _unitOfWork;
private readonly ITokenService _tokenService;
private readonly ILogger<PluginController> _logger;
_unitOfWork = unitOfWork;
_tokenService = tokenService;
_logger = logger;
}
public PluginController(IUnitOfWork unitOfWork, ITokenService tokenService, ILogger<PluginController> logger)
/// <summary>
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
/// </summary>
/// <remarks>This API is not fully built out and may require more information in later releases</remarks>
/// <param name="apiKey">API key which will be used to authenticate and return a valid user token back</param>
/// <param name="pluginName">Name of the Plugin</param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("authenticate")]
public async Task<ActionResult<UserDto>> Authenticate([Required] string apiKey, [Required] string pluginName)
{
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
// Should log into access table so we can tell the user
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId <= 0) return Unauthorized();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user.UserName, userId);
return new UserDto
{
_unitOfWork = unitOfWork;
_tokenService = tokenService;
_logger = logger;
}
/// <summary>
/// Authenticate with the Server given an apiKey. This will log you in by returning the user object and the JWT token.
/// </summary>
/// <remarks>This API is not fully built out and may require more information in later releases</remarks>
/// <param name="apiKey">API key which will be used to authenticate and return a valid user token back</param>
/// <param name="pluginName">Name of the Plugin</param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost("authenticate")]
public async Task<ActionResult<UserDto>> Authenticate([Required] string apiKey, [Required] string pluginName)
{
// NOTE: In order to log information about plugins, we need some Plugin Description information for each request
// Should log into access table so we can tell the user
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId <= 0) return Unauthorized();
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user.UserName, userId);
return new UserDto
{
Username = user.UserName,
Token = await _tokenService.CreateToken(user),
ApiKey = user.ApiKey,
};
}
Username = user.UserName,
Token = await _tokenService.CreateToken(user),
ApiKey = user.ApiKey,
};
}
}

File diff suppressed because it is too large Load diff

View file

@ -13,483 +13,482 @@ using API.SignalR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
namespace API.Controllers;
[Authorize]
public class ReadingListController : BaseApiController
{
[Authorize]
public class ReadingListController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly IReadingListService _readingListService;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
{
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
private readonly IReadingListService _readingListService;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
_unitOfWork = unitOfWork;
_eventHub = eventHub;
_readingListService = readingListService;
}
public ReadingListController(IUnitOfWork unitOfWork, IEventHub eventHub, IReadingListService readingListService)
/// <summary>
/// Fetches a single Reading List
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
}
/// <summary>
/// Returns reading lists (paginated) for a given user.
/// </summary>
/// <param name="includePromoted">Defaults to true</param>
/// <returns></returns>
[HttpPost("lists")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, [FromQuery] bool includePromoted = true)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
userParams);
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
return Ok(items);
}
/// <summary>
/// Returns all Reading Lists the user has access to that have a series within it.
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("lists-for-series")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true);
return Ok(items);
}
/// <summary>
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
/// </summary>
/// <remarks>This call is expensive</remarks>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet("items")]
public async Task<ActionResult<IEnumerable<ReadingListItemDto>>> GetListForUser(int readingListId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
return Ok(items);
}
/// <summary>
/// Updates an items position
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-position")]
public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
{
// Make sure UI buffers events
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
_readingListService = readingListService;
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
/// <summary>
/// Fetches a single Reading List
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated");
return BadRequest("Couldn't update position");
}
/// <summary>
/// Deletes a list item from the list. Will reorder all item positions afterwards
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("delete-item")]
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, userId));
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
/// <summary>
/// Returns reading lists (paginated) for a given user.
/// </summary>
/// <param name="includePromoted">Defaults to true</param>
/// <returns></returns>
[HttpPost("lists")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, [FromQuery] bool includePromoted = true)
if (await _readingListService.DeleteReadingListItem(dto))
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
userParams);
Response.AddPaginationHeader(items.CurrentPage, items.PageSize, items.TotalCount, items.TotalPages);
return Ok(items);
return Ok("Updated");
}
/// <summary>
/// Returns all Reading Lists the user has access to that have a series within it.
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("lists-for-series")]
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForSeries(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForSeriesAndUserAsync(userId, seriesId, true);
return BadRequest("Couldn't delete item");
}
return Ok(items);
/// <summary>
/// Removes all entries that are fully read from the reading list
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpPost("remove-read")]
public async Task<ActionResult> DeleteReadFromList([FromQuery] int readingListId)
{
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
/// <summary>
/// Fetches all reading list items for a given list including rich metadata around series, volume, chapters, and progress
/// </summary>
/// <remarks>This call is expensive</remarks>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpGet("items")]
public async Task<ActionResult<IEnumerable<ReadingListItemDto>>> GetListForUser(int readingListId)
if (await _readingListService.RemoveFullyReadItems(readingListId, user))
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var items = await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId);
return Ok(items);
return Ok("Updated");
}
return BadRequest("Could not remove read items");
}
/// <summary>
/// Updates an items position
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-position")]
public async Task<ActionResult> UpdateListItemPosition(UpdateReadingListPosition dto)
/// <summary>
/// Deletes a reading list
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpDelete]
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
{
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
if (user == null)
{
// Make sure UI buffers events
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
if (await _readingListService.UpdateReadingListItemPosition(dto)) return Ok("Updated");
return BadRequest("Couldn't update position");
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
/// <summary>
/// Deletes a list item from the list. Will reorder all item positions afterwards
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("delete-item")]
public async Task<ActionResult> DeleteListItem(UpdateReadingListPosition dto)
if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was deleted");
return BadRequest("There was an issue deleting reading list");
}
/// <summary>
/// Creates a new List with a unique title. Returns the new ReadingList back
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("create")]
public async Task<ActionResult<ReadingListDto>> CreateList(CreateReadingListDto dto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.ReadingListsWithItems);
// When creating, we need to make sure Title is unique
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
if (hasExisting)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
if (await _readingListService.DeleteReadingListItem(dto))
{
return Ok("Updated");
}
return BadRequest("Couldn't delete item");
return BadRequest("A list of this name already exists");
}
/// <summary>
/// Removes all entries that are fully read from the reading list
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpPost("remove-read")]
public async Task<ActionResult> DeleteReadFromList([FromQuery] int readingListId)
var readingList = DbFactory.ReadingList(dto.Title, string.Empty, false);
user.ReadingLists.Add(readingList);
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
await _unitOfWork.CommitAsync();
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title));
}
/// <summary>
/// Update the properties (title, summary) of a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update")]
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
{
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
if (readingList == null) return BadRequest("List does not exist");
var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername());
if (user == null)
{
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
if (await _readingListService.RemoveFullyReadItems(readingListId, user))
{
return Ok("Updated");
}
return BadRequest("Could not remove read items");
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
/// <summary>
/// Deletes a reading list
/// </summary>
/// <param name="readingListId"></param>
/// <returns></returns>
[HttpDelete]
public async Task<ActionResult> DeleteList([FromQuery] int readingListId)
if (!string.IsNullOrEmpty(dto.Title))
{
var user = await _readingListService.UserHasReadingListAccess(readingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
if (await _readingListService.DeleteReadingList(readingListId, user)) return Ok("List was deleted");
return BadRequest("There was an issue deleting reading list");
readingList.Title = dto.Title; // Should I check if this is unique?
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
}
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Summary = dto.Summary;
}
/// <summary>
/// Creates a new List with a unique title. Returns the new ReadingList back
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("create")]
public async Task<ActionResult<ReadingListDto>> CreateList(CreateReadingListDto dto)
readingList.Promoted = dto.Promoted;
readingList.CoverImageLocked = dto.CoverImageLocked;
if (!dto.CoverImageLocked)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.ReadingListsWithItems);
// When creating, we need to make sure Title is unique
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
if (hasExisting)
{
return BadRequest("A list of this name already exists");
}
var readingList = DbFactory.ReadingList(dto.Title, string.Empty, false);
user.ReadingLists.Add(readingList);
if (!_unitOfWork.HasChanges()) return BadRequest("There was a problem creating list");
await _unitOfWork.CommitAsync();
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByTitleAsync(user.Id, dto.Title));
}
/// <summary>
/// Update the properties (title, summary) of a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update")]
public async Task<ActionResult> UpdateList(UpdateReadingListDto dto)
{
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
if (readingList == null) return BadRequest("List does not exist");
var user = await _readingListService.UserHasReadingListAccess(readingList.Id, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Title = dto.Title; // Should I check if this is unique?
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
}
if (!string.IsNullOrEmpty(dto.Title))
{
readingList.Summary = dto.Summary;
}
readingList.Promoted = dto.Promoted;
readingList.CoverImageLocked = dto.CoverImageLocked;
if (!dto.CoverImageLocked)
{
readingList.CoverImageLocked = false;
readingList.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
_unitOfWork.ReadingListRepository.Update(readingList);
}
readingList.CoverImageLocked = false;
readingList.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
_unitOfWork.ReadingListRepository.Update(readingList);
}
if (await _unitOfWork.CommitAsync())
_unitOfWork.ReadingListRepository.Update(readingList);
if (await _unitOfWork.CommitAsync())
{
return Ok("Updated");
}
return BadRequest("Could not update reading list");
}
/// <summary>
/// Adds all chapters from a Series to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-series")]
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIdsForSeries =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
return BadRequest("Could not update reading list");
}
catch
{
await _unitOfWork.RollbackAsync();
}
/// <summary>
/// Adds all chapters from a Series to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-series")]
public async Task<ActionResult> UpdateListBySeries(UpdateReadingListBySeriesDto dto)
return Ok("Nothing to do");
}
/// <summary>
/// Adds all chapters from a list of volumes and chapters to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-multiple")]
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
foreach (var chapterId in dto.ChapterIds)
{
chapterIds.Add(chapterId);
}
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIdsForSeries =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
return Ok("Nothing to do");
}
/// <summary>
/// Adds all chapters from a list of series to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-multiple-series")]
public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
foreach (var seriesId in ids.Keys)
{
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
if (await _readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
/// <summary>
/// Adds all chapters from a list of volumes and chapters to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-multiple")]
public async Task<ActionResult> UpdateListByMultiple(UpdateReadingListByMultipleDto dto)
try
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
if (_unitOfWork.HasChanges())
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIds = await _unitOfWork.VolumeRepository.GetChapterIdsByVolumeIds(dto.VolumeIds);
foreach (var chapterId in dto.ChapterIds)
{
chapterIds.Add(chapterId);
}
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIds, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
/// <summary>
/// Adds all chapters from a list of series to a reading list
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("update-by-multiple-series")]
public async Task<ActionResult> UpdateListByMultipleSeries(UpdateReadingListByMultipleSeriesDto dto)
catch
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var ids = await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(dto.SeriesIds.ToArray());
foreach (var seriesId in ids.Keys)
{
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(seriesId, ids[seriesId], readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
await _unitOfWork.RollbackAsync();
}
[HttpPost("update-by-volume")]
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
return Ok("Nothing to do");
}
[HttpPost("update-by-volume")]
public async Task<ActionResult> UpdateListByVolume(UpdateReadingListByVolumeDto dto)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
var chapterIdsForVolume =
(await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
[HttpPost("update-by-chapter")]
public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto dto)
var chapterIdsForVolume =
(await _unitOfWork.ChapterRepository.GetChaptersAsync(dto.VolumeId)).Select(c => c.Id).ToList();
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForVolume, readingList))
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
_unitOfWork.ReadingListRepository.Update(readingList);
}
/// <summary>
/// Returns the next chapter within the reading list
/// </summary>
/// <param name="currentChapterId"></param>
/// <param name="readingListId"></param>
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
[HttpGet("next-chapter")]
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
try
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
var index = items.IndexOf(readingListItem) + 1;
if (items.Count > index)
if (_unitOfWork.HasChanges())
{
return items[index].ChapterId;
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
return Ok(-1);
}
/// <summary>
/// Returns the prev chapter within the reading list
/// </summary>
/// <param name="currentChapterId"></param>
/// <param name="readingListId"></param>
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
[HttpGet("prev-chapter")]
public async Task<ActionResult<int>> GetPrevChapter(int currentChapterId, int readingListId)
catch
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
var index = items.IndexOf(readingListItem) - 1;
if (0 <= index)
{
return items[index].ChapterId;
}
return Ok(-1);
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
[HttpPost("update-by-chapter")]
public async Task<ActionResult> UpdateListByChapter(UpdateReadingListByChapterDto dto)
{
var user = await _readingListService.UserHasReadingListAccess(dto.ReadingListId, User.GetUsername());
if (user == null)
{
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
}
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
if (readingList == null) return BadRequest("Reading List does not exist");
// If there are adds, tell tracking this has been modified
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, new List<int>() { dto.ChapterId }, readingList))
{
_unitOfWork.ReadingListRepository.Update(readingList);
}
try
{
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
return Ok("Updated");
}
}
catch
{
await _unitOfWork.RollbackAsync();
}
return Ok("Nothing to do");
}
/// <summary>
/// Returns the next chapter within the reading list
/// </summary>
/// <param name="currentChapterId"></param>
/// <param name="readingListId"></param>
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
[HttpGet("next-chapter")]
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
var index = items.IndexOf(readingListItem) + 1;
if (items.Count > index)
{
return items[index].ChapterId;
}
return Ok(-1);
}
/// <summary>
/// Returns the prev chapter within the reading list
/// </summary>
/// <param name="currentChapterId"></param>
/// <param name="readingListId"></param>
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
[HttpGet("prev-chapter")]
public async Task<ActionResult<int>> GetPrevChapter(int currentChapterId, int readingListId)
{
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(readingListId)).ToList();
var readingListItem = items.SingleOrDefault(rl => rl.ChapterId == currentChapterId);
if (readingListItem == null) return BadRequest("Id does not exist");
var index = items.IndexOf(readingListItem) - 1;
if (0 <= index)
{
return items[index].ChapterId;
}
return Ok(-1);
}
}

View file

@ -19,480 +19,479 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers
namespace API.Controllers;
public class SeriesController : BaseApiController
{
public class SeriesController : BaseApiController
private readonly ILogger<SeriesController> _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IUnitOfWork _unitOfWork;
private readonly ISeriesService _seriesService;
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, ISeriesService seriesService)
{
private readonly ILogger<SeriesController> _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IUnitOfWork _unitOfWork;
private readonly ISeriesService _seriesService;
_logger = logger;
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
_seriesService = seriesService;
}
[HttpPost]
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
public SeriesController(ILogger<SeriesController> logger, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, ISeriesService seriesService)
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for library");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
}
/// <summary>
/// Fetches a Series for a given Id
/// </summary>
/// <param name="seriesId">Series Id to fetch details for</param>
/// <returns></returns>
/// <exception cref="KavitaException">Throws an exception if the series Id does exist</exception>
[HttpGet("{seriesId:int}")]
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
try
{
_logger = logger;
_taskScheduler = taskScheduler;
_unitOfWork = unitOfWork;
_seriesService = seriesService;
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId));
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue fetching {SeriesId}", seriesId);
throw new KavitaException("This series does not exist");
}
[HttpPost]
public async Task<ActionResult<IEnumerable<Series>>> GetSeriesForLibrary(int libraryId, [FromQuery] UserParams userParams, [FromBody] FilterDto filterDto)
}
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("{seriesId}")]
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
{
var username = User.GetUsername();
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
return Ok(await _seriesService.DeleteMultipleSeries(new[] {seriesId}));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("delete-multiple")]
public async Task<ActionResult> DeleteMultipleSeries(DeleteSeriesDto dto)
{
var username = User.GetUsername();
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", dto.SeriesIds, username);
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
return BadRequest("There was an issue deleting the series requested");
}
/// <summary>
/// Returns All volumes for a series with progress information and Chapters
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("volumes")]
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId));
}
[HttpGet("volume")]
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));
}
[HttpGet("chapter")]
public async Task<ActionResult<ChapterDto>> GetChapter(int chapterId)
{
return Ok(await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId));
}
[HttpGet("chapter-metadata")]
public async Task<ActionResult<ChapterDto>> GetChapterMetadata(int chapterId)
{
return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId));
}
[HttpPost("update-rating")]
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
if (!await _seriesService.UpdateRating(user, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
return Ok();
}
[HttpPost("update")]
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
{
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
if (series == null) return BadRequest("Series does not exist");
var seriesExists =
await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId,
series.Format);
if (series.Name != updateSeries.Name && seriesExists)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for library");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
}
/// <summary>
/// Fetches a Series for a given Id
/// </summary>
/// <param name="seriesId">Series Id to fetch details for</param>
/// <returns></returns>
/// <exception cref="KavitaException">Throws an exception if the series Id does exist</exception>
[HttpGet("{seriesId:int}")]
public async Task<ActionResult<SeriesDto>> GetSeries(int seriesId)
series.Name = updateSeries.Name.Trim();
series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name);
if (!string.IsNullOrEmpty(updateSeries.SortName.Trim()))
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
try
series.SortName = updateSeries.SortName.Trim();
}
series.LocalizedName = updateSeries.LocalizedName.Trim();
series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName);
series.NameLocked = updateSeries.NameLocked;
series.SortNameLocked = updateSeries.SortNameLocked;
series.LocalizedNameLocked = updateSeries.LocalizedNameLocked;
var needsRefreshMetadata = false;
// This is when you hit Reset
if (series.CoverImageLocked && !updateSeries.CoverImageLocked)
{
// Trigger a refresh when we are moving from a locked image to a non-locked
needsRefreshMetadata = true;
series.CoverImage = string.Empty;
series.CoverImageLocked = updateSeries.CoverImageLocked;
}
_unitOfWork.SeriesRepository.Update(series);
if (await _unitOfWork.CommitAsync())
{
if (needsRefreshMetadata)
{
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId));
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue fetching {SeriesId}", seriesId);
throw new KavitaException("This series does not exist");
}
}
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("{seriesId}")]
public async Task<ActionResult<bool>> DeleteSeries(int seriesId)
{
var username = User.GetUsername();
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
return Ok(await _seriesService.DeleteMultipleSeries(new[] {seriesId}));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("delete-multiple")]
public async Task<ActionResult> DeleteMultipleSeries(DeleteSeriesDto dto)
{
var username = User.GetUsername();
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", dto.SeriesIds, username);
if (await _seriesService.DeleteMultipleSeries(dto.SeriesIds)) return Ok();
return BadRequest("There was an issue deleting the series requested");
}
/// <summary>
/// Returns All volumes for a series with progress information and Chapters
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("volumes")]
public async Task<ActionResult<IEnumerable<VolumeDto>>> GetVolumes(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId));
}
[HttpGet("volume")]
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, userId));
}
[HttpGet("chapter")]
public async Task<ActionResult<ChapterDto>> GetChapter(int chapterId)
{
return Ok(await _unitOfWork.ChapterRepository.GetChapterDtoAsync(chapterId));
}
[HttpGet("chapter-metadata")]
public async Task<ActionResult<ChapterDto>> GetChapterMetadata(int chapterId)
{
return Ok(await _unitOfWork.ChapterRepository.GetChapterMetadataDtoAsync(chapterId));
}
[HttpPost("update-rating")]
public async Task<ActionResult> UpdateSeriesRating(UpdateSeriesRatingDto updateSeriesRatingDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Ratings);
if (!await _seriesService.UpdateRating(user, updateSeriesRatingDto)) return BadRequest("There was a critical error.");
return Ok();
}
[HttpPost("update")]
public async Task<ActionResult> UpdateSeries(UpdateSeriesDto updateSeries)
return BadRequest("There was an error with updating the series");
}
[HttpPost("recently-added")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
}
[HttpPost("recently-updated-series")]
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
}
[HttpPost("all")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
}
/// <summary>
/// Fetches series that are on deck aka have progress on them.
/// </summary>
/// <param name="filterDto"></param>
/// <param name="userParams"></param>
/// <param name="libraryId">Default of 0 meaning all libraries</param>
/// <returns></returns>
[HttpPost("on-deck")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
return Ok(pagedList);
}
/// <summary>
/// Runs a Cover Image Generation task
/// </summary>
/// <param name="refreshSeriesDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
/// <summary>
/// Scan a series and force each file to be updated. This should be invoked via the User, hence why we force.
/// </summary>
/// <param name="refreshSeriesDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")]
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
/// <summary>
/// Run a file analysis on the series.
/// </summary>
/// <param name="refreshSeriesDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("analyze")]
public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
/// <summary>
/// Returns metadata for a given series
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("metadata")]
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
{
var metadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
return Ok(metadata);
}
/// <summary>
/// Update series metadata
/// </summary>
/// <param name="updateSeriesMetadataDto"></param>
/// <returns></returns>
[HttpPost("metadata")]
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
{
if (await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto))
{
_logger.LogInformation("{UserName} is updating Series {SeriesName}", User.GetUsername(), updateSeries.Name);
return Ok("Successfully updated");
}
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(updateSeries.Id);
return BadRequest("Could not update metadata");
}
if (series == null) return BadRequest("Series does not exist");
/// <summary>
/// Returns all Series grouped by the passed Collection Id with Pagination.
/// </summary>
/// <param name="collectionId">Collection Id to pull series from</param>
/// <param name="userParams">Pagination information</param>
/// <returns></returns>
[HttpGet("series-by-collection")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
var seriesExists =
await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId,
series.Format);
if (series.Name != updateSeries.Name && seriesExists)
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for collection");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
}
/// <summary>
/// Fetches Series for a set of Ids. This will check User for permission access and filter out any Ids that don't exist or
/// the user does not have access to.
/// </summary>
/// <returns></returns>
[HttpPost("series-by-ids")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
{
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
}
/// <summary>
/// Get the age rating for the <see cref="AgeRating"/> enum value
/// </summary>
/// <param name="ageRating"></param>
/// <returns></returns>
[HttpGet("age-rating")]
public ActionResult<string> GetAgeRating(int ageRating)
{
var val = (AgeRating) ageRating;
return Ok(val.ToDescription());
}
/// <summary>
/// Get a special DTO for Series Detail page.
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
/// <remarks>Do not rely on this API externally. May change without hesitation. </remarks>
[HttpGet("series-detail")]
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return await _seriesService.GetSeriesDetail(seriesId, userId);
}
/// <summary>
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
/// then null is returned
/// </summary>
/// <param name="mangaFileId"></param>
/// <returns></returns>
[HttpGet("series-for-mangafile")]
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
}
/// <summary>
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
/// then null is returned
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("series-for-chapter")]
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
}
/// <summary>
/// Fetches the related series for a given series
/// </summary>
/// <param name="seriesId"></param>
/// <param name="relation">Type of Relationship to pull back</param>
/// <returns></returns>
[HttpGet("related")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
{
// Send back a custom DTO with each type or maybe sorted in some way
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
}
/// <summary>
/// Returns all related series against the passed series Id
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("all-related")]
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
}
/// <summary>
/// Update the relations attached to the Series. Does not generate associated Sequel/Prequel pairs on target series.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy="RequireAdminRole")]
[HttpPost("update-related")]
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation);
UpdateRelationForKind(dto.Characters, series.Relations.Where(r => r.RelationKind == RelationKind.Character).ToList(), series, RelationKind.Character);
UpdateRelationForKind(dto.Contains, series.Relations.Where(r => r.RelationKind == RelationKind.Contains).ToList(), series, RelationKind.Contains);
UpdateRelationForKind(dto.Others, series.Relations.Where(r => r.RelationKind == RelationKind.Other).ToList(), series, RelationKind.Other);
UpdateRelationForKind(dto.SideStories, series.Relations.Where(r => r.RelationKind == RelationKind.SideStory).ToList(), series, RelationKind.SideStory);
UpdateRelationForKind(dto.SpinOffs, series.Relations.Where(r => r.RelationKind == RelationKind.SpinOff).ToList(), series, RelationKind.SpinOff);
UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
if (!_unitOfWork.HasChanges()) return Ok();
if (await _unitOfWork.CommitAsync()) return Ok();
return BadRequest("There was an issue updating relationships");
}
// TODO: Move this to a Service and Unit Test it
private void UpdateRelationForKind(ICollection<int> dtoTargetSeriesIds, IEnumerable<SeriesRelation> adaptations, Series series, RelationKind kind)
{
foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
{
// If the seriesId isn't in dto, it means we've removed or reclassified
series.Relations.Remove(adaptation);
}
// At this point, we only have things to add
foreach (var targetSeriesId in dtoTargetSeriesIds)
{
// This ensures we don't allow any duplicates to be added
if (series.Relations.SingleOrDefault(r =>
r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
null) continue;
series.Relations.Add(new SeriesRelation()
{
return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library.");
}
series.Name = updateSeries.Name.Trim();
series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name);
if (!string.IsNullOrEmpty(updateSeries.SortName.Trim()))
{
series.SortName = updateSeries.SortName.Trim();
}
series.LocalizedName = updateSeries.LocalizedName.Trim();
series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName);
series.NameLocked = updateSeries.NameLocked;
series.SortNameLocked = updateSeries.SortNameLocked;
series.LocalizedNameLocked = updateSeries.LocalizedNameLocked;
var needsRefreshMetadata = false;
// This is when you hit Reset
if (series.CoverImageLocked && !updateSeries.CoverImageLocked)
{
// Trigger a refresh when we are moving from a locked image to a non-locked
needsRefreshMetadata = true;
series.CoverImage = string.Empty;
series.CoverImageLocked = updateSeries.CoverImageLocked;
}
Series = series,
SeriesId = series.Id,
TargetSeriesId = targetSeriesId,
RelationKind = kind
});
_unitOfWork.SeriesRepository.Update(series);
if (await _unitOfWork.CommitAsync())
{
if (needsRefreshMetadata)
{
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id);
}
return Ok();
}
return BadRequest("There was an error with updating the series");
}
[HttpPost("recently-added")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetRecentlyAdded(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
}
[HttpPost("recently-updated-series")]
public async Task<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetRecentlyUpdatedSeries(userId));
}
[HttpPost("all")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeries(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId, userId, userParams, filterDto);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
}
/// <summary>
/// Fetches series that are on deck aka have progress on them.
/// </summary>
/// <param name="filterDto"></param>
/// <param name="userParams"></param>
/// <param name="libraryId">Default of 0 meaning all libraries</param>
/// <returns></returns>
[HttpPost("on-deck")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
return Ok(pagedList);
}
/// <summary>
/// Runs a Cover Image Generation task
/// </summary>
/// <param name="refreshSeriesDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshSeriesMetadata(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.RefreshSeriesMetadata(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
/// <summary>
/// Scan a series and force each file to be updated. This should be invoked via the User, hence why we force.
/// </summary>
/// <param name="refreshSeriesDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")]
public ActionResult ScanSeries(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.ScanSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
/// <summary>
/// Run a file analysis on the series.
/// </summary>
/// <param name="refreshSeriesDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("analyze")]
public ActionResult AnalyzeSeries(RefreshSeriesDto refreshSeriesDto)
{
_taskScheduler.AnalyzeFilesForSeries(refreshSeriesDto.LibraryId, refreshSeriesDto.SeriesId, refreshSeriesDto.ForceUpdate);
return Ok();
}
/// <summary>
/// Returns metadata for a given series
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("metadata")]
public async Task<ActionResult<SeriesMetadataDto>> GetSeriesMetadata(int seriesId)
{
var metadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
return Ok(metadata);
}
/// <summary>
/// Update series metadata
/// </summary>
/// <param name="updateSeriesMetadataDto"></param>
/// <returns></returns>
[HttpPost("metadata")]
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
{
if (await _seriesService.UpdateSeriesMetadata(updateSeriesMetadataDto))
{
return Ok("Successfully updated");
}
return BadRequest("Could not update metadata");
}
/// <summary>
/// Returns all Series grouped by the passed Collection Id with Pagination.
/// </summary>
/// <param name="collectionId">Collection Id to pull series from</param>
/// <param name="userParams">Pagination information</param>
/// <returns></returns>
[HttpGet("series-by-collection")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetSeriesByCollectionTag(int collectionId, [FromQuery] UserParams userParams)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var series =
await _unitOfWork.SeriesRepository.GetSeriesDtoForCollectionAsync(collectionId, userId, userParams);
// Apply progress/rating information (I can't work out how to do this in initial query)
if (series == null) return BadRequest("Could not get series for collection");
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, series);
Response.AddPaginationHeader(series.CurrentPage, series.PageSize, series.TotalCount, series.TotalPages);
return Ok(series);
}
/// <summary>
/// Fetches Series for a set of Ids. This will check User for permission access and filter out any Ids that don't exist or
/// the user does not have access to.
/// </summary>
/// <returns></returns>
[HttpPost("series-by-ids")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetAllSeriesById(SeriesByIdsDto dto)
{
if (dto.SeriesIds == null) return BadRequest("Must pass seriesIds");
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesDtoForIdsAsync(dto.SeriesIds, userId));
}
/// <summary>
/// Get the age rating for the <see cref="AgeRating"/> enum value
/// </summary>
/// <param name="ageRating"></param>
/// <returns></returns>
[HttpGet("age-rating")]
public ActionResult<string> GetAgeRating(int ageRating)
{
var val = (AgeRating) ageRating;
return Ok(val.ToDescription());
}
/// <summary>
/// Get a special DTO for Series Detail page.
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
/// <remarks>Do not rely on this API externally. May change without hesitation. </remarks>
[HttpGet("series-detail")]
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return await _seriesService.GetSeriesDetail(seriesId, userId);
}
/// <summary>
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
/// then null is returned
/// </summary>
/// <param name="mangaFileId"></param>
/// <returns></returns>
[HttpGet("series-for-mangafile")]
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
}
/// <summary>
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
/// then null is returned
/// </summary>
/// <param name="chapterId"></param>
/// <returns></returns>
[HttpGet("series-for-chapter")]
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
}
/// <summary>
/// Fetches the related series for a given series
/// </summary>
/// <param name="seriesId"></param>
/// <param name="relation">Type of Relationship to pull back</param>
/// <returns></returns>
[HttpGet("related")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetRelatedSeries(int seriesId, RelationKind relation)
{
// Send back a custom DTO with each type or maybe sorted in some way
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForRelationKind(userId, seriesId, relation));
}
/// <summary>
/// Returns all related series against the passed series Id
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
[HttpGet("all-related")]
public async Task<ActionResult<RelatedSeriesDto>> GetAllRelatedSeries(int seriesId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
return Ok(await _unitOfWork.SeriesRepository.GetRelatedSeries(userId, seriesId));
}
/// <summary>
/// Update the relations attached to the Series. Does not generate associated Sequel/Prequel pairs on target series.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy="RequireAdminRole")]
[HttpPost("update-related")]
public async Task<ActionResult> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
{
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
UpdateRelationForKind(dto.Adaptations, series.Relations.Where(r => r.RelationKind == RelationKind.Adaptation).ToList(), series, RelationKind.Adaptation);
UpdateRelationForKind(dto.Characters, series.Relations.Where(r => r.RelationKind == RelationKind.Character).ToList(), series, RelationKind.Character);
UpdateRelationForKind(dto.Contains, series.Relations.Where(r => r.RelationKind == RelationKind.Contains).ToList(), series, RelationKind.Contains);
UpdateRelationForKind(dto.Others, series.Relations.Where(r => r.RelationKind == RelationKind.Other).ToList(), series, RelationKind.Other);
UpdateRelationForKind(dto.SideStories, series.Relations.Where(r => r.RelationKind == RelationKind.SideStory).ToList(), series, RelationKind.SideStory);
UpdateRelationForKind(dto.SpinOffs, series.Relations.Where(r => r.RelationKind == RelationKind.SpinOff).ToList(), series, RelationKind.SpinOff);
UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
if (!_unitOfWork.HasChanges()) return Ok();
if (await _unitOfWork.CommitAsync()) return Ok();
return BadRequest("There was an issue updating relationships");
}
// TODO: Move this to a Service and Unit Test it
private void UpdateRelationForKind(ICollection<int> dtoTargetSeriesIds, IEnumerable<SeriesRelation> adaptations, Series series, RelationKind kind)
{
foreach (var adaptation in adaptations.Where(adaptation => !dtoTargetSeriesIds.Contains(adaptation.TargetSeriesId)))
{
// If the seriesId isn't in dto, it means we've removed or reclassified
series.Relations.Remove(adaptation);
}
// At this point, we only have things to add
foreach (var targetSeriesId in dtoTargetSeriesIds)
{
// This ensures we don't allow any duplicates to be added
if (series.Relations.SingleOrDefault(r =>
r.RelationKind == kind && r.TargetSeriesId == targetSeriesId) !=
null) continue;
series.Relations.Add(new SeriesRelation()
{
Series = series,
SeriesId = series.Id,
TargetSeriesId = targetSeriesId,
RelationKind = kind
});
_unitOfWork.SeriesRepository.Update(series);
}
}
}
}

View file

@ -8,6 +8,7 @@ using API.DTOs.Jobs;
using API.DTOs.Stats;
using API.DTOs.Update;
using API.Extensions;
using API.Logging;
using API.Services;
using API.Services.Tasks;
using Hangfire;
@ -20,143 +21,141 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TaskScheduler = System.Threading.Tasks.TaskScheduler;
namespace API.Controllers
namespace API.Controllers;
[Authorize(Policy = "RequireAdminRole")]
public class ServerController : BaseApiController
{
[Authorize(Policy = "RequireAdminRole")]
public class ServerController : BaseApiController
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly ILogger<ServerController> _logger;
private readonly IBackupService _backupService;
private readonly IArchiveService _archiveService;
private readonly IVersionUpdaterService _versionUpdaterService;
private readonly IStatsService _statsService;
private readonly ICleanupService _cleanupService;
private readonly IEmailService _emailService;
private readonly IBookmarkService _bookmarkService;
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService)
{
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly ILogger<ServerController> _logger;
private readonly IConfiguration _config;
private readonly IBackupService _backupService;
private readonly IArchiveService _archiveService;
private readonly IVersionUpdaterService _versionUpdaterService;
private readonly IStatsService _statsService;
private readonly ICleanupService _cleanupService;
private readonly IEmailService _emailService;
private readonly IBookmarkService _bookmarkService;
_applicationLifetime = applicationLifetime;
_logger = logger;
_backupService = backupService;
_archiveService = archiveService;
_versionUpdaterService = versionUpdaterService;
_statsService = statsService;
_cleanupService = cleanupService;
_emailService = emailService;
_bookmarkService = bookmarkService;
}
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService)
/// <summary>
/// Attempts to Restart the server. Does not work, will shutdown the instance.
/// </summary>
/// <returns></returns>
[HttpPost("restart")]
public ActionResult RestartServer()
{
_logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername());
_applicationLifetime.StopApplication();
return Ok();
}
/// <summary>
/// Performs an ad-hoc cleanup of Cache
/// </summary>
/// <returns></returns>
[HttpPost("clear-cache")]
public ActionResult ClearCache()
{
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
_cleanupService.CleanupCacheDirectory();
return Ok();
}
/// <summary>
/// Performs an ad-hoc backup of the Database
/// </summary>
/// <returns></returns>
[HttpPost("backup-db")]
public ActionResult BackupDatabase()
{
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
RecurringJob.Trigger("backup");
return Ok();
}
/// <summary>
/// Returns non-sensitive information about the current system
/// </summary>
/// <returns></returns>
[HttpGet("server-info")]
public async Task<ActionResult<ServerInfoDto>> GetVersion()
{
return Ok(await _statsService.GetServerInfo());
}
/// <summary>
/// Triggers the scheduling of the convert bookmarks job. Only one job will run at a time.
/// </summary>
/// <returns></returns>
[HttpPost("convert-bookmarks")]
public ActionResult ScheduleConvertBookmarks()
{
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllBookmarkToWebP());
return Ok();
}
[HttpGet("logs")]
public ActionResult GetLogs()
{
var files = _backupService.GetLogFiles();
try
{
_applicationLifetime = applicationLifetime;
_logger = logger;
_config = config;
_backupService = backupService;
_archiveService = archiveService;
_versionUpdaterService = versionUpdaterService;
_statsService = statsService;
_cleanupService = cleanupService;
_emailService = emailService;
_bookmarkService = bookmarkService;
var zipPath = _archiveService.CreateZipForDownload(files, "logs");
return PhysicalFile(zipPath, "application/zip", Path.GetFileName(zipPath), true);
}
/// <summary>
/// Attempts to Restart the server. Does not work, will shutdown the instance.
/// </summary>
/// <returns></returns>
[HttpPost("restart")]
public ActionResult RestartServer()
catch (KavitaException ex)
{
_logger.LogInformation("{UserName} is restarting server from admin dashboard", User.GetUsername());
_applicationLifetime.StopApplication();
return Ok();
return BadRequest(ex.Message);
}
}
/// <summary>
/// Performs an ad-hoc cleanup of Cache
/// </summary>
/// <returns></returns>
[HttpPost("clear-cache")]
public ActionResult ClearCache()
{
_logger.LogInformation("{UserName} is clearing cache of server from admin dashboard", User.GetUsername());
_cleanupService.CleanupCacheDirectory();
/// <summary>
/// Checks for updates, if no updates that are > current version installed, returns null
/// </summary>
[HttpGet("check-update")]
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
{
return Ok(await _versionUpdaterService.CheckForUpdate());
}
return Ok();
}
[HttpGet("changelog")]
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
{
return Ok(await _versionUpdaterService.GetAllReleases());
}
/// <summary>
/// Performs an ad-hoc backup of the Database
/// </summary>
/// <returns></returns>
[HttpPost("backup-db")]
public ActionResult BackupDatabase()
{
_logger.LogInformation("{UserName} is backing up database of server from admin dashboard", User.GetUsername());
RecurringJob.Trigger("backup");
return Ok();
}
/// <summary>
/// Is this server accessible to the outside net
/// </summary>
/// <returns></returns>
[HttpGet("accessible")]
[AllowAnonymous]
public async Task<ActionResult<bool>> IsServerAccessible()
{
return await _emailService.CheckIfAccessible(Request.Host.ToString());
}
/// <summary>
/// Returns non-sensitive information about the current system
/// </summary>
/// <returns></returns>
[HttpGet("server-info")]
public async Task<ActionResult<ServerInfoDto>> GetVersion()
{
return Ok(await _statsService.GetServerInfo());
}
/// <summary>
/// Triggers the scheduling of the convert bookmarks job. Only one job will run at a time.
/// </summary>
/// <returns></returns>
[HttpPost("convert-bookmarks")]
public ActionResult ScheduleConvertBookmarks()
{
BackgroundJob.Enqueue(() => _bookmarkService.ConvertAllBookmarkToWebP());
return Ok();
}
[HttpGet("logs")]
public ActionResult GetLogs()
{
var files = _backupService.GetLogFiles(_config.GetMaxRollingFiles(), _config.GetLoggingFileName());
try
{
var zipPath = _archiveService.CreateZipForDownload(files, "logs");
return PhysicalFile(zipPath, "application/zip", Path.GetFileName(zipPath), true);
}
catch (KavitaException ex)
{
return BadRequest(ex.Message);
}
}
/// <summary>
/// Checks for updates, if no updates that are > current version installed, returns null
/// </summary>
[HttpGet("check-update")]
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
{
return Ok(await _versionUpdaterService.CheckForUpdate());
}
[HttpGet("changelog")]
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
{
return Ok(await _versionUpdaterService.GetAllReleases());
}
/// <summary>
/// Is this server accessible to the outside net
/// </summary>
/// <returns></returns>
[HttpGet("accessible")]
[AllowAnonymous]
public async Task<ActionResult<bool>> IsServerAccessible()
{
return await _emailService.CheckIfAccessible(Request.Host.ToString());
}
[HttpGet("jobs")]
public ActionResult<IEnumerable<JobDto>> GetJobs()
{
var recurringJobs = Hangfire.JobStorage.Current.GetConnection().GetRecurringJobs().Select(
dto =>
[HttpGet("jobs")]
public ActionResult<IEnumerable<JobDto>> GetJobs()
{
var recurringJobs = JobStorage.Current.GetConnection().GetRecurringJobs().Select(
dto =>
new JobDto() {
Id = dto.Id,
Title = dto.Id.Replace('-', ' '),
@ -165,10 +164,9 @@ namespace API.Controllers
LastExecution = dto.LastExecution,
});
// For now, let's just do something simple
//var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs("default", 0, int.MaxValue);
return Ok(recurringJobs);
// For now, let's just do something simple
//var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs("default", 0, int.MaxValue);
return Ok(recurringJobs);
}
}
}

View file

@ -9,6 +9,7 @@ using API.DTOs.Settings;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers.Converters;
using API.Logging;
using API.Services;
using API.Services.Tasks.Scanner;
using AutoMapper;
@ -20,285 +21,284 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace API.Controllers
namespace API.Controllers;
public class SettingsController : BaseApiController
{
public class SettingsController : BaseApiController
private readonly ILogger<SettingsController> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
private readonly IMapper _mapper;
private readonly IEmailService _emailService;
private readonly ILibraryWatcher _libraryWatcher;
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher)
{
private readonly ILogger<SettingsController> _logger;
private readonly IUnitOfWork _unitOfWork;
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
private readonly IMapper _mapper;
private readonly IEmailService _emailService;
private readonly ILibraryWatcher _libraryWatcher;
_logger = logger;
_unitOfWork = unitOfWork;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
_mapper = mapper;
_emailService = emailService;
_libraryWatcher = libraryWatcher;
}
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher)
[AllowAnonymous]
[HttpGet("base-url")]
public async Task<ActionResult<string>> GetBaseUrl()
{
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settingsDto.BaseUrl);
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet]
public async Task<ActionResult<ServerSettingDto>> GetSettings()
{
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settingsDto);
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset")]
public async Task<ActionResult<ServerSettingDto>> ResetSettings()
{
_logger.LogInformation("{UserName} is resetting Server Settings", User.GetUsername());
return await UpdateSettings(_mapper.Map<ServerSettingDto>(Seed.DefaultSettings));
}
/// <summary>
/// Resets the email service url
/// </summary>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-email-url")]
public async Task<ActionResult<ServerSettingDto>> ResetEmailServiceUrlSettings()
{
_logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername());
var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl);
emailSetting.Value = EmailService.DefaultApiUrl;
_unitOfWork.SettingsRepository.Update(emailSetting);
if (!await _unitOfWork.CommitAsync())
{
_logger = logger;
_unitOfWork = unitOfWork;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
_mapper = mapper;
_emailService = emailService;
_libraryWatcher = libraryWatcher;
await _unitOfWork.RollbackAsync();
}
[AllowAnonymous]
[HttpGet("base-url")]
public async Task<ActionResult<string>> GetBaseUrl()
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("test-email-url")]
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
{
return Ok(await _emailService.TestConnectivity(dto.Url));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost]
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
{
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
// We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
var updateBookmarks = false;
var originalBookmarkDirectory = _directoryService.BookmarkDirectory;
var bookmarkDirectory = updateSettingsDto.BookmarksDirectory;
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
{
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settingsDto.BaseUrl);
bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet]
public async Task<ActionResult<ServerSettingDto>> GetSettings()
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
{
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settingsDto);
bookmarkDirectory = _directoryService.BookmarkDirectory;
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset")]
public async Task<ActionResult<ServerSettingDto>> ResetSettings()
foreach (var setting in currentSettings)
{
_logger.LogInformation("{UserName} is resetting Server Settings", User.GetUsername());
return await UpdateSettings(_mapper.Map<ServerSettingDto>(Seed.DefaultSettings));
}
/// <summary>
/// Resets the email service url
/// </summary>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-email-url")]
public async Task<ActionResult<ServerSettingDto>> ResetEmailServiceUrlSettings()
{
_logger.LogInformation("{UserName} is resetting Email Service Url Setting", User.GetUsername());
var emailSetting = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl);
emailSetting.Value = EmailService.DefaultApiUrl;
_unitOfWork.SettingsRepository.Update(emailSetting);
if (!await _unitOfWork.CommitAsync())
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
{
await _unitOfWork.RollbackAsync();
setting.Value = updateSettingsDto.TaskBackup;
_unitOfWork.SettingsRepository.Update(setting);
}
return Ok(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync());
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("test-email-url")]
public async Task<ActionResult<EmailTestResultDto>> TestEmailServiceUrl(TestEmailDto dto)
{
return Ok(await _emailService.TestConnectivity(dto.Url));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpPost]
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
{
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
// We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
var updateBookmarks = false;
var originalBookmarkDirectory = _directoryService.BookmarkDirectory;
var bookmarkDirectory = updateSettingsDto.BookmarksDirectory;
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
{
bookmarkDirectory = _directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
setting.Value = updateSettingsDto.TaskScan;
_unitOfWork.SettingsRepository.Update(setting);
}
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
{
bookmarkDirectory = _directoryService.BookmarkDirectory;
setting.Value = updateSettingsDto.Port + string.Empty;
// Port is managed in appSetting.json
Configuration.Port = updateSettingsDto.Port;
_unitOfWork.SettingsRepository.Update(setting);
}
foreach (var setting in currentSettings)
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
{
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
{
setting.Value = updateSettingsDto.TaskBackup;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
{
setting.Value = updateSettingsDto.TaskScan;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.Port + string.Empty;
// Port is managed in appSetting.json
Configuration.Port = updateSettingsDto.Port;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
{
var path = !updateSettingsDto.BaseUrl.StartsWith("/")
? $"/{updateSettingsDto.BaseUrl}"
: updateSettingsDto.BaseUrl;
path = !path.EndsWith("/")
? $"{path}/"
: path;
setting.Value = path;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
Configuration.LogLevel = updateSettingsDto.LoggingLevel;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.EnableOpds + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.ConvertBookmarkToWebP && updateSettingsDto.ConvertBookmarkToWebP + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.ConvertBookmarkToWebP + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
{
// Validate new directory can be used
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
{
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
}
originalBookmarkDirectory = setting.Value;
// Normalize the path deliminators. Just to look nice in DB, no functionality
setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory);
_unitOfWork.SettingsRepository.Update(setting);
updateBookmarks = true;
}
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
if (!updateSettingsDto.AllowStatCollection)
{
_taskScheduler.CancelStatsTasks();
}
else
{
await _taskScheduler.ScheduleStatsTasks();
}
}
if (setting.Key == ServerSettingKey.EnableSwaggerUi && updateSettingsDto.EnableSwaggerUi + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.EnableSwaggerUi + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value)
{
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
{
return BadRequest("Total Backups must be between 1 and 30");
}
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
{
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
FlurlHttp.ConfigureClient(setting.Value, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
if (updateSettingsDto.EnableFolderWatching)
{
await _libraryWatcher.StartWatching();
}
else
{
_libraryWatcher.StopWatching();
}
}
var path = !updateSettingsDto.BaseUrl.StartsWith("/")
? $"/{updateSettingsDto.BaseUrl}"
: updateSettingsDto.BaseUrl;
path = !path.EndsWith("/")
? $"{path}/"
: path;
setting.Value = path;
_unitOfWork.SettingsRepository.Update(setting);
}
if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto);
try
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
{
await _unitOfWork.CommitAsync();
if (updateBookmarks)
{
_directoryService.ExistOrCreate(bookmarkDirectory);
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
}
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel);
_unitOfWork.SettingsRepository.Update(setting);
}
catch (Exception ex)
if (setting.Key == ServerSettingKey.EnableOpds && updateSettingsDto.EnableOpds + string.Empty != setting.Value)
{
_logger.LogError(ex, "There was an exception when updating server settings");
await _unitOfWork.RollbackAsync();
return BadRequest("There was a critical issue. Please try again.");
setting.Value = updateSettingsDto.EnableOpds + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.ConvertBookmarkToWebP && updateSettingsDto.ConvertBookmarkToWebP + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.ConvertBookmarkToWebP + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
_logger.LogInformation("Server Settings updated");
await _taskScheduler.ScheduleTasks();
return Ok(updateSettingsDto);
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
{
// Validate new directory can be used
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
{
return BadRequest("Bookmark Directory does not have correct permissions for Kavita to use");
}
originalBookmarkDirectory = setting.Value;
// Normalize the path deliminators. Just to look nice in DB, no functionality
setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory);
_unitOfWork.SettingsRepository.Update(setting);
updateBookmarks = true;
}
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
if (!updateSettingsDto.AllowStatCollection)
{
_taskScheduler.CancelStatsTasks();
}
else
{
await _taskScheduler.ScheduleStatsTasks();
}
}
if (setting.Key == ServerSettingKey.EnableSwaggerUi && updateSettingsDto.EnableSwaggerUi + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.EnableSwaggerUi + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.TotalBackups && updateSettingsDto.TotalBackups + string.Empty != setting.Value)
{
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
{
return BadRequest("Total Backups must be between 1 and 30");
}
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
{
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
FlurlHttp.ConfigureClient(setting.Value, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.EnableFolderWatching && updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
if (updateSettingsDto.EnableFolderWatching)
{
await _libraryWatcher.StartWatching();
}
else
{
_libraryWatcher.StopWatching();
}
}
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("task-frequencies")]
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto);
try
{
return Ok(CronConverter.Options);
await _unitOfWork.CommitAsync();
if (updateBookmarks)
{
_directoryService.ExistOrCreate(bookmarkDirectory);
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an exception when updating server settings");
await _unitOfWork.RollbackAsync();
return BadRequest("There was a critical issue. Please try again.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("library-types")]
public ActionResult<IEnumerable<string>> GetLibraryTypes()
{
return Ok(Enum.GetValues<LibraryType>().Select(t => t.ToDescription()));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("log-levels")]
public ActionResult<IEnumerable<string>> GetLogLevels()
{
return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"});
}
_logger.LogInformation("Server Settings updated");
await _taskScheduler.ScheduleTasks();
return Ok(updateSettingsDto);
}
[HttpGet("opds-enabled")]
public async Task<ActionResult<bool>> GetOpdsEnabled()
{
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settingsDto.EnableOpds);
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("task-frequencies")]
public ActionResult<IEnumerable<string>> GetTaskFrequencies()
{
return Ok(CronConverter.Options);
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("library-types")]
public ActionResult<IEnumerable<string>> GetLibraryTypes()
{
return Ok(Enum.GetValues<LibraryType>().Select(t => t.ToDescription()));
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("log-levels")]
public ActionResult<IEnumerable<string>> GetLogLevels()
{
return Ok(new [] {"Trace", "Debug", "Information", "Warning", "Critical"});
}
[HttpGet("opds-enabled")]
public async Task<ActionResult<bool>> GetOpdsEnabled()
{
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
return Ok(settingsDto.EnableOpds);
}
}

View file

@ -12,298 +12,297 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NetVips;
namespace API.Controllers
namespace API.Controllers;
/// <summary>
///
/// </summary>
[Authorize(Policy = "RequireAdminRole")]
public class UploadController : BaseApiController
{
/// <summary>
///
/// </summary>
[Authorize(Policy = "RequireAdminRole")]
public class UploadController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IImageService _imageService;
private readonly ILogger<UploadController> _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
private readonly IEventHub _eventHub;
/// <inheritdoc />
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub)
{
private readonly IUnitOfWork _unitOfWork;
private readonly IImageService _imageService;
private readonly ILogger<UploadController> _logger;
private readonly ITaskScheduler _taskScheduler;
private readonly IDirectoryService _directoryService;
private readonly IEventHub _eventHub;
_unitOfWork = unitOfWork;
_imageService = imageService;
_logger = logger;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
_eventHub = eventHub;
}
/// <inheritdoc />
public UploadController(IUnitOfWork unitOfWork, IImageService imageService, ILogger<UploadController> logger,
ITaskScheduler taskScheduler, IDirectoryService directoryService, IEventHub eventHub)
/// <summary>
/// This stores a file (image) in temp directory for use in a cover image replacement flow.
/// This is automatically cleaned up.
/// </summary>
/// <param name="dto">Escaped url to download from</param>
/// <returns>filename</returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("upload-by-url")]
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
{
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
try
{
_unitOfWork = unitOfWork;
_imageService = imageService;
_logger = logger;
_taskScheduler = taskScheduler;
_directoryService = directoryService;
_eventHub = eventHub;
var path = await dto.Url
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
return BadRequest($"Could not download file");
if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
return $"coverupload_{dateString}.{format}";
}
catch (FlurlHttpException ex)
{
// Unauthorized
if (ex.StatusCode == 401)
return BadRequest("The server requires authentication to load the url externally");
}
/// <summary>
/// This stores a file (image) in temp directory for use in a cover image replacement flow.
/// This is automatically cleaned up.
/// </summary>
/// <param name="dto">Escaped url to download from</param>
/// <returns>filename</returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("upload-by-url")]
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
return BadRequest("Unable to download image, please use another url or upload by file");
}
/// <summary>
/// Replaces series cover image and locks it with a base64 encoded image
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("series")]
public async Task<ActionResult> UploadSeriesCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
try
{
var path = await dto.Url
.DownloadFileAsync(_directoryService.TempDirectory, $"coverupload_{dateString}.{format}");
if (string.IsNullOrEmpty(path) || !_directoryService.FileSystem.File.Exists(path))
return BadRequest($"Could not download file");
if (!await _imageService.IsImage(path)) return BadRequest("Url does not return a valid image");
return $"coverupload_{dateString}.{format}";
}
catch (FlurlHttpException ex)
{
// Unauthorized
if (ex.StatusCode == 401)
return BadRequest("The server requires authentication to load the url externally");
}
return BadRequest("Unable to download image, please use another url or upload by file");
return BadRequest("You must pass a url to use");
}
/// <summary>
/// Replaces series cover image and locks it with a base64 encoded image
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("series")]
public async Task<ActionResult> UploadSeriesCoverImageFromUrl(UploadFileDto uploadFileDto)
try
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id));
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
if (!string.IsNullOrEmpty(filePath))
{
return BadRequest("You must pass a url to use");
series.CoverImage = filePath;
series.CoverImageLocked = true;
_unitOfWork.SeriesRepository.Update(series);
}
try
if (_unitOfWork.HasChanges())
{
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, ImageService.GetSeriesFormat(uploadFileDto.Id));
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(uploadFileDto.Id);
if (!string.IsNullOrEmpty(filePath))
{
series.CoverImage = filePath;
series.CoverImageLocked = true;
_unitOfWork.SeriesRepository.Update(series);
}
if (_unitOfWork.HasChanges())
{
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
await _unitOfWork.CommitAsync();
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Series {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
await _unitOfWork.CommitAsync();
return Ok();
}
return BadRequest("Unable to save cover image to Series");
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Series {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
/// <summary>
/// Replaces collection tag cover image and locks it with a base64 encoded image
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("collection")]
public async Task<ActionResult> UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto)
return BadRequest("Unable to save cover image to Series");
}
/// <summary>
/// Replaces collection tag cover image and locks it with a base64 encoded image
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("collection")]
public async Task<ActionResult> UploadCollectionCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
}
try
{
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
if (!string.IsNullOrEmpty(filePath))
{
tag.CoverImage = filePath;
tag.CoverImageLocked = true;
_unitOfWork.CollectionTagRepository.Update(tag);
}
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Collection Tag {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Collection Tag");
return BadRequest("You must pass a url to use");
}
/// <summary>
/// Replaces reading list cover image and locks it with a base64 encoded image
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("reading-list")]
public async Task<ActionResult> UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
try
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetCollectionTagFormat(uploadFileDto.Id)}");
var tag = await _unitOfWork.CollectionTagRepository.GetTagAsync(uploadFileDto.Id);
if (!string.IsNullOrEmpty(filePath))
{
return BadRequest("You must pass a url to use");
tag.CoverImage = filePath;
tag.CoverImageLocked = true;
_unitOfWork.CollectionTagRepository.Update(tag);
}
try
if (_unitOfWork.HasChanges())
{
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
if (!string.IsNullOrEmpty(filePath))
{
readingList.CoverImage = filePath;
readingList.CoverImageLocked = true;
_unitOfWork.ReadingListRepository.Update(readingList);
}
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
return Ok();
}
return BadRequest("Unable to save cover image to Reading List");
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Collection Tag {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
/// <summary>
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("chapter")]
public async Task<ActionResult> UploadChapterCoverImageFromUrl(UploadFileDto uploadFileDto)
return BadRequest("Unable to save cover image to Collection Tag");
}
/// <summary>
/// Replaces reading list cover image and locks it with a base64 encoded image
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("reading-list")]
public async Task<ActionResult> UploadReadingListCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
}
try
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
if (!string.IsNullOrEmpty(filePath))
{
chapter.CoverImage = filePath;
chapter.CoverImageLocked = true;
_unitOfWork.ChapterRepository.Update(chapter);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
volume.CoverImage = chapter.CoverImage;
_unitOfWork.VolumeRepository.Update(volume);
}
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Chapter {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Chapter");
return BadRequest("You must pass a url to use");
}
/// <summary>
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
/// </summary>
/// <param name="uploadFileDto">Does not use Url property</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-chapter-lock")]
public async Task<ActionResult> ResetChapterLock(UploadFileDto uploadFileDto)
try
{
try
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetReadingListFormat(uploadFileDto.Id)}");
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(uploadFileDto.Id);
if (!string.IsNullOrEmpty(filePath))
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
var originalFile = chapter.CoverImage;
chapter.CoverImage = string.Empty;
chapter.CoverImageLocked = false;
readingList.CoverImage = filePath;
readingList.CoverImageLocked = true;
_unitOfWork.ReadingListRepository.Update(readingList);
}
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(readingList.Id, MessageFactoryEntityTypes.ReadingList), false);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Reading List {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Reading List");
}
/// <summary>
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
/// </summary>
/// <param name="uploadFileDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[RequestSizeLimit(8_000_000)]
[HttpPost("chapter")]
public async Task<ActionResult> UploadChapterCoverImageFromUrl(UploadFileDto uploadFileDto)
{
// Check if Url is non empty, request the image and place in temp, then ask image service to handle it.
// See if we can do this all in memory without touching underlying system
if (string.IsNullOrEmpty(uploadFileDto.Url))
{
return BadRequest("You must pass a url to use");
}
try
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
var filePath = _imageService.CreateThumbnailFromBase64(uploadFileDto.Url, $"{ImageService.GetChapterFormat(uploadFileDto.Id, chapter.VolumeId)}");
if (!string.IsNullOrEmpty(filePath))
{
chapter.CoverImage = filePath;
chapter.CoverImageLocked = true;
_unitOfWork.ChapterRepository.Update(chapter);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
volume.CoverImage = chapter.CoverImage;
_unitOfWork.VolumeRepository.Update(volume);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
System.IO.File.Delete(originalFile);
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
return Ok();
}
}
catch (Exception e)
if (_unitOfWork.HasChanges())
{
_logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
await _unitOfWork.CommitAsync();
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.VolumeId, MessageFactoryEntityTypes.Volume), false);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
return Ok();
}
return BadRequest("Unable to resetting cover lock for Chapter");
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue uploading cover image for Chapter {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to save cover image to Chapter");
}
/// <summary>
/// Replaces chapter cover image and locks it with a base64 encoded image. This will update the parent volume's cover image.
/// </summary>
/// <param name="uploadFileDto">Does not use Url property</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("reset-chapter-lock")]
public async Task<ActionResult> ResetChapterLock(UploadFileDto uploadFileDto)
{
try
{
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(uploadFileDto.Id);
var originalFile = chapter.CoverImage;
chapter.CoverImage = string.Empty;
chapter.CoverImageLocked = false;
_unitOfWork.ChapterRepository.Update(chapter);
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(chapter.VolumeId);
volume.CoverImage = chapter.CoverImage;
_unitOfWork.VolumeRepository.Update(volume);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(volume.SeriesId);
if (_unitOfWork.HasChanges())
{
await _unitOfWork.CommitAsync();
System.IO.File.Delete(originalFile);
_taskScheduler.RefreshSeriesMetadata(series.LibraryId, series.Id, true);
return Ok();
}
}
catch (Exception e)
{
_logger.LogError(e, "There was an issue resetting cover lock for Chapter {Id}", uploadFileDto.Id);
await _unitOfWork.RollbackAsync();
}
return BadRequest("Unable to resetting cover lock for Chapter");
}
}

View file

@ -13,112 +13,111 @@ using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
namespace API.Controllers;
[Authorize]
public class UsersController : BaseApiController
{
[Authorize]
public class UsersController : BaseApiController
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IEventHub _eventHub;
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IEventHub _eventHub;
_unitOfWork = unitOfWork;
_mapper = mapper;
_eventHub = eventHub;
}
public UsersController(IUnitOfWork unitOfWork, IMapper mapper, IEventHub eventHub)
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete-user")]
public async Task<ActionResult> DeleteUser(string username)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
_unitOfWork.UserRepository.Delete(user);
if (await _unitOfWork.CommitAsync()) return Ok();
return BadRequest("Could not delete the user.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet]
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
{
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync());
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("pending")]
public async Task<ActionResult<IEnumerable<MemberDto>>> GetPendingUsers()
{
return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync());
}
[HttpGet("has-reading-progress")]
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
}
[HttpGet("has-library-access")]
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
{
var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername());
return Ok(libs.Any(x => x.Id == libraryId));
}
[HttpPost("update-preferences")]
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
AppUserIncludes.UserPreferences);
var existingPreferences = user.UserPreferences;
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
_unitOfWork.UserRepository.Update(existingPreferences);
if (await _unitOfWork.CommitAsync())
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_eventHub = eventHub;
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
return Ok(preferencesDto);
}
[Authorize(Policy = "RequireAdminRole")]
[HttpDelete("delete-user")]
public async Task<ActionResult> DeleteUser(string username)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
_unitOfWork.UserRepository.Delete(user);
return BadRequest("There was an issue saving preferences.");
}
if (await _unitOfWork.CommitAsync()) return Ok();
[HttpGet("get-preferences")]
public async Task<ActionResult<UserPreferencesDto>> GetPreferences()
{
return _mapper.Map<UserPreferencesDto>(
await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()));
return BadRequest("Could not delete the user.");
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet]
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
{
return Ok(await _unitOfWork.UserRepository.GetEmailConfirmedMemberDtosAsync());
}
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("pending")]
public async Task<ActionResult<IEnumerable<MemberDto>>> GetPendingUsers()
{
return Ok(await _unitOfWork.UserRepository.GetPendingMemberDtosAsync());
}
[HttpGet("has-reading-progress")]
public async Task<ActionResult<bool>> HasReadingProgress(int libraryId)
{
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
return Ok(await _unitOfWork.AppUserProgressRepository.UserHasProgress(library.Type, userId));
}
[HttpGet("has-library-access")]
public async Task<ActionResult<bool>> HasLibraryAccess(int libraryId)
{
var libs = await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername());
return Ok(libs.Any(x => x.Id == libraryId));
}
[HttpPost("update-preferences")]
public async Task<ActionResult<UserPreferencesDto>> UpdatePreferences(UserPreferencesDto preferencesDto)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(),
AppUserIncludes.UserPreferences);
var existingPreferences = user.UserPreferences;
existingPreferences.ReadingDirection = preferencesDto.ReadingDirection;
existingPreferences.ScalingOption = preferencesDto.ScalingOption;
existingPreferences.PageSplitOption = preferencesDto.PageSplitOption;
existingPreferences.AutoCloseMenu = preferencesDto.AutoCloseMenu;
existingPreferences.ShowScreenHints = preferencesDto.ShowScreenHints;
existingPreferences.ReaderMode = preferencesDto.ReaderMode;
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.BackgroundColor = string.IsNullOrEmpty(preferencesDto.BackgroundColor) ? "#000000" : preferencesDto.BackgroundColor;
existingPreferences.BookReaderMargin = preferencesDto.BookReaderMargin;
existingPreferences.BookReaderLineSpacing = preferencesDto.BookReaderLineSpacing;
existingPreferences.BookReaderFontFamily = preferencesDto.BookReaderFontFamily;
existingPreferences.BookReaderFontSize = preferencesDto.BookReaderFontSize;
existingPreferences.BookReaderTapToPaginate = preferencesDto.BookReaderTapToPaginate;
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
preferencesDto.Theme ??= await _unitOfWork.SiteThemeRepository.GetDefaultTheme();
existingPreferences.BookThemeName = preferencesDto.BookReaderThemeName;
existingPreferences.BookReaderLayoutMode = preferencesDto.BookReaderLayoutMode;
existingPreferences.BookReaderImmersiveMode = preferencesDto.BookReaderImmersiveMode;
existingPreferences.GlobalPageLayoutMode = preferencesDto.GlobalPageLayoutMode;
existingPreferences.BlurUnreadSummaries = preferencesDto.BlurUnreadSummaries;
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
_unitOfWork.UserRepository.Update(existingPreferences);
if (await _unitOfWork.CommitAsync())
{
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
return Ok(preferencesDto);
}
return BadRequest("There was an issue saving preferences.");
}
[HttpGet("get-preferences")]
public async Task<ActionResult<UserPreferencesDto>> GetPreferences()
{
return _mapper.Map<UserPreferencesDto>(
await _unitOfWork.UserRepository.GetPreferencesAsync(User.GetUsername()));
}
}
}

View file

@ -1,8 +1,7 @@
namespace API.DTOs.Account
namespace API.DTOs.Account;
public class LoginDto
{
public class LoginDto
{
public string Username { get; init; }
public string Password { get; set; }
}
public string Username { get; init; }
public string Password { get; set; }
}

View file

@ -1,23 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace API.DTOs.Account
namespace API.DTOs.Account;
public class ResetPasswordDto
{
public class ResetPasswordDto
{
/// <summary>
/// The Username of the User
/// </summary>
[Required]
public string UserName { get; init; }
/// <summary>
/// The new password
/// </summary>
[Required]
[StringLength(32, MinimumLength = 6)]
public string Password { get; init; }
/// <summary>
/// The old, existing password. If an admin is performing the change, this is not required. Otherwise, it is.
/// </summary>
public string OldPassword { get; init; }
}
/// <summary>
/// The Username of the User
/// </summary>
[Required]
public string UserName { get; init; }
/// <summary>
/// The new password
/// </summary>
[Required]
[StringLength(32, MinimumLength = 6)]
public string Password { get; init; }
/// <summary>
/// The old, existing password. If an admin is performing the change, this is not required. Otherwise, it is.
/// </summary>
public string OldPassword { get; init; }
}

View file

@ -5,89 +5,88 @@ using API.DTOs.Reader;
using API.Entities.Enums;
using API.Entities.Interfaces;
namespace API.DTOs
{
/// <summary>
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
/// file (abstracted from type).
/// </summary>
public class ChapterDto : IHasReadTimeEstimate
{
public int Id { get; init; }
/// <summary>
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
/// </summary>
public string Range { get; init; }
/// <summary>
/// Smallest number of the Range.
/// </summary>
public string Number { get; init; }
/// <summary>
/// Total number of pages in all MangaFiles
/// </summary>
public int Pages { get; init; }
/// <summary>
/// If this Chapter contains files that could only be identified as Series or has Special Identifier from filename
/// </summary>
public bool IsSpecial { get; init; }
/// <summary>
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
/// </summary>
public string Title { get; set; }
/// <summary>
/// The files that represent this Chapter
/// </summary>
public ICollection<MangaFileDto> Files { get; init; }
/// <summary>
/// Calculated at API time. Number of pages read for this Chapter for logged in user.
/// </summary>
public int PagesRead { get; set; }
/// <summary>
/// If the Cover Image is locked for this entity
/// </summary>
public bool CoverImageLocked { get; set; }
/// <summary>
/// Volume Id this Chapter belongs to
/// </summary>
public int VolumeId { get; init; }
/// <summary>
/// When chapter was created
/// </summary>
public DateTime Created { get; init; }
/// <summary>
/// When the chapter was released.
/// </summary>
/// <remarks>Metadata field</remarks>
public DateTime ReleaseDate { get; init; }
/// <summary>
/// Title of the Chapter/Issue
/// </summary>
/// <remarks>Metadata field</remarks>
public string TitleName { get; set; }
/// <summary>
/// Summary of the Chapter
/// </summary>
/// <remarks>This is not set normally, only for Series Detail</remarks>
public string Summary { get; init; }
/// <summary>
/// Age Rating for the issue/chapter
/// </summary>
public AgeRating AgeRating { get; init; }
/// <summary>
/// Total words in a Chapter (books only)
/// </summary>
public long WordCount { get; set; } = 0L;
namespace API.DTOs;
/// <summary>
/// Formatted Volume title ie) Volume 2.
/// </summary>
/// <remarks>Only available when fetched from Series Detail API</remarks>
public string VolumeTitle { get; set; } = string.Empty;
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
public int MinHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
public int MaxHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
public int AvgHoursToRead { get; set; }
}
/// <summary>
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
/// file (abstracted from type).
/// </summary>
public class ChapterDto : IHasReadTimeEstimate
{
public int Id { get; init; }
/// <summary>
/// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2".
/// </summary>
public string Range { get; init; }
/// <summary>
/// Smallest number of the Range.
/// </summary>
public string Number { get; init; }
/// <summary>
/// Total number of pages in all MangaFiles
/// </summary>
public int Pages { get; init; }
/// <summary>
/// If this Chapter contains files that could only be identified as Series or has Special Identifier from filename
/// </summary>
public bool IsSpecial { get; init; }
/// <summary>
/// Used for books/specials to display custom title. For non-specials/books, will be set to <see cref="Range"/>
/// </summary>
public string Title { get; set; }
/// <summary>
/// The files that represent this Chapter
/// </summary>
public ICollection<MangaFileDto> Files { get; init; }
/// <summary>
/// Calculated at API time. Number of pages read for this Chapter for logged in user.
/// </summary>
public int PagesRead { get; set; }
/// <summary>
/// If the Cover Image is locked for this entity
/// </summary>
public bool CoverImageLocked { get; set; }
/// <summary>
/// Volume Id this Chapter belongs to
/// </summary>
public int VolumeId { get; init; }
/// <summary>
/// When chapter was created
/// </summary>
public DateTime Created { get; init; }
/// <summary>
/// When the chapter was released.
/// </summary>
/// <remarks>Metadata field</remarks>
public DateTime ReleaseDate { get; init; }
/// <summary>
/// Title of the Chapter/Issue
/// </summary>
/// <remarks>Metadata field</remarks>
public string TitleName { get; set; }
/// <summary>
/// Summary of the Chapter
/// </summary>
/// <remarks>This is not set normally, only for Series Detail</remarks>
public string Summary { get; init; }
/// <summary>
/// Age Rating for the issue/chapter
/// </summary>
public AgeRating AgeRating { get; init; }
/// <summary>
/// Total words in a Chapter (books only)
/// </summary>
public long WordCount { get; set; } = 0L;
/// <summary>
/// Formatted Volume title ie) Volume 2.
/// </summary>
/// <remarks>Only available when fetched from Series Detail API</remarks>
public string VolumeTitle { get; set; } = string.Empty;
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
public int MinHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
public int MaxHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
public int AvgHoursToRead { get; set; }
}

View file

@ -1,18 +1,17 @@
using System.Collections.Generic;
namespace API.DTOs.CollectionTags
namespace API.DTOs.CollectionTags;
public class CollectionTagBulkAddDto
{
public class CollectionTagBulkAddDto
{
/// <summary>
/// Collection Tag Id
/// </summary>
/// <remarks>Can be 0 which then will use Title to create a tag</remarks>
public int CollectionTagId { get; init; }
public string CollectionTagTitle { get; init; }
/// <summary>
/// Series Ids to add onto Collection Tag
/// </summary>
public IEnumerable<int> SeriesIds { get; init; }
}
/// <summary>
/// Collection Tag Id
/// </summary>
/// <remarks>Can be 0 which then will use Title to create a tag</remarks>
public int CollectionTagId { get; init; }
public string CollectionTagTitle { get; init; }
/// <summary>
/// Series Ids to add onto Collection Tag
/// </summary>
public IEnumerable<int> SeriesIds { get; init; }
}

View file

@ -1,15 +1,14 @@
namespace API.DTOs.CollectionTags
namespace API.DTOs.CollectionTags;
public class CollectionTagDto
{
public class CollectionTagDto
{
public int Id { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
/// <summary>
/// The cover image string. This is used on Frontend to show or hide the Cover Image
/// </summary>
public string CoverImage { get; set; }
public bool CoverImageLocked { get; set; }
}
public int Id { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
/// <summary>
/// The cover image string. This is used on Frontend to show or hide the Cover Image
/// </summary>
public string CoverImage { get; set; }
public bool CoverImageLocked { get; set; }
}

View file

@ -1,10 +1,9 @@
using System.Collections.Generic;
namespace API.DTOs.CollectionTags
namespace API.DTOs.CollectionTags;
public class UpdateSeriesForTagDto
{
public class UpdateSeriesForTagDto
{
public CollectionTagDto Tag { get; init; }
public IEnumerable<int> SeriesIdsToRemove { get; init; }
}
public CollectionTagDto Tag { get; init; }
public IEnumerable<int> SeriesIdsToRemove { get; init; }
}

View file

@ -2,16 +2,15 @@
using System.ComponentModel.DataAnnotations;
using API.Entities.Enums;
namespace API.DTOs
namespace API.DTOs;
public class CreateLibraryDto
{
public class CreateLibraryDto
{
[Required]
public string Name { get; init; }
[Required]
public LibraryType Type { get; init; }
[Required]
[MinLength(1)]
public IEnumerable<string> Folders { get; init; }
}
}
[Required]
public string Name { get; init; }
[Required]
public LibraryType Type { get; init; }
[Required]
[MinLength(1)]
public IEnumerable<string> Folders { get; init; }
}

View file

@ -1,9 +1,8 @@
using System.Collections.Generic;
namespace API.DTOs
namespace API.DTOs;
public class DeleteSeriesDto
{
public class DeleteSeriesDto
{
public IList<int> SeriesIds { get; set; }
}
public IList<int> SeriesIds { get; set; }
}

View file

@ -2,11 +2,10 @@
using System.ComponentModel.DataAnnotations;
using API.DTOs.Reader;
namespace API.DTOs.Downloads
namespace API.DTOs.Downloads;
public class DownloadBookmarkDto
{
public class DownloadBookmarkDto
{
[Required]
public IEnumerable<BookmarkDto> Bookmarks { get; set; }
}
[Required]
public IEnumerable<BookmarkDto> Bookmarks { get; set; }
}

View file

@ -3,101 +3,100 @@ using System.Runtime.InteropServices;
using API.Entities;
using API.Entities.Enums;
namespace API.DTOs.Filtering
namespace API.DTOs.Filtering;
public class FilterDto
{
public class FilterDto
{
/// <summary>
/// The type of Formats you want to be returned. An empty list will return all formats back
/// </summary>
public IList<MangaFormat> Formats { get; init; } = new List<MangaFormat>();
/// <summary>
/// The type of Formats you want to be returned. An empty list will return all formats back
/// </summary>
public IList<MangaFormat> Formats { get; init; } = new List<MangaFormat>();
/// <summary>
/// The progress you want to be returned. This can be bitwise manipulated. Defaults to all applicable states.
/// </summary>
public ReadStatus ReadStatus { get; init; } = new ReadStatus();
/// <summary>
/// The progress you want to be returned. This can be bitwise manipulated. Defaults to all applicable states.
/// </summary>
public ReadStatus ReadStatus { get; init; } = new ReadStatus();
/// <summary>
/// A list of library ids to restrict search to. Defaults to all libraries by passing empty list
/// </summary>
public IList<int> Libraries { get; init; } = new List<int>();
/// <summary>
/// A list of Genre ids to restrict search to. Defaults to all genres by passing an empty list
/// </summary>
public IList<int> Genres { get; init; } = new List<int>();
/// <summary>
/// A list of Writers to restrict search to. Defaults to all Writers by passing an empty list
/// </summary>
public IList<int> Writers { get; init; } = new List<int>();
/// <summary>
/// A list of Penciller ids to restrict search to. Defaults to all Pencillers by passing an empty list
/// </summary>
public IList<int> Penciller { get; init; } = new List<int>();
/// <summary>
/// A list of Inker ids to restrict search to. Defaults to all Inkers by passing an empty list
/// </summary>
public IList<int> Inker { get; init; } = new List<int>();
/// <summary>
/// A list of Colorist ids to restrict search to. Defaults to all Colorists by passing an empty list
/// </summary>
public IList<int> Colorist { get; init; } = new List<int>();
/// <summary>
/// A list of Letterer ids to restrict search to. Defaults to all Letterers by passing an empty list
/// </summary>
public IList<int> Letterer { get; init; } = new List<int>();
/// <summary>
/// A list of CoverArtist ids to restrict search to. Defaults to all CoverArtists by passing an empty list
/// </summary>
public IList<int> CoverArtist { get; init; } = new List<int>();
/// <summary>
/// A list of Editor ids to restrict search to. Defaults to all Editors by passing an empty list
/// </summary>
public IList<int> Editor { get; init; } = new List<int>();
/// <summary>
/// A list of Publisher ids to restrict search to. Defaults to all Publishers by passing an empty list
/// </summary>
public IList<int> Publisher { get; init; } = new List<int>();
/// <summary>
/// A list of Character ids to restrict search to. Defaults to all Characters by passing an empty list
/// </summary>
public IList<int> Character { get; init; } = new List<int>();
/// <summary>
/// A list of Translator ids to restrict search to. Defaults to all Translatorss by passing an empty list
/// </summary>
public IList<int> Translators { get; init; } = new List<int>();
/// <summary>
/// A list of Collection Tag ids to restrict search to. Defaults to all Collection Tags by passing an empty list
/// </summary>
public IList<int> CollectionTags { get; init; } = new List<int>();
/// <summary>
/// A list of Tag ids to restrict search to. Defaults to all Tags by passing an empty list
/// </summary>
public IList<int> Tags { get; init; } = new List<int>();
/// <summary>
/// Will return back everything with the rating and above
/// <see cref="AppUserRating.Rating"/>
/// </summary>
public int Rating { get; init; }
/// <summary>
/// Sorting Options for a query. Defaults to null, which uses the queries natural sorting order
/// </summary>
public SortOptions SortOptions { get; set; } = null;
/// <summary>
/// Age Ratings. Empty list will return everything back
/// </summary>
public IList<AgeRating> AgeRating { get; init; } = new List<AgeRating>();
/// <summary>
/// Languages (ISO 639-1 code) to filter by. Empty list will return everything back
/// </summary>
public IList<string> Languages { get; init; } = new List<string>();
/// <summary>
/// Publication statuses to filter by. Empty list will return everything back
/// </summary>
public IList<PublicationStatus> PublicationStatus { get; init; } = new List<PublicationStatus>();
/// <summary>
/// A list of library ids to restrict search to. Defaults to all libraries by passing empty list
/// </summary>
public IList<int> Libraries { get; init; } = new List<int>();
/// <summary>
/// A list of Genre ids to restrict search to. Defaults to all genres by passing an empty list
/// </summary>
public IList<int> Genres { get; init; } = new List<int>();
/// <summary>
/// A list of Writers to restrict search to. Defaults to all Writers by passing an empty list
/// </summary>
public IList<int> Writers { get; init; } = new List<int>();
/// <summary>
/// A list of Penciller ids to restrict search to. Defaults to all Pencillers by passing an empty list
/// </summary>
public IList<int> Penciller { get; init; } = new List<int>();
/// <summary>
/// A list of Inker ids to restrict search to. Defaults to all Inkers by passing an empty list
/// </summary>
public IList<int> Inker { get; init; } = new List<int>();
/// <summary>
/// A list of Colorist ids to restrict search to. Defaults to all Colorists by passing an empty list
/// </summary>
public IList<int> Colorist { get; init; } = new List<int>();
/// <summary>
/// A list of Letterer ids to restrict search to. Defaults to all Letterers by passing an empty list
/// </summary>
public IList<int> Letterer { get; init; } = new List<int>();
/// <summary>
/// A list of CoverArtist ids to restrict search to. Defaults to all CoverArtists by passing an empty list
/// </summary>
public IList<int> CoverArtist { get; init; } = new List<int>();
/// <summary>
/// A list of Editor ids to restrict search to. Defaults to all Editors by passing an empty list
/// </summary>
public IList<int> Editor { get; init; } = new List<int>();
/// <summary>
/// A list of Publisher ids to restrict search to. Defaults to all Publishers by passing an empty list
/// </summary>
public IList<int> Publisher { get; init; } = new List<int>();
/// <summary>
/// A list of Character ids to restrict search to. Defaults to all Characters by passing an empty list
/// </summary>
public IList<int> Character { get; init; } = new List<int>();
/// <summary>
/// A list of Translator ids to restrict search to. Defaults to all Translatorss by passing an empty list
/// </summary>
public IList<int> Translators { get; init; } = new List<int>();
/// <summary>
/// A list of Collection Tag ids to restrict search to. Defaults to all Collection Tags by passing an empty list
/// </summary>
public IList<int> CollectionTags { get; init; } = new List<int>();
/// <summary>
/// A list of Tag ids to restrict search to. Defaults to all Tags by passing an empty list
/// </summary>
public IList<int> Tags { get; init; } = new List<int>();
/// <summary>
/// Will return back everything with the rating and above
/// <see cref="AppUserRating.Rating"/>
/// </summary>
public int Rating { get; init; }
/// <summary>
/// Sorting Options for a query. Defaults to null, which uses the queries natural sorting order
/// </summary>
public SortOptions SortOptions { get; set; } = null;
/// <summary>
/// Age Ratings. Empty list will return everything back
/// </summary>
public IList<AgeRating> AgeRating { get; init; } = new List<AgeRating>();
/// <summary>
/// Languages (ISO 639-1 code) to filter by. Empty list will return everything back
/// </summary>
public IList<string> Languages { get; init; } = new List<string>();
/// <summary>
/// Publication statuses to filter by. Empty list will return everything back
/// </summary>
public IList<PublicationStatus> PublicationStatus { get; init; } = new List<PublicationStatus>();
/// <summary>
/// An optional name string to filter by. Empty string will ignore.
/// </summary>
public string SeriesNameQuery { get; init; } = string.Empty;
}
/// <summary>
/// An optional name string to filter by. Empty string will ignore.
/// </summary>
public string SeriesNameQuery { get; init; } = string.Empty;
}

View file

@ -2,17 +2,16 @@
using System.Collections.Generic;
using API.Entities.Enums;
namespace API.DTOs
namespace API.DTOs;
public class LibraryDto
{
public class LibraryDto
{
public int Id { get; init; }
public string Name { get; init; }
/// <summary>
/// Last time Library was scanned
/// </summary>
public DateTime LastScanned { get; init; }
public LibraryType Type { get; init; }
public ICollection<string> Folders { get; init; }
}
public int Id { get; init; }
public string Name { get; init; }
/// <summary>
/// Last time Library was scanned
/// </summary>
public DateTime LastScanned { get; init; }
public LibraryType Type { get; init; }
public ICollection<string> Folders { get; init; }
}

View file

@ -1,15 +1,14 @@
using System;
using API.Entities.Enums;
namespace API.DTOs
{
public class MangaFileDto
{
public int Id { get; init; }
public string FilePath { get; init; }
public int Pages { get; init; }
public MangaFormat Format { get; init; }
public DateTime Created { get; init; }
namespace API.DTOs;
public class MangaFileDto
{
public int Id { get; init; }
public string FilePath { get; init; }
public int Pages { get; init; }
public MangaFormat Format { get; init; }
public DateTime Created { get; init; }
}
}

View file

@ -1,19 +1,18 @@
using System;
using System.Collections.Generic;
namespace API.DTOs
namespace API.DTOs;
/// <summary>
/// Represents a member of a Kavita server.
/// </summary>
public class MemberDto
{
/// <summary>
/// Represents a member of a Kavita server.
/// </summary>
public class MemberDto
{
public int Id { get; init; }
public string Username { get; init; }
public string Email { get; init; }
public DateTime Created { get; init; }
public DateTime LastActive { get; init; }
public IEnumerable<LibraryDto> Libraries { get; init; }
public IEnumerable<string> Roles { get; init; }
}
public int Id { get; init; }
public string Username { get; init; }
public string Email { get; init; }
public DateTime Created { get; init; }
public DateTime LastActive { get; init; }
public IEnumerable<LibraryDto> Libraries { get; init; }
public IEnumerable<string> Roles { get; init; }
}

View file

@ -1,56 +1,55 @@
using System.Collections.Generic;
using API.Entities.Enums;
namespace API.DTOs.Metadata
namespace API.DTOs.Metadata;
/// <summary>
/// Exclusively metadata about a given chapter
/// </summary>
public class ChapterMetadataDto
{
public int Id { get; set; }
public int ChapterId { get; set; }
public string Title { get; set; }
public ICollection<PersonDto> Writers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> CoverArtists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Publishers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
public ICollection<GenreTagDto> Genres { get; set; } = new List<GenreTagDto>();
/// <summary>
/// Exclusively metadata about a given chapter
/// Collection of all Tags from underlying chapters for a Series
/// </summary>
public class ChapterMetadataDto
{
public int Id { get; set; }
public int ChapterId { get; set; }
public string Title { get; set; }
public ICollection<PersonDto> Writers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> CoverArtists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Publishers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
public ICollection<TagDto> Tags { get; set; } = new List<TagDto>();
public AgeRating AgeRating { get; set; }
public string ReleaseDate { get; set; }
public PublicationStatus PublicationStatus { get; set; }
/// <summary>
/// Summary for the Chapter/Issue
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Language for the Chapter/Issue
/// </summary>
public string Language { get; set; }
/// <summary>
/// Number in the TotalCount of issues
/// </summary>
public int Count { get; set; }
/// <summary>
/// Total number of issues for the series
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Number of Words for this chapter. Only applies to Epub
/// </summary>
public long WordCount { get; set; }
public ICollection<GenreTagDto> Genres { get; set; } = new List<GenreTagDto>();
/// <summary>
/// Collection of all Tags from underlying chapters for a Series
/// </summary>
public ICollection<TagDto> Tags { get; set; } = new List<TagDto>();
public AgeRating AgeRating { get; set; }
public string ReleaseDate { get; set; }
public PublicationStatus PublicationStatus { get; set; }
/// <summary>
/// Summary for the Chapter/Issue
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Language for the Chapter/Issue
/// </summary>
public string Language { get; set; }
/// <summary>
/// Number in the TotalCount of issues
/// </summary>
public int Count { get; set; }
/// <summary>
/// Total number of issues for the series
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Number of Words for this chapter. Only applies to Epub
/// </summary>
public long WordCount { get; set; }
}
}

View file

@ -1,8 +1,7 @@
namespace API.DTOs.Metadata
namespace API.DTOs.Metadata;
public class GenreTagDto
{
public class GenreTagDto
{
public int Id { get; set; }
public string Title { get; set; }
}
public int Id { get; set; }
public string Title { get; set; }
}

View file

@ -2,61 +2,60 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
/// <summary>
///
/// </summary>
[XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")]
public class Feed
{
/// <summary>
///
/// </summary>
[XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")]
public class Feed
[XmlElement("updated")]
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
[XmlElement("id")]
public string Id { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("icon")]
public string Icon { get; set; } = "/favicon.ico";
[XmlElement("author")]
public FeedAuthor Author { get; set; } = new FeedAuthor()
{
[XmlElement("updated")]
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
Name = "Kavita",
Uri = "https://kavitareader.com"
};
[XmlElement("id")]
public string Id { get; set; }
[XmlElement("totalResults", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? Total { get; set; } = null;
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("itemsPerPage", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? ItemsPerPage { get; set; } = null;
[XmlElement("icon")]
public string Icon { get; set; } = "/favicon.ico";
[XmlElement("startIndex", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? StartIndex { get; set; } = null;
[XmlElement("author")]
public FeedAuthor Author { get; set; } = new FeedAuthor()
{
Name = "Kavita",
Uri = "https://kavitareader.com"
};
[XmlElement("link")]
public List<FeedLink> Links { get; set; } = new List<FeedLink>() ;
[XmlElement("totalResults", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? Total { get; set; } = null;
[XmlElement("entry")]
public List<FeedEntry> Entries { get; set; } = new List<FeedEntry>();
[XmlElement("itemsPerPage", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? ItemsPerPage { get; set; } = null;
public bool ShouldSerializeTotal()
{
return Total.HasValue;
}
[XmlElement("startIndex", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? StartIndex { get; set; } = null;
public bool ShouldSerializeItemsPerPage()
{
return ItemsPerPage.HasValue;
}
[XmlElement("link")]
public List<FeedLink> Links { get; set; } = new List<FeedLink>() ;
[XmlElement("entry")]
public List<FeedEntry> Entries { get; set; } = new List<FeedEntry>();
public bool ShouldSerializeTotal()
{
return Total.HasValue;
}
public bool ShouldSerializeItemsPerPage()
{
return ItemsPerPage.HasValue;
}
public bool ShouldSerializeStartIndex()
{
return StartIndex.HasValue;
}
public bool ShouldSerializeStartIndex()
{
return StartIndex.HasValue;
}
}

View file

@ -1,12 +1,11 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
public class FeedAuthor
{
public class FeedAuthor
{
[XmlElement("name")]
public string Name { get; set; }
[XmlElement("uri")]
public string Uri { get; set; }
}
[XmlElement("name")]
public string Name { get; set; }
[XmlElement("uri")]
public string Uri { get; set; }
}

View file

@ -2,50 +2,49 @@
using System.Collections.Generic;
using System.Xml.Serialization;
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
public class FeedEntry
{
public class FeedEntry
{
[XmlElement("updated")]
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
[XmlElement("updated")]
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
[XmlElement("id")]
public string Id { get; set; }
[XmlElement("id")]
public string Id { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("summary")]
public string Summary { get; set; }
[XmlElement("summary")]
public string Summary { get; set; }
/// <summary>
/// Represents Size of the Entry
/// Tag: , ElementName = "dcterms:extent"
/// <example>2 MB</example>
/// </summary>
[XmlElement("extent", Namespace = "http://purl.org/dc/terms/")]
public string Extent { get; set; }
/// <summary>
/// Represents Size of the Entry
/// Tag: , ElementName = "dcterms:extent"
/// <example>2 MB</example>
/// </summary>
[XmlElement("extent", Namespace = "http://purl.org/dc/terms/")]
public string Extent { get; set; }
/// <summary>
/// Format of the file
/// https://dublincore.org/specifications/dublin-core/dcmi-terms/
/// </summary>
[XmlElement("format", Namespace = "http://purl.org/dc/terms/format")]
public string Format { get; set; }
/// <summary>
/// Format of the file
/// https://dublincore.org/specifications/dublin-core/dcmi-terms/
/// </summary>
[XmlElement("format", Namespace = "http://purl.org/dc/terms/format")]
public string Format { get; set; }
[XmlElement("language", Namespace = "http://purl.org/dc/terms/")]
public string Language { get; set; }
[XmlElement("language", Namespace = "http://purl.org/dc/terms/")]
public string Language { get; set; }
[XmlElement("content")]
public FeedEntryContent Content { get; set; }
[XmlElement("content")]
public FeedEntryContent Content { get; set; }
[XmlElement("link")]
public List<FeedLink> Links = new List<FeedLink>();
[XmlElement("link")]
public List<FeedLink> Links = new List<FeedLink>();
// [XmlElement("author")]
// public List<FeedAuthor> Authors = new List<FeedAuthor>();
// [XmlElement("author")]
// public List<FeedAuthor> Authors = new List<FeedAuthor>();
// [XmlElement("category")]
// public List<FeedCategory> Categories = new List<FeedCategory>();
}
// [XmlElement("category")]
// public List<FeedCategory> Categories = new List<FeedCategory>();
}

View file

@ -1,12 +1,11 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
public class FeedEntryContent
{
public class FeedEntryContent
{
[XmlAttribute("type")]
public string Type = "text";
[XmlText]
public string Text;
}
[XmlAttribute("type")]
public string Type = "text";
[XmlText]
public string Text;
}

View file

@ -1,33 +1,32 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
public class FeedLink
{
public class FeedLink
/// <summary>
/// Relation on the Link
/// </summary>
[XmlAttribute("rel")]
public string Rel { get; set; }
/// <summary>
/// Should be any of the types here <see cref="FeedLinkType"/>
/// </summary>
[XmlAttribute("type")]
public string Type { get; set; }
[XmlAttribute("href")]
public string Href { get; set; }
[XmlAttribute("title")]
public string Title { get; set; }
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
public int TotalPages { get; set; }
public bool ShouldSerializeTotalPages()
{
/// <summary>
/// Relation on the Link
/// </summary>
[XmlAttribute("rel")]
public string Rel { get; set; }
/// <summary>
/// Should be any of the types here <see cref="FeedLinkType"/>
/// </summary>
[XmlAttribute("type")]
public string Type { get; set; }
[XmlAttribute("href")]
public string Href { get; set; }
[XmlAttribute("title")]
public string Title { get; set; }
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
public int TotalPages { get; set; }
public bool ShouldSerializeTotalPages()
{
return TotalPages > 0;
}
return TotalPages > 0;
}
}

View file

@ -1,24 +1,23 @@
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
public static class FeedLinkRelation
{
public static class FeedLinkRelation
{
public const string Debug = "debug";
public const string Search = "search";
public const string Self = "self";
public const string Start = "start";
public const string Next = "next";
public const string Prev = "prev";
public const string Alternate = "alternate";
public const string SubSection = "subsection";
public const string Related = "related";
public const string Image = "http://opds-spec.org/image";
public const string Thumbnail = "http://opds-spec.org/image/thumbnail";
/// <summary>
/// This will allow for a download to occur
/// </summary>
public const string Acquisition = "http://opds-spec.org/acquisition/open-access";
public const string Debug = "debug";
public const string Search = "search";
public const string Self = "self";
public const string Start = "start";
public const string Next = "next";
public const string Prev = "prev";
public const string Alternate = "alternate";
public const string SubSection = "subsection";
public const string Related = "related";
public const string Image = "http://opds-spec.org/image";
public const string Thumbnail = "http://opds-spec.org/image/thumbnail";
/// <summary>
/// This will allow for a download to occur
/// </summary>
public const string Acquisition = "http://opds-spec.org/acquisition/open-access";
#pragma warning disable S1075
public const string Stream = "http://vaemendis.net/opds-pse/stream";
public const string Stream = "http://vaemendis.net/opds-pse/stream";
#pragma warning restore S1075
}
}

View file

@ -1,11 +1,10 @@
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
public static class FeedLinkType
{
public static class FeedLinkType
{
public const string Atom = "application/atom+xml";
public const string AtomSearch = "application/opensearchdescription+xml";
public const string AtomNavigation = "application/atom+xml;profile=opds-catalog;kind=navigation";
public const string AtomAcquisition = "application/atom+xml;profile=opds-catalog;kind=acquisition";
public const string Image = "image/jpeg";
}
public const string Atom = "application/atom+xml";
public const string AtomSearch = "application/opensearchdescription+xml";
public const string AtomNavigation = "application/atom+xml;profile=opds-catalog;kind=navigation";
public const string AtomAcquisition = "application/atom+xml;profile=opds-catalog;kind=acquisition";
public const string Image = "image/jpeg";
}

View file

@ -1,42 +1,41 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
[XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public class OpenSearchDescription
{
/// <summary>
/// Contains a brief human-readable title that identifies this search engine.
/// </summary>
public string ShortName { get; set; }
/// <summary>
/// Contains an extended human-readable title that identifies this search engine.
/// </summary>
public string LongName { get; set; }
/// <summary>
/// Contains a human-readable text description of the search engine.
/// </summary>
public string Description { get; set; }
/// <summary>
/// https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#the-url-element
/// </summary>
public SearchLink Url { get; set; }
/// <summary>
/// Contains a set of words that are used as keywords to identify and categorize this search content.
/// Tags must be a single word and are delimited by the space character (' ').
/// </summary>
public string Tags { get; set; }
/// <summary>
/// Contains a URL that identifies the location of an image that can be used in association with this search content.
/// <example><Image height="64" width="64" type="image/png">http://example.com/websearch.png</Image></example>
/// </summary>
public string Image { get; set; }
public string InputEncoding { get; set; } = "UTF-8";
public string OutputEncoding { get; set; } = "UTF-8";
/// <summary>
/// Contains the human-readable name or identifier of the creator or maintainer of the description document.
/// </summary>
public string Developer { get; set; } = "kavitareader.com";
namespace API.DTOs.OPDS;
[XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public class OpenSearchDescription
{
/// <summary>
/// Contains a brief human-readable title that identifies this search engine.
/// </summary>
public string ShortName { get; set; }
/// <summary>
/// Contains an extended human-readable title that identifies this search engine.
/// </summary>
public string LongName { get; set; }
/// <summary>
/// Contains a human-readable text description of the search engine.
/// </summary>
public string Description { get; set; }
/// <summary>
/// https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#the-url-element
/// </summary>
public SearchLink Url { get; set; }
/// <summary>
/// Contains a set of words that are used as keywords to identify and categorize this search content.
/// Tags must be a single word and are delimited by the space character (' ').
/// </summary>
public string Tags { get; set; }
/// <summary>
/// Contains a URL that identifies the location of an image that can be used in association with this search content.
/// <example><Image height="64" width="64" type="image/png">http://example.com/websearch.png</Image></example>
/// </summary>
public string Image { get; set; }
public string InputEncoding { get; set; } = "UTF-8";
public string OutputEncoding { get; set; } = "UTF-8";
/// <summary>
/// Contains the human-readable name or identifier of the creator or maintainer of the description document.
/// </summary>
public string Developer { get; set; } = "kavitareader.com";
}
}

View file

@ -1,16 +1,15 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
namespace API.DTOs.OPDS;
public class SearchLink
{
public class SearchLink
{
[XmlAttribute("type")]
public string Type { get; set; }
[XmlAttribute("type")]
public string Type { get; set; }
[XmlAttribute("rel")]
public string Rel { get; set; } = "results";
[XmlAttribute("rel")]
public string Rel { get; set; } = "results";
[XmlAttribute("template")]
public string Template { get; set; }
}
[XmlAttribute("template")]
public string Template { get; set; }
}

View file

@ -1,11 +1,10 @@
using API.Entities.Enums;
namespace API.DTOs
namespace API.DTOs;
public class PersonDto
{
public class PersonDto
{
public int Id { get; set; }
public string Name { get; set; }
public PersonRole Role { get; set; }
}
public int Id { get; set; }
public string Name { get; set; }
public PersonRole Role { get; set; }
}

View file

@ -1,21 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace API.DTOs
namespace API.DTOs;
public class ProgressDto
{
public class ProgressDto
{
[Required]
public int VolumeId { get; set; }
[Required]
public int ChapterId { get; set; }
[Required]
public int PageNum { get; set; }
[Required]
public int SeriesId { get; set; }
/// <summary>
/// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position
/// on pages that combine multiple "chapters".
/// </summary>
public string BookScrollId { get; set; }
}
[Required]
public int VolumeId { get; set; }
[Required]
public int ChapterId { get; set; }
[Required]
public int PageNum { get; set; }
[Required]
public int SeriesId { get; set; }
/// <summary>
/// For Book reader, this can be an optional string of the id of a part marker, to help resume reading position
/// on pages that combine multiple "chapters".
/// </summary>
public string BookScrollId { get; set; }
}

View file

@ -1,21 +1,20 @@
using System.Collections.Generic;
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class BookChapterItem
{
public class BookChapterItem
{
/// <summary>
/// Name of the Chapter
/// </summary>
public string Title { get; set; }
/// <summary>
/// A part represents the id of the anchor so we can scroll to it. 01_values.xhtml#h_sVZPaxUSy/
/// </summary>
public string Part { get; set; }
/// <summary>
/// Page Number to load for the chapter
/// </summary>
public int Page { get; set; }
public ICollection<BookChapterItem> Children { get; set; }
}
/// <summary>
/// Name of the Chapter
/// </summary>
public string Title { get; set; }
/// <summary>
/// A part represents the id of the anchor so we can scroll to it. 01_values.xhtml#h_sVZPaxUSy/
/// </summary>
public string Part { get; set; }
/// <summary>
/// Page Number to load for the chapter
/// </summary>
public int Page { get; set; }
public ICollection<BookChapterItem> Children { get; set; }
}

View file

@ -1,19 +1,18 @@
using API.Entities.Enums;
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class BookInfoDto : IChapterInfoDto
{
public class BookInfoDto : IChapterInfoDto
{
public string BookTitle { get; set; }
public int SeriesId { get; set; }
public int VolumeId { get; set; }
public MangaFormat SeriesFormat { get; set; }
public string SeriesName { get; set; }
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int LibraryId { get; set; }
public int Pages { get; set; }
public bool IsSpecial { get; set; }
public string ChapterTitle { get; set; }
}
public string BookTitle { get; set; }
public int SeriesId { get; set; }
public int VolumeId { get; set; }
public MangaFormat SeriesFormat { get; set; }
public string SeriesName { get; set; }
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int LibraryId { get; set; }
public int Pages { get; set; }
public bool IsSpecial { get; set; }
public string ChapterTitle { get; set; }
}

View file

@ -1,17 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class BookmarkDto
{
public class BookmarkDto
{
public int Id { get; set; }
[Required]
public int Page { get; set; }
[Required]
public int VolumeId { get; set; }
[Required]
public int SeriesId { get; set; }
[Required]
public int ChapterId { get; set; }
}
public int Id { get; set; }
[Required]
public int Page { get; set; }
[Required]
public int VolumeId { get; set; }
[Required]
public int SeriesId { get; set; }
[Required]
public int ChapterId { get; set; }
}

View file

@ -1,9 +1,8 @@
using System.Collections.Generic;
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class BulkRemoveBookmarkForSeriesDto
{
public class BulkRemoveBookmarkForSeriesDto
{
public ICollection<int> SeriesIds { get; init; }
}
public ICollection<int> SeriesIds { get; init; }
}

View file

@ -1,19 +1,18 @@
using API.Entities.Enums;
namespace API.DTOs.Reader
{
public interface IChapterInfoDto
{
public int SeriesId { get; set; }
public int VolumeId { get; set; }
public MangaFormat SeriesFormat { get; set; }
public string SeriesName { get; set; }
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int LibraryId { get; set; }
public int Pages { get; set; }
public bool IsSpecial { get; set; }
public string ChapterTitle { get; set; }
namespace API.DTOs.Reader;
public interface IChapterInfoDto
{
public int SeriesId { get; set; }
public int VolumeId { get; set; }
public MangaFormat SeriesFormat { get; set; }
public string SeriesName { get; set; }
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int LibraryId { get; set; }
public int Pages { get; set; }
public bool IsSpecial { get; set; }
public string ChapterTitle { get; set; }
}
}

View file

@ -1,9 +1,8 @@
using System.Collections.Generic;
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class MarkMultipleSeriesAsReadDto
{
public class MarkMultipleSeriesAsReadDto
{
public IReadOnlyList<int> SeriesIds { get; init; }
}
public IReadOnlyList<int> SeriesIds { get; init; }
}

View file

@ -1,7 +1,6 @@
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class MarkReadDto
{
public class MarkReadDto
{
public int SeriesId { get; init; }
}
public int SeriesId { get; init; }
}

View file

@ -1,8 +1,7 @@
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class MarkVolumeReadDto
{
public class MarkVolumeReadDto
{
public int SeriesId { get; init; }
public int VolumeId { get; init; }
}
public int SeriesId { get; init; }
public int VolumeId { get; init; }
}

View file

@ -1,20 +1,19 @@
using System.Collections.Generic;
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
/// <summary>
/// This is used for bulk updating a set of volume and or chapters in one go
/// </summary>
public class MarkVolumesReadDto
{
public int SeriesId { get; set; }
/// <summary>
/// This is used for bulk updating a set of volume and or chapters in one go
/// A list of Volumes to mark read
/// </summary>
public class MarkVolumesReadDto
{
public int SeriesId { get; set; }
/// <summary>
/// A list of Volumes to mark read
/// </summary>
public IReadOnlyList<int> VolumeIds { get; set; }
/// <summary>
/// A list of additional Chapters to mark as read
/// </summary>
public IReadOnlyList<int> ChapterIds { get; set; }
}
public IReadOnlyList<int> VolumeIds { get; set; }
/// <summary>
/// A list of additional Chapters to mark as read
/// </summary>
public IReadOnlyList<int> ChapterIds { get; set; }
}

View file

@ -1,7 +1,6 @@
namespace API.DTOs.Reader
namespace API.DTOs.Reader;
public class RemoveBookmarkForSeriesDto
{
public class RemoveBookmarkForSeriesDto
{
public int SeriesId { get; init; }
}
public int SeriesId { get; init; }
}

View file

@ -1,7 +1,6 @@
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class CreateReadingListDto
{
public class CreateReadingListDto
{
public string Title { get; init; }
}
public string Title { get; init; }
}

View file

@ -1,18 +1,17 @@
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class ReadingListDto
{
public class ReadingListDto
{
public int Id { get; init; }
public string Title { get; set; }
public string Summary { get; set; }
/// <summary>
/// Reading lists that are promoted are only done by admins
/// </summary>
public bool Promoted { get; set; }
public bool CoverImageLocked { get; set; }
/// <summary>
/// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set.
/// </summary>
public string CoverImage { get; set; } = string.Empty;
}
public int Id { get; init; }
public string Title { get; set; }
public string Summary { get; set; }
/// <summary>
/// Reading lists that are promoted are only done by admins
/// </summary>
public bool Promoted { get; set; }
public bool CoverImageLocked { get; set; }
/// <summary>
/// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set.
/// </summary>
public string CoverImage { get; set; } = string.Empty;
}

View file

@ -1,25 +1,24 @@
using API.Entities.Enums;
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class ReadingListItemDto
{
public class ReadingListItemDto
{
public int Id { get; init; }
public int Order { get; init; }
public int ChapterId { get; init; }
public int SeriesId { get; init; }
public string SeriesName { get; set; }
public MangaFormat SeriesFormat { get; set; }
public int PagesRead { get; set; }
public int PagesTotal { get; set; }
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int VolumeId { get; set; }
public int LibraryId { get; set; }
public string Title { get; set; }
/// <summary>
/// Used internally only
/// </summary>
public int ReadingListId { get; set; }
}
public int Id { get; init; }
public int Order { get; init; }
public int ChapterId { get; init; }
public int SeriesId { get; init; }
public string SeriesName { get; set; }
public MangaFormat SeriesFormat { get; set; }
public int PagesRead { get; set; }
public int PagesTotal { get; set; }
public string ChapterNumber { get; set; }
public string VolumeNumber { get; set; }
public int VolumeId { get; set; }
public int LibraryId { get; set; }
public string Title { get; set; }
/// <summary>
/// Used internally only
/// </summary>
public int ReadingListId { get; set; }
}

View file

@ -1,9 +1,8 @@
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class UpdateReadingListByChapterDto
{
public class UpdateReadingListByChapterDto
{
public int ChapterId { get; init; }
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}
public int ChapterId { get; init; }
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}

View file

@ -1,12 +1,11 @@
using System.Collections.Generic;
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class UpdateReadingListByMultipleDto
{
public class UpdateReadingListByMultipleDto
{
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
public IReadOnlyList<int> VolumeIds { get; init; }
public IReadOnlyList<int> ChapterIds { get; init; }
}
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
public IReadOnlyList<int> VolumeIds { get; init; }
public IReadOnlyList<int> ChapterIds { get; init; }
}

View file

@ -1,10 +1,9 @@
using System.Collections.Generic;
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class UpdateReadingListByMultipleSeriesDto
{
public class UpdateReadingListByMultipleSeriesDto
{
public int ReadingListId { get; init; }
public IReadOnlyList<int> SeriesIds { get; init; }
}
public int ReadingListId { get; init; }
public IReadOnlyList<int> SeriesIds { get; init; }
}

View file

@ -1,8 +1,7 @@
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class UpdateReadingListBySeriesDto
{
public class UpdateReadingListBySeriesDto
{
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}

View file

@ -1,9 +1,8 @@
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class UpdateReadingListByVolumeDto
{
public class UpdateReadingListByVolumeDto
{
public int VolumeId { get; init; }
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}
public int VolumeId { get; init; }
public int SeriesId { get; init; }
public int ReadingListId { get; init; }
}

View file

@ -1,11 +1,10 @@
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
public class UpdateReadingListDto
{
public class UpdateReadingListDto
{
public int ReadingListId { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
public bool CoverImageLocked { get; set; }
}
public int ReadingListId { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public bool Promoted { get; set; }
public bool CoverImageLocked { get; set; }
}

View file

@ -1,18 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace API.DTOs.ReadingLists
namespace API.DTOs.ReadingLists;
/// <summary>
/// DTO for moving a reading list item to another position within the same list
/// </summary>
public class UpdateReadingListPosition
{
/// <summary>
/// DTO for moving a reading list item to another position within the same list
/// </summary>
public class UpdateReadingListPosition
{
[Required]
public int ReadingListId { get; set; }
[Required]
public int ReadingListItemId { get; set; }
public int FromPosition { get; set; }
[Required]
public int ToPosition { get; set; }
}
[Required] public int ReadingListId { get; set; }
[Required] public int ReadingListItemId { get; set; }
public int FromPosition { get; set; }
[Required] public int ToPosition { get; set; }
}

View file

@ -1,22 +1,21 @@
namespace API.DTOs
namespace API.DTOs;
/// <summary>
/// Used for running some task against a Series.
/// </summary>
public class RefreshSeriesDto
{
/// <summary>
/// Used for running some task against a Series.
/// Library Id series belongs to
/// </summary>
public class RefreshSeriesDto
{
/// <summary>
/// Library Id series belongs to
/// </summary>
public int LibraryId { get; init; }
/// <summary>
/// Series Id
/// </summary>
public int SeriesId { get; init; }
/// <summary>
/// Should the task force opening/re-calculation.
/// </summary>
/// <remarks>This is expensive if true. Defaults to true.</remarks>
public bool ForceUpdate { get; init; } = true;
}
public int LibraryId { get; init; }
/// <summary>
/// Series Id
/// </summary>
public int SeriesId { get; init; }
/// <summary>
/// Should the task force opening/re-calculation.
/// </summary>
/// <remarks>This is expensive if true. Defaults to true.</remarks>
public bool ForceUpdate { get; init; } = true;
}

View file

@ -1,15 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace API.DTOs
namespace API.DTOs;
public class RegisterDto
{
public class RegisterDto
{
[Required]
public string Username { get; init; }
[Required]
public string Email { get; init; }
[Required]
[StringLength(32, MinimumLength = 6)]
public string Password { get; set; }
}
[Required]
public string Username { get; init; }
[Required]
public string Email { get; init; }
[Required]
[StringLength(32, MinimumLength = 6)]
public string Password { get; set; }
}

View file

@ -1,18 +1,17 @@
using API.Entities.Enums;
namespace API.DTOs.Search
{
public class SearchResultDto
{
public int SeriesId { get; init; }
public string Name { get; init; }
public string OriginalName { get; init; }
public string SortName { get; init; }
public string LocalizedName { get; init; }
public MangaFormat Format { get; init; }
namespace API.DTOs.Search;
// Grouping information
public string LibraryName { get; set; }
public int LibraryId { get; set; }
}
public class SearchResultDto
{
public int SeriesId { get; init; }
public string Name { get; init; }
public string OriginalName { get; init; }
public string SortName { get; init; }
public string LocalizedName { get; init; }
public MangaFormat Format { get; init; }
// Grouping information
public string LibraryName { get; set; }
public int LibraryId { get; set; }
}

View file

@ -1,7 +1,6 @@
namespace API.DTOs
namespace API.DTOs;
public class SeriesByIdsDto
{
public class SeriesByIdsDto
{
public int[] SeriesIds { get; init; }
}
public int[] SeriesIds { get; init; }
}

View file

@ -2,65 +2,64 @@
using API.Entities.Enums;
using API.Entities.Interfaces;
namespace API.DTOs
namespace API.DTOs;
public class SeriesDto : IHasReadTimeEstimate
{
public class SeriesDto : IHasReadTimeEstimate
{
public int Id { get; init; }
public string Name { get; init; }
public string OriginalName { get; init; }
public string LocalizedName { get; init; }
public string SortName { get; init; }
public string Summary { get; init; }
public int Pages { get; init; }
public bool CoverImageLocked { get; set; }
/// <summary>
/// Sum of pages read from linked Volumes. Calculated at API-time.
/// </summary>
public int PagesRead { get; set; }
/// <summary>
/// DateTime representing last time the series was Read. Calculated at API-time.
/// </summary>
public DateTime LatestReadDate { get; set; }
/// <summary>
/// DateTime representing last time a chapter was added to the Series
/// </summary>
public DateTime LastChapterAdded { get; set; }
/// <summary>
/// Rating from logged in user. Calculated at API-time.
/// </summary>
public int UserRating { get; set; }
/// <summary>
/// Review from logged in user. Calculated at API-time.
/// </summary>
public string UserReview { get; set; }
public MangaFormat Format { get; set; }
public int Id { get; init; }
public string Name { get; init; }
public string OriginalName { get; init; }
public string LocalizedName { get; init; }
public string SortName { get; init; }
public string Summary { get; init; }
public int Pages { get; init; }
public bool CoverImageLocked { get; set; }
/// <summary>
/// Sum of pages read from linked Volumes. Calculated at API-time.
/// </summary>
public int PagesRead { get; set; }
/// <summary>
/// DateTime representing last time the series was Read. Calculated at API-time.
/// </summary>
public DateTime LatestReadDate { get; set; }
/// <summary>
/// DateTime representing last time a chapter was added to the Series
/// </summary>
public DateTime LastChapterAdded { get; set; }
/// <summary>
/// Rating from logged in user. Calculated at API-time.
/// </summary>
public int UserRating { get; set; }
/// <summary>
/// Review from logged in user. Calculated at API-time.
/// </summary>
public string UserReview { get; set; }
public MangaFormat Format { get; set; }
public DateTime Created { get; set; }
public DateTime Created { get; set; }
public bool NameLocked { get; set; }
public bool SortNameLocked { get; set; }
public bool LocalizedNameLocked { get; set; }
/// <summary>
/// Total number of words for the series. Only applies to epubs.
/// </summary>
public long WordCount { get; set; }
public bool NameLocked { get; set; }
public bool SortNameLocked { get; set; }
public bool LocalizedNameLocked { get; set; }
/// <summary>
/// Total number of words for the series. Only applies to epubs.
/// </summary>
public long WordCount { get; set; }
public int LibraryId { get; set; }
public string LibraryName { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
public int MinHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
public int MaxHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
public int AvgHoursToRead { get; set; }
/// <summary>
/// The highest level folder for this Series
/// </summary>
public string FolderPath { get; set; }
/// <summary>
/// The last time the folder for this series was scanned
/// </summary>
public DateTime LastFolderScanned { get; set; }
}
public int LibraryId { get; set; }
public string LibraryName { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
public int MinHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
public int MaxHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
public int AvgHoursToRead { get; set; }
/// <summary>
/// The highest level folder for this Series
/// </summary>
public string FolderPath { get; set; }
/// <summary>
/// The last time the folder for this series was scanned
/// </summary>
public DateTime LastFolderScanned { get; set; }
}

View file

@ -4,83 +4,82 @@ using API.DTOs.CollectionTags;
using API.DTOs.Metadata;
using API.Entities.Enums;
namespace API.DTOs
namespace API.DTOs;
public class SeriesMetadataDto
{
public class SeriesMetadataDto
{
public int Id { get; set; }
public string Summary { get; set; } = string.Empty;
/// <summary>
/// Collections the Series belongs to
/// </summary>
public ICollection<CollectionTagDto> CollectionTags { get; set; }
/// <summary>
/// Genres for the Series
/// </summary>
public ICollection<GenreTagDto> Genres { get; set; }
/// <summary>
/// Collection of all Tags from underlying chapters for a Series
/// </summary>
public ICollection<TagDto> Tags { get; set; }
public ICollection<PersonDto> Writers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> CoverArtists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Publishers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
/// <summary>
/// Highest Age Rating from all Chapters
/// </summary>
public AgeRating AgeRating { get; set; } = AgeRating.Unknown;
/// <summary>
/// Earliest Year from all chapters
/// </summary>
public int ReleaseYear { get; set; }
/// <summary>
/// Language of the content (BCP-47 code)
/// </summary>
public string Language { get; set; } = string.Empty;
/// <summary>
/// Max number of issues/volumes in the series (Max of Volume/Issue field in ComicInfo)
/// </summary>
public int MaxCount { get; set; } = 0;
/// <summary>
/// Total number of issues/volumes for the series
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Publication status of the Series
/// </summary>
public PublicationStatus PublicationStatus { get; set; }
public int Id { get; set; }
public string Summary { get; set; } = string.Empty;
/// <summary>
/// Collections the Series belongs to
/// </summary>
public ICollection<CollectionTagDto> CollectionTags { get; set; }
/// <summary>
/// Genres for the Series
/// </summary>
public ICollection<GenreTagDto> Genres { get; set; }
/// <summary>
/// Collection of all Tags from underlying chapters for a Series
/// </summary>
public ICollection<TagDto> Tags { get; set; }
public ICollection<PersonDto> Writers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> CoverArtists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Publishers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
public ICollection<PersonDto> Translators { get; set; } = new List<PersonDto>();
/// <summary>
/// Highest Age Rating from all Chapters
/// </summary>
public AgeRating AgeRating { get; set; } = AgeRating.Unknown;
/// <summary>
/// Earliest Year from all chapters
/// </summary>
public int ReleaseYear { get; set; }
/// <summary>
/// Language of the content (BCP-47 code)
/// </summary>
public string Language { get; set; } = string.Empty;
/// <summary>
/// Max number of issues/volumes in the series (Max of Volume/Issue field in ComicInfo)
/// </summary>
public int MaxCount { get; set; } = 0;
/// <summary>
/// Total number of issues/volumes for the series
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Publication status of the Series
/// </summary>
public PublicationStatus PublicationStatus { get; set; }
public bool LanguageLocked { get; set; }
public bool SummaryLocked { get; set; }
/// <summary>
/// Locked by user so metadata updates from scan loop will not override AgeRating
/// </summary>
public bool AgeRatingLocked { get; set; }
/// <summary>
/// Locked by user so metadata updates from scan loop will not override PublicationStatus
/// </summary>
public bool PublicationStatusLocked { get; set; }
public bool GenresLocked { get; set; }
public bool TagsLocked { get; set; }
public bool WritersLocked { get; set; }
public bool CharactersLocked { get; set; }
public bool ColoristsLocked { get; set; }
public bool EditorsLocked { get; set; }
public bool InkersLocked { get; set; }
public bool LetterersLocked { get; set; }
public bool PencillersLocked { get; set; }
public bool PublishersLocked { get; set; }
public bool TranslatorsLocked { get; set; }
public bool CoverArtistsLocked { get; set; }
public bool LanguageLocked { get; set; }
public bool SummaryLocked { get; set; }
/// <summary>
/// Locked by user so metadata updates from scan loop will not override AgeRating
/// </summary>
public bool AgeRatingLocked { get; set; }
/// <summary>
/// Locked by user so metadata updates from scan loop will not override PublicationStatus
/// </summary>
public bool PublicationStatusLocked { get; set; }
public bool GenresLocked { get; set; }
public bool TagsLocked { get; set; }
public bool WritersLocked { get; set; }
public bool CharactersLocked { get; set; }
public bool ColoristsLocked { get; set; }
public bool EditorsLocked { get; set; }
public bool InkersLocked { get; set; }
public bool LetterersLocked { get; set; }
public bool PencillersLocked { get; set; }
public bool PublishersLocked { get; set; }
public bool TranslatorsLocked { get; set; }
public bool CoverArtistsLocked { get; set; }
public int SeriesId { get; set; }
}
public int SeriesId { get; set; }
}

View file

@ -1,64 +1,63 @@
using API.Services;
namespace API.DTOs.Settings
{
public class ServerSettingDto
{
public string CacheDirectory { get; set; }
public string TaskScan { get; set; }
/// <summary>
/// Logging level for server. Managed in appsettings.json.
/// </summary>
public string LoggingLevel { get; set; }
public string TaskBackup { get; set; }
/// <summary>
/// Port the server listens on. Managed in appsettings.json.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Allows anonymous information to be collected and sent to KavitaStats
/// </summary>
public bool AllowStatCollection { get; set; }
/// <summary>
/// Enables OPDS connections to be made to the server.
/// </summary>
public bool EnableOpds { get; set; }
/// <summary>
/// Base Url for the kavita. Requires restart to take effect.
/// </summary>
public string BaseUrl { get; set; }
/// <summary>
/// Where Bookmarks are stored.
/// </summary>
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="DirectoryService.BookmarkDirectory"/></remarks>
public string BookmarksDirectory { get; set; }
/// <summary>
/// Email service to use for the invite user flow, forgot password, etc.
/// </summary>
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
public string EmailServiceUrl { get; set; }
public string InstallVersion { get; set; }
/// <summary>
/// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs.
/// </summary>
public string InstallId { get; set; }
/// <summary>
/// If the server should save bookmarks as WebP encoding
/// </summary>
public bool ConvertBookmarkToWebP { get; set; }
/// <summary>
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
/// </summary>
public bool EnableSwaggerUi { get; set; }
namespace API.DTOs.Settings;
/// <summary>
/// The amount of Backups before cleanup
/// </summary>
/// <remarks>Value should be between 1 and 30</remarks>
public int TotalBackups { get; set; } = 30;
/// <summary>
/// If Kavita should watch the library folders and process changes
/// </summary>
public bool EnableFolderWatching { get; set; } = true;
}
public class ServerSettingDto
{
public string CacheDirectory { get; set; }
public string TaskScan { get; set; }
/// <summary>
/// Logging level for server. Managed in appsettings.json.
/// </summary>
public string LoggingLevel { get; set; }
public string TaskBackup { get; set; }
/// <summary>
/// Port the server listens on. Managed in appsettings.json.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Allows anonymous information to be collected and sent to KavitaStats
/// </summary>
public bool AllowStatCollection { get; set; }
/// <summary>
/// Enables OPDS connections to be made to the server.
/// </summary>
public bool EnableOpds { get; set; }
/// <summary>
/// Base Url for the kavita. Requires restart to take effect.
/// </summary>
public string BaseUrl { get; set; }
/// <summary>
/// Where Bookmarks are stored.
/// </summary>
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="DirectoryService.BookmarkDirectory"/></remarks>
public string BookmarksDirectory { get; set; }
/// <summary>
/// Email service to use for the invite user flow, forgot password, etc.
/// </summary>
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
public string EmailServiceUrl { get; set; }
public string InstallVersion { get; set; }
/// <summary>
/// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs.
/// </summary>
public string InstallId { get; set; }
/// <summary>
/// If the server should save bookmarks as WebP encoding
/// </summary>
public bool ConvertBookmarkToWebP { get; set; }
/// <summary>
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
/// </summary>
public bool EnableSwaggerUi { get; set; }
/// <summary>
/// The amount of Backups before cleanup
/// </summary>
/// <remarks>Value should be between 1 and 30</remarks>
public int TotalBackups { get; set; } = 30;
/// <summary>
/// If Kavita should watch the library folders and process changes
/// </summary>
public bool EnableFolderWatching { get; set; } = true;
}

View file

@ -1,123 +1,122 @@
using API.Entities.Enums;
namespace API.DTOs.Stats
namespace API.DTOs.Stats;
/// <summary>
/// Represents information about a Kavita Installation
/// </summary>
public class ServerInfoDto
{
/// <summary>
/// Represents information about a Kavita Installation
/// Unique Id that represents a unique install
/// </summary>
public class ServerInfoDto
{
/// <summary>
/// Unique Id that represents a unique install
/// </summary>
public string InstallId { get; set; }
public string Os { get; set; }
/// <summary>
/// If the Kavita install is using Docker
/// </summary>
public bool IsDocker { get; set; }
/// <summary>
/// Version of .NET instance is running
/// </summary>
public string DotnetVersion { get; set; }
/// <summary>
/// Version of Kavita
/// </summary>
public string KavitaVersion { get; set; }
/// <summary>
/// Number of Cores on the instance
/// </summary>
public int NumOfCores { get; set; }
/// <summary>
/// The number of libraries on the instance
/// </summary>
public int NumberOfLibraries { get; set; }
/// <summary>
/// Does any user have bookmarks
/// </summary>
public bool HasBookmarks { get; set; }
/// <summary>
/// The site theme the install is using
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public string ActiveSiteTheme { get; set; }
/// <summary>
/// The reading mode the main user has as a preference
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public ReaderMode MangaReaderMode { get; set; }
public string InstallId { get; set; }
public string Os { get; set; }
/// <summary>
/// If the Kavita install is using Docker
/// </summary>
public bool IsDocker { get; set; }
/// <summary>
/// Version of .NET instance is running
/// </summary>
public string DotnetVersion { get; set; }
/// <summary>
/// Version of Kavita
/// </summary>
public string KavitaVersion { get; set; }
/// <summary>
/// Number of Cores on the instance
/// </summary>
public int NumOfCores { get; set; }
/// <summary>
/// The number of libraries on the instance
/// </summary>
public int NumberOfLibraries { get; set; }
/// <summary>
/// Does any user have bookmarks
/// </summary>
public bool HasBookmarks { get; set; }
/// <summary>
/// The site theme the install is using
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public string ActiveSiteTheme { get; set; }
/// <summary>
/// The reading mode the main user has as a preference
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public ReaderMode MangaReaderMode { get; set; }
/// <summary>
/// Number of users on the install
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfUsers { get; set; }
/// <summary>
/// Number of users on the install
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfUsers { get; set; }
/// <summary>
/// Number of collections on the install
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfCollections { get; set; }
/// <summary>
/// Number of reading lists on the install (Sum of all users)
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfReadingLists { get; set; }
/// <summary>
/// Is OPDS enabled
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public bool OPDSEnabled { get; set; }
/// <summary>
/// Total number of files in the instance
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int TotalFiles { get; set; }
/// <summary>
/// Total number of Genres in the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int TotalGenres { get; set; }
/// <summary>
/// Total number of People in the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int TotalPeople { get; set; }
/// <summary>
/// Is this instance storing bookmarks as WebP
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public bool StoreBookmarksAsWebP { get; set; }
/// <summary>
/// Number of users on this instance using Card Layout
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int UsersOnCardLayout { get; set; }
/// <summary>
/// Number of users on this instance using List Layout
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int UsersOnListLayout { get; set; }
/// <summary>
/// Max number of Series for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxSeriesInALibrary { get; set; }
/// <summary>
/// Max number of Volumes for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxVolumesInASeries { get; set; }
/// <summary>
/// Max number of Chapters for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxChaptersInASeries { get; set; }
/// <summary>
/// Does this instance have relationships setup between series
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public bool UsingSeriesRelationships { get; set; }
/// <summary>
/// Number of collections on the install
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfCollections { get; set; }
/// <summary>
/// Number of reading lists on the install (Sum of all users)
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int NumberOfReadingLists { get; set; }
/// <summary>
/// Is OPDS enabled
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public bool OPDSEnabled { get; set; }
/// <summary>
/// Total number of files in the instance
/// </summary>
/// <remarks>Introduced in v0.5.2</remarks>
public int TotalFiles { get; set; }
/// <summary>
/// Total number of Genres in the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int TotalGenres { get; set; }
/// <summary>
/// Total number of People in the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int TotalPeople { get; set; }
/// <summary>
/// Is this instance storing bookmarks as WebP
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public bool StoreBookmarksAsWebP { get; set; }
/// <summary>
/// Number of users on this instance using Card Layout
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int UsersOnCardLayout { get; set; }
/// <summary>
/// Number of users on this instance using List Layout
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int UsersOnListLayout { get; set; }
/// <summary>
/// Max number of Series for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxSeriesInALibrary { get; set; }
/// <summary>
/// Max number of Volumes for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxVolumesInASeries { get; set; }
/// <summary>
/// Max number of Chapters for any library on the instance
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public int MaxChaptersInASeries { get; set; }
/// <summary>
/// Does this instance have relationships setup between series
/// </summary>
/// <remarks>Introduced in v0.5.4</remarks>
public bool UsingSeriesRelationships { get; set; }
}
}

View file

@ -1,42 +1,41 @@
namespace API.DTOs.Update
namespace API.DTOs.Update;
/// <summary>
/// Update Notification denoting a new release available for user to update to
/// </summary>
public class UpdateNotificationDto
{
/// <summary>
/// Update Notification denoting a new release available for user to update to
/// Current installed Version
/// </summary>
public class UpdateNotificationDto
{
/// <summary>
/// Current installed Version
/// </summary>
public string CurrentVersion { get; init; }
/// <summary>
/// Semver of the release version
/// <example>0.4.3</example>
/// </summary>
public string UpdateVersion { get; init; }
/// <summary>
/// Release body in HTML
/// </summary>
public string UpdateBody { get; init; }
/// <summary>
/// Title of the release
/// </summary>
public string UpdateTitle { get; init; }
/// <summary>
/// Github Url
/// </summary>
public string UpdateUrl { get; init; }
/// <summary>
/// If this install is within Docker
/// </summary>
public bool IsDocker { get; init; }
/// <summary>
/// Is this a pre-release
/// </summary>
public bool IsPrerelease { get; init; }
/// <summary>
/// Date of the publish
/// </summary>
public string PublishDate { get; init; }
}
public string CurrentVersion { get; init; }
/// <summary>
/// Semver of the release version
/// <example>0.4.3</example>
/// </summary>
public string UpdateVersion { get; init; }
/// <summary>
/// Release body in HTML
/// </summary>
public string UpdateBody { get; init; }
/// <summary>
/// Title of the release
/// </summary>
public string UpdateTitle { get; init; }
/// <summary>
/// Github Url
/// </summary>
public string UpdateUrl { get; init; }
/// <summary>
/// If this install is within Docker
/// </summary>
public bool IsDocker { get; init; }
/// <summary>
/// Is this a pre-release
/// </summary>
public bool IsPrerelease { get; init; }
/// <summary>
/// Date of the publish
/// </summary>
public string PublishDate { get; init; }
}

View file

@ -1,13 +1,12 @@
using System.Collections.Generic;
using API.Entities.Enums;
namespace API.DTOs
namespace API.DTOs;
public class UpdateLibraryDto
{
public class UpdateLibraryDto
{
public int Id { get; init; }
public string Name { get; init; }
public LibraryType Type { get; set; }
public IEnumerable<string> Folders { get; init; }
}
public int Id { get; init; }
public string Name { get; init; }
public LibraryType Type { get; set; }
public IEnumerable<string> Folders { get; init; }
}

View file

@ -1,10 +1,9 @@
using System.Collections.Generic;
namespace API.DTOs
namespace API.DTOs;
public class UpdateLibraryForUserDto
{
public class UpdateLibraryForUserDto
{
public string Username { get; init; }
public IEnumerable<LibraryDto> SelectedLibraries { get; init; }
}
}
public string Username { get; init; }
public IEnumerable<LibraryDto> SelectedLibraries { get; init; }
}

View file

@ -1,10 +1,9 @@
using System.Collections.Generic;
namespace API.DTOs
namespace API.DTOs;
public class UpdateRbsDto
{
public class UpdateRbsDto
{
public string Username { get; init; }
public IList<string> Roles { get; init; }
}
}
public string Username { get; init; }
public IList<string> Roles { get; init; }
}

View file

@ -1,15 +1,14 @@
namespace API.DTOs
{
public class UpdateSeriesDto
{
public int Id { get; init; }
public string Name { get; init; }
public string LocalizedName { get; init; }
public string SortName { get; init; }
public bool CoverImageLocked { get; set; }
namespace API.DTOs;
public bool NameLocked { get; set; }
public bool SortNameLocked { get; set; }
public bool LocalizedNameLocked { get; set; }
}
public class UpdateSeriesDto
{
public int Id { get; init; }
public string Name { get; init; }
public string LocalizedName { get; init; }
public string SortName { get; init; }
public bool CoverImageLocked { get; set; }
public bool NameLocked { get; set; }
public bool SortNameLocked { get; set; }
public bool LocalizedNameLocked { get; set; }
}

View file

@ -1,11 +1,10 @@
using System.Collections.Generic;
using API.DTOs.CollectionTags;
namespace API.DTOs
namespace API.DTOs;
public class UpdateSeriesMetadataDto
{
public class UpdateSeriesMetadataDto
{
public SeriesMetadataDto SeriesMetadata { get; set; }
public ICollection<CollectionTagDto> CollectionTags { get; set; }
}
public SeriesMetadataDto SeriesMetadata { get; set; }
public ICollection<CollectionTagDto> CollectionTags { get; set; }
}

View file

@ -1,12 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace API.DTOs
namespace API.DTOs;
public class UpdateSeriesRatingDto
{
public class UpdateSeriesRatingDto
{
public int SeriesId { get; init; }
public int UserRating { get; init; }
[MaxLength(1000)]
public string UserReview { get; init; }
}
}
public int SeriesId { get; init; }
public int UserRating { get; init; }
[MaxLength(1000)]
public string UserReview { get; init; }
}

View file

@ -1,14 +1,13 @@
namespace API.DTOs.Uploads
namespace API.DTOs.Uploads;
public class UploadFileDto
{
public class UploadFileDto
{
/// <summary>
/// Id of the Entity
/// </summary>
public int Id { get; set; }
/// <summary>
/// Base Url encoding of the file to upload from (can be null)
/// </summary>
public string Url { get; set; }
}
/// <summary>
/// Id of the Entity
/// </summary>
public int Id { get; set; }
/// <summary>
/// Base Url encoding of the file to upload from (can be null)
/// </summary>
public string Url { get; set; }
}

View file

@ -1,13 +1,12 @@

namespace API.DTOs
namespace API.DTOs;
public class UserDto
{
public class UserDto
{
public string Username { get; init; }
public string Email { get; init; }
public string Token { get; set; }
public string RefreshToken { get; set; }
public string ApiKey { get; init; }
public UserPreferencesDto Preferences { get; set; }
}
public string Username { get; init; }
public string Email { get; init; }
public string Token { get; set; }
public string RefreshToken { get; set; }
public string ApiKey { get; init; }
public UserPreferencesDto Preferences { get; set; }
}

View file

@ -5,116 +5,115 @@ using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
namespace API.DTOs
namespace API.DTOs;
public class UserPreferencesDto
{
public class UserPreferencesDto
{
/// <summary>
/// Manga Reader Option: What direction should the next/prev page buttons go
/// </summary>
[Required]
public ReadingDirection ReadingDirection { get; set; }
/// <summary>
/// Manga Reader Option: How should the image be scaled to screen
/// </summary>
[Required]
public ScalingOption ScalingOption { get; set; }
/// <summary>
/// Manga Reader Option: Which side of a split image should we show first
/// </summary>
[Required]
public PageSplitOption PageSplitOption { get; set; }
/// <summary>
/// Manga Reader Option: How the manga reader should perform paging or reading of the file
/// <example>
/// Webtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging
/// by clicking top/bottom sides of reader.
/// </example>
/// </summary>
[Required]
public ReaderMode ReaderMode { get; set; }
/// <summary>
/// Manga Reader Option: How many pages to display in the reader at once
/// </summary>
[Required]
public LayoutMode LayoutMode { get; set; }
/// <summary>
/// Manga Reader Option: Background color of the reader
/// </summary>
[Required]
public string BackgroundColor { get; set; } = "#000000";
/// <summary>
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
/// </summary>
[Required]
public bool AutoCloseMenu { get; set; }
/// <summary>
/// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change
/// </summary>
[Required]
public bool ShowScreenHints { get; set; } = true;
/// <summary>
/// Book Reader Option: Override extra Margin
/// </summary>
[Required]
public int BookReaderMargin { get; set; }
/// <summary>
/// Book Reader Option: Override line-height
/// </summary>
[Required]
public int BookReaderLineSpacing { get; set; }
/// <summary>
/// Book Reader Option: Override font size
/// </summary>
[Required]
public int BookReaderFontSize { get; set; }
/// <summary>
/// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override
/// </summary>
[Required]
public string BookReaderFontFamily { get; set; }
/// <summary>
/// Book Reader Option: Allows tapping on side of screens to paginate
/// </summary>
[Required]
public bool BookReaderTapToPaginate { get; set; }
/// <summary>
/// Book Reader Option: What direction should the next/prev page buttons go
/// </summary>
[Required]
public ReadingDirection BookReaderReadingDirection { get; set; }
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
[Required]
public SiteTheme Theme { get; set; }
[Required]
public string BookReaderThemeName { get; set; }
[Required]
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
/// <summary>
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
/// </summary>
/// <remarks>Defaults to false</remarks>
[Required]
public bool BookReaderImmersiveMode { get; set; } = false;
/// <summary>
/// Global Site Option: If the UI should layout items as Cards or List items
/// </summary>
/// <remarks>Defaults to Cards</remarks>
[Required]
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
/// <summary>
/// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already
/// </summary>
/// <remarks>Defaults to false</remarks>
[Required]
public bool BlurUnreadSummaries { get; set; } = false;
/// <summary>
/// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB.
/// </summary>
[Required]
public bool PromptForDownloadSize { get; set; } = true;
}
/// <summary>
/// Manga Reader Option: What direction should the next/prev page buttons go
/// </summary>
[Required]
public ReadingDirection ReadingDirection { get; set; }
/// <summary>
/// Manga Reader Option: How should the image be scaled to screen
/// </summary>
[Required]
public ScalingOption ScalingOption { get; set; }
/// <summary>
/// Manga Reader Option: Which side of a split image should we show first
/// </summary>
[Required]
public PageSplitOption PageSplitOption { get; set; }
/// <summary>
/// Manga Reader Option: How the manga reader should perform paging or reading of the file
/// <example>
/// Webtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging
/// by clicking top/bottom sides of reader.
/// </example>
/// </summary>
[Required]
public ReaderMode ReaderMode { get; set; }
/// <summary>
/// Manga Reader Option: How many pages to display in the reader at once
/// </summary>
[Required]
public LayoutMode LayoutMode { get; set; }
/// <summary>
/// Manga Reader Option: Background color of the reader
/// </summary>
[Required]
public string BackgroundColor { get; set; } = "#000000";
/// <summary>
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
/// </summary>
[Required]
public bool AutoCloseMenu { get; set; }
/// <summary>
/// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change
/// </summary>
[Required]
public bool ShowScreenHints { get; set; } = true;
/// <summary>
/// Book Reader Option: Override extra Margin
/// </summary>
[Required]
public int BookReaderMargin { get; set; }
/// <summary>
/// Book Reader Option: Override line-height
/// </summary>
[Required]
public int BookReaderLineSpacing { get; set; }
/// <summary>
/// Book Reader Option: Override font size
/// </summary>
[Required]
public int BookReaderFontSize { get; set; }
/// <summary>
/// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override
/// </summary>
[Required]
public string BookReaderFontFamily { get; set; }
/// <summary>
/// Book Reader Option: Allows tapping on side of screens to paginate
/// </summary>
[Required]
public bool BookReaderTapToPaginate { get; set; }
/// <summary>
/// Book Reader Option: What direction should the next/prev page buttons go
/// </summary>
[Required]
public ReadingDirection BookReaderReadingDirection { get; set; }
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
[Required]
public SiteTheme Theme { get; set; }
[Required]
public string BookReaderThemeName { get; set; }
[Required]
public BookPageLayoutMode BookReaderLayoutMode { get; set; }
/// <summary>
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
/// </summary>
/// <remarks>Defaults to false</remarks>
[Required]
public bool BookReaderImmersiveMode { get; set; } = false;
/// <summary>
/// Global Site Option: If the UI should layout items as Cards or List items
/// </summary>
/// <remarks>Defaults to Cards</remarks>
[Required]
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
/// <summary>
/// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already
/// </summary>
/// <remarks>Defaults to false</remarks>
[Required]
public bool BlurUnreadSummaries { get; set; } = false;
/// <summary>
/// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB.
/// </summary>
[Required]
public bool PromptForDownloadSize { get; set; } = true;
}

View file

@ -4,26 +4,25 @@ using System.Collections.Generic;
using API.Entities;
using API.Entities.Interfaces;
namespace API.DTOs
namespace API.DTOs;
public class VolumeDto : IHasReadTimeEstimate
{
public class VolumeDto : IHasReadTimeEstimate
{
public int Id { get; set; }
/// <inheritdoc cref="Volume.Number"/>
public int Number { get; set; }
/// <inheritdoc cref="Volume.Name"/>
public string Name { get; set; }
public int Pages { get; set; }
public int PagesRead { get; set; }
public DateTime LastModified { get; set; }
public DateTime Created { get; set; }
public int SeriesId { get; set; }
public ICollection<ChapterDto> Chapters { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
public int MinHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
public int MaxHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
public int AvgHoursToRead { get; set; }
}
public int Id { get; set; }
/// <inheritdoc cref="Volume.Number"/>
public int Number { get; set; }
/// <inheritdoc cref="Volume.Name"/>
public string Name { get; set; }
public int Pages { get; set; }
public int PagesRead { get; set; }
public DateTime LastModified { get; set; }
public DateTime Created { get; set; }
public int SeriesId { get; set; }
public ICollection<ChapterDto> Chapters { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
public int MinHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
public int MaxHoursToRead { get; set; }
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
public int AvgHoursToRead { get; set; }
}

View file

@ -11,141 +11,140 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace API.Data
namespace API.Data;
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
IdentityRoleClaim<int>, IdentityUserToken<int>>
{
public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
IdentityRoleClaim<int>, IdentityUserToken<int>>
public DataContext(DbContextOptions options) : base(options)
{
public DataContext(DbContextOptions options) : base(options)
{
ChangeTracker.Tracked += OnEntityTracked;
ChangeTracker.StateChanged += OnEntityStateChanged;
}
public DbSet<Library> Library { get; set; }
public DbSet<Series> Series { get; set; }
public DbSet<Chapter> Chapter { get; set; }
public DbSet<Volume> Volume { get; set; }
public DbSet<AppUser> AppUser { get; set; }
public DbSet<MangaFile> MangaFile { get; set; }
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
public DbSet<AppUserRating> AppUserRating { get; set; }
public DbSet<ServerSetting> ServerSetting { get; set; }
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
public DbSet<CollectionTag> CollectionTag { get; set; }
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
public DbSet<ReadingList> ReadingList { get; set; }
public DbSet<ReadingListItem> ReadingListItem { get; set; }
public DbSet<Person> Person { get; set; }
public DbSet<Genre> Genre { get; set; }
public DbSet<Tag> Tag { get; set; }
public DbSet<SiteTheme> SiteTheme { get; set; }
public DbSet<SeriesRelation> SeriesRelation { get; set; }
public DbSet<FolderPath> FolderPath { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUser>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
builder.Entity<AppRole>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
builder.Entity<SeriesRelation>()
.HasOne(pt => pt.Series)
.WithMany(p => p.Relations)
.HasForeignKey(pt => pt.SeriesId)
.OnDelete(DeleteBehavior.ClientCascade);
builder.Entity<SeriesRelation>()
.HasOne(pt => pt.TargetSeries)
.WithMany(t => t.RelationOf)
.HasForeignKey(pt => pt.TargetSeriesId)
.OnDelete(DeleteBehavior.ClientCascade);
builder.Entity<AppUserPreferences>()
.Property(b => b.BookThemeName)
.HasDefaultValue("Dark");
builder.Entity<AppUserPreferences>()
.Property(b => b.BackgroundColor)
.HasDefaultValue("#000000");
builder.Entity<AppUserPreferences>()
.Property(b => b.GlobalPageLayoutMode)
.HasDefaultValue(PageLayoutMode.Cards);
}
private static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
{
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
{
entity.Created = DateTime.Now;
entity.LastModified = DateTime.Now;
}
}
private static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
{
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
entity.LastModified = DateTime.Now;
}
private void OnSaveChanges()
{
foreach (var saveEntity in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.Select(entry => entry.Entity)
.OfType<IHasConcurrencyToken>())
{
saveEntity.OnSavingChanges();
}
}
#region SaveChanges overrides
public override int SaveChanges()
{
this.OnSaveChanges();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
this.OnSaveChanges();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
this.OnSaveChanges();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
this.OnSaveChanges();
return base.SaveChangesAsync(cancellationToken);
}
#endregion
ChangeTracker.Tracked += OnEntityTracked;
ChangeTracker.StateChanged += OnEntityStateChanged;
}
public DbSet<Library> Library { get; set; }
public DbSet<Series> Series { get; set; }
public DbSet<Chapter> Chapter { get; set; }
public DbSet<Volume> Volume { get; set; }
public DbSet<AppUser> AppUser { get; set; }
public DbSet<MangaFile> MangaFile { get; set; }
public DbSet<AppUserProgress> AppUserProgresses { get; set; }
public DbSet<AppUserRating> AppUserRating { get; set; }
public DbSet<ServerSetting> ServerSetting { get; set; }
public DbSet<AppUserPreferences> AppUserPreferences { get; set; }
public DbSet<SeriesMetadata> SeriesMetadata { get; set; }
public DbSet<CollectionTag> CollectionTag { get; set; }
public DbSet<AppUserBookmark> AppUserBookmark { get; set; }
public DbSet<ReadingList> ReadingList { get; set; }
public DbSet<ReadingListItem> ReadingListItem { get; set; }
public DbSet<Person> Person { get; set; }
public DbSet<Genre> Genre { get; set; }
public DbSet<Tag> Tag { get; set; }
public DbSet<SiteTheme> SiteTheme { get; set; }
public DbSet<SeriesRelation> SeriesRelation { get; set; }
public DbSet<FolderPath> FolderPath { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUser>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
builder.Entity<AppRole>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
builder.Entity<SeriesRelation>()
.HasOne(pt => pt.Series)
.WithMany(p => p.Relations)
.HasForeignKey(pt => pt.SeriesId)
.OnDelete(DeleteBehavior.ClientCascade);
builder.Entity<SeriesRelation>()
.HasOne(pt => pt.TargetSeries)
.WithMany(t => t.RelationOf)
.HasForeignKey(pt => pt.TargetSeriesId)
.OnDelete(DeleteBehavior.ClientCascade);
builder.Entity<AppUserPreferences>()
.Property(b => b.BookThemeName)
.HasDefaultValue("Dark");
builder.Entity<AppUserPreferences>()
.Property(b => b.BackgroundColor)
.HasDefaultValue("#000000");
builder.Entity<AppUserPreferences>()
.Property(b => b.GlobalPageLayoutMode)
.HasDefaultValue(PageLayoutMode.Cards);
}
private static void OnEntityTracked(object sender, EntityTrackedEventArgs e)
{
if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity)
{
entity.Created = DateTime.Now;
entity.LastModified = DateTime.Now;
}
}
private static void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
{
if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity)
entity.LastModified = DateTime.Now;
}
private void OnSaveChanges()
{
foreach (var saveEntity in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.Select(entry => entry.Entity)
.OfType<IHasConcurrencyToken>())
{
saveEntity.OnSavingChanges();
}
}
#region SaveChanges overrides
public override int SaveChanges()
{
this.OnSaveChanges();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
this.OnSaveChanges();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
this.OnSaveChanges();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
this.OnSaveChanges();
return base.SaveChangesAsync(cancellationToken);
}
#endregion
}

View file

@ -9,162 +9,161 @@ using API.Extensions;
using API.Parser;
using API.Services.Tasks;
namespace API.Data
namespace API.Data;
/// <summary>
/// Responsible for creating Series, Volume, Chapter, MangaFiles for use in <see cref="ScannerService"/>
/// </summary>
public static class DbFactory
{
/// <summary>
/// Responsible for creating Series, Volume, Chapter, MangaFiles for use in <see cref="ScannerService"/>
/// </summary>
public static class DbFactory
public static Series Series(string name)
{
public static Series Series(string name)
return new Series
{
return new Series
{
Name = name,
OriginalName = name,
LocalizedName = name,
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
SortName = name,
Volumes = new List<Volume>(),
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
};
}
public static Series Series(string name, string localizedName)
{
if (string.IsNullOrEmpty(localizedName))
{
localizedName = name;
}
return new Series
{
Name = name,
OriginalName = name,
LocalizedName = localizedName,
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName),
SortName = name,
Volumes = new List<Volume>(),
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
};
}
public static Volume Volume(string volumeNumber)
{
return new Volume()
{
Name = volumeNumber,
Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
Chapters = new List<Chapter>()
};
}
public static Chapter Chapter(ParserInfo info)
{
var specialTreatment = info.IsSpecialInfo();
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
return new Chapter()
{
Number = specialTreatment ? "0" : Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty,
Range = specialTreatment ? info.Filename : info.Chapters,
Title = (specialTreatment && info.Format == MangaFormat.Epub)
? info.Title
: specialTitle,
Files = new List<MangaFile>(),
IsSpecial = specialTreatment,
};
}
public static SeriesMetadata SeriesMetadata(ComicInfo info)
{
return SeriesMetadata(Array.Empty<CollectionTag>());
}
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
{
return new SeriesMetadata()
{
CollectionTags = collectionTags,
Summary = string.Empty
};
}
public static CollectionTag CollectionTag(int id, string title, string summary, bool promoted)
{
return new CollectionTag()
{
Id = id,
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
Title = title?.Trim(),
Summary = summary?.Trim(),
Promoted = promoted
};
}
public static ReadingList ReadingList(string title, string summary, bool promoted)
{
return new ReadingList()
{
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
Title = title?.Trim(),
Summary = summary?.Trim(),
Promoted = promoted,
Items = new List<ReadingListItem>()
};
}
public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId)
{
return new ReadingListItem()
{
Order = index,
ChapterId = chapterId,
SeriesId = seriesId,
VolumeId = volumeId
};
}
public static Genre Genre(string name, bool external)
{
return new Genre()
{
Title = name.Trim().SentenceCase(),
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
ExternalTag = external
};
}
public static Tag Tag(string name, bool external)
{
return new Tag()
{
Title = name.Trim().SentenceCase(),
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
ExternalTag = external
};
}
public static Person Person(string name, PersonRole role)
{
return new Person()
{
Name = name.Trim(),
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
Role = role
};
}
public static MangaFile MangaFile(string filePath, MangaFormat format, int pages)
{
return new MangaFile()
{
FilePath = filePath,
Format = format,
Pages = pages,
LastModified = File.GetLastWriteTime(filePath) // NOTE: Changed this from DateTime.Now
};
}
Name = name,
OriginalName = name,
LocalizedName = name,
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
SortName = name,
Volumes = new List<Volume>(),
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
};
}
public static Series Series(string name, string localizedName)
{
if (string.IsNullOrEmpty(localizedName))
{
localizedName = name;
}
return new Series
{
Name = name,
OriginalName = name,
LocalizedName = localizedName,
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(localizedName),
SortName = name,
Volumes = new List<Volume>(),
Metadata = SeriesMetadata(Array.Empty<CollectionTag>())
};
}
public static Volume Volume(string volumeNumber)
{
return new Volume()
{
Name = volumeNumber,
Number = (int) Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(volumeNumber),
Chapters = new List<Chapter>()
};
}
public static Chapter Chapter(ParserInfo info)
{
var specialTreatment = info.IsSpecialInfo();
var specialTitle = specialTreatment ? info.Filename : info.Chapters;
return new Chapter()
{
Number = specialTreatment ? "0" : Services.Tasks.Scanner.Parser.Parser.MinNumberFromRange(info.Chapters) + string.Empty,
Range = specialTreatment ? info.Filename : info.Chapters,
Title = (specialTreatment && info.Format == MangaFormat.Epub)
? info.Title
: specialTitle,
Files = new List<MangaFile>(),
IsSpecial = specialTreatment,
};
}
public static SeriesMetadata SeriesMetadata(ComicInfo info)
{
return SeriesMetadata(Array.Empty<CollectionTag>());
}
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
{
return new SeriesMetadata()
{
CollectionTags = collectionTags,
Summary = string.Empty
};
}
public static CollectionTag CollectionTag(int id, string title, string summary, bool promoted)
{
return new CollectionTag()
{
Id = id,
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
Title = title?.Trim(),
Summary = summary?.Trim(),
Promoted = promoted
};
}
public static ReadingList ReadingList(string title, string summary, bool promoted)
{
return new ReadingList()
{
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(title?.Trim()).ToUpper(),
Title = title?.Trim(),
Summary = summary?.Trim(),
Promoted = promoted,
Items = new List<ReadingListItem>()
};
}
public static ReadingListItem ReadingListItem(int index, int seriesId, int volumeId, int chapterId)
{
return new ReadingListItem()
{
Order = index,
ChapterId = chapterId,
SeriesId = seriesId,
VolumeId = volumeId
};
}
public static Genre Genre(string name, bool external)
{
return new Genre()
{
Title = name.Trim().SentenceCase(),
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
ExternalTag = external
};
}
public static Tag Tag(string name, bool external)
{
return new Tag()
{
Title = name.Trim().SentenceCase(),
NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
ExternalTag = external
};
}
public static Person Person(string name, PersonRole role)
{
return new Person()
{
Name = name.Trim(),
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(name),
Role = role
};
}
public static MangaFile MangaFile(string filePath, MangaFormat format, int pages)
{
return new MangaFile()
{
FilePath = filePath,
Format = format,
Pages = pages,
LastModified = File.GetLastWriteTime(filePath) // NOTE: Changed this from DateTime.Now
};
}
}

View file

@ -1,9 +0,0 @@
namespace API.Data
{
public class LogLevelOptions
{
public const string Logging = "LogLevel";
public string Default { get; set; }
}
}

View file

@ -3,122 +3,121 @@ using System.Linq;
using API.Entities.Enums;
using Kavita.Common.Extensions;
namespace API.Data.Metadata
namespace API.Data.Metadata;
/// <summary>
/// A representation of a ComicInfo.xml file
/// </summary>
/// <remarks>See reference of the loose spec here: https://anansi-project.github.io/docs/comicinfo/documentation</remarks>
public class ComicInfo
{
public string Summary { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Series { get; set; } = string.Empty;
/// <summary>
/// A representation of a ComicInfo.xml file
/// Localized Series name. Not standard.
/// </summary>
/// <remarks>See reference of the loose spec here: https://anansi-project.github.io/docs/comicinfo/documentation</remarks>
public class ComicInfo
public string LocalizedSeries { get; set; } = string.Empty;
public string SeriesSort { get; set; } = string.Empty;
public string Number { get; set; } = string.Empty;
/// <summary>
/// The total number of items in the series.
/// </summary>
public int Count { get; set; } = 0;
public string Volume { get; set; } = string.Empty;
public string Notes { get; set; } = string.Empty;
public string Genre { get; set; } = string.Empty;
public int PageCount { get; set; }
// ReSharper disable once InconsistentNaming
/// <summary>
/// IETF BCP 47 Code to represent the language of the content
/// </summary>
public string LanguageISO { get; set; } = string.Empty;
/// <summary>
/// This is the link to where the data was scraped from
/// </summary>
public string Web { get; set; } = string.Empty;
public int Day { get; set; } = 0;
public int Month { get; set; } = 0;
public int Year { get; set; } = 0;
/// <summary>
/// Rating based on the content. Think PG-13, R for movies. See <see cref="AgeRating"/> for valid types
/// </summary>
public string AgeRating { get; set; } = string.Empty;
/// <summary>
/// User's rating of the content
/// </summary>
public float UserRating { get; set; }
public string StoryArc { get; set; } = string.Empty;
public string SeriesGroup { get; set; } = string.Empty;
public string AlternateNumber { get; set; } = string.Empty;
public int AlternateCount { get; set; } = 0;
public string AlternateSeries { get; set; } = string.Empty;
/// <summary>
/// This is Epub only: calibre:title_sort
/// Represents the sort order for the title
/// </summary>
public string TitleSort { get; set; } = string.Empty;
/// <summary>
/// This comes from ComicInfo and is free form text. We use this to validate against a set of tags and mark a file as
/// special.
/// </summary>
public string Format { get; set; } = string.Empty;
/// <summary>
/// The translator, can be comma separated. This is part of ComicInfo.xml draft v2.1
/// </summary>
/// See https://github.com/anansi-project/comicinfo/issues/2 for information about this tag
public string Translator { get; set; } = string.Empty;
/// <summary>
/// Misc tags. This is part of ComicInfo.xml draft v2.1
/// </summary>
/// See https://github.com/anansi-project/comicinfo/issues/1 for information about this tag
public string Tags { get; set; } = string.Empty;
/// <summary>
/// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple.
/// </summary>
public string Writer { get; set; } = string.Empty;
public string Penciller { get; set; } = string.Empty;
public string Inker { get; set; } = string.Empty;
public string Colorist { get; set; } = string.Empty;
public string Letterer { get; set; } = string.Empty;
public string CoverArtist { get; set; } = string.Empty;
public string Editor { get; set; } = string.Empty;
public string Publisher { get; set; } = string.Empty;
public string Characters { get; set; } = string.Empty;
public static AgeRating ConvertAgeRatingToEnum(string value)
{
public string Summary { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Series { get; set; } = string.Empty;
/// <summary>
/// Localized Series name. Not standard.
/// </summary>
public string LocalizedSeries { get; set; } = string.Empty;
public string SeriesSort { get; set; } = string.Empty;
public string Number { get; set; } = string.Empty;
/// <summary>
/// The total number of items in the series.
/// </summary>
public int Count { get; set; } = 0;
public string Volume { get; set; } = string.Empty;
public string Notes { get; set; } = string.Empty;
public string Genre { get; set; } = string.Empty;
public int PageCount { get; set; }
// ReSharper disable once InconsistentNaming
/// <summary>
/// IETF BCP 47 Code to represent the language of the content
/// </summary>
public string LanguageISO { get; set; } = string.Empty;
/// <summary>
/// This is the link to where the data was scraped from
/// </summary>
public string Web { get; set; } = string.Empty;
public int Day { get; set; } = 0;
public int Month { get; set; } = 0;
public int Year { get; set; } = 0;
/// <summary>
/// Rating based on the content. Think PG-13, R for movies. See <see cref="AgeRating"/> for valid types
/// </summary>
public string AgeRating { get; set; } = string.Empty;
/// <summary>
/// User's rating of the content
/// </summary>
public float UserRating { get; set; }
public string StoryArc { get; set; } = string.Empty;
public string SeriesGroup { get; set; } = string.Empty;
public string AlternateNumber { get; set; } = string.Empty;
public int AlternateCount { get; set; } = 0;
public string AlternateSeries { get; set; } = string.Empty;
/// <summary>
/// This is Epub only: calibre:title_sort
/// Represents the sort order for the title
/// </summary>
public string TitleSort { get; set; } = string.Empty;
/// <summary>
/// This comes from ComicInfo and is free form text. We use this to validate against a set of tags and mark a file as
/// special.
/// </summary>
public string Format { get; set; } = string.Empty;
/// <summary>
/// The translator, can be comma separated. This is part of ComicInfo.xml draft v2.1
/// </summary>
/// See https://github.com/anansi-project/comicinfo/issues/2 for information about this tag
public string Translator { get; set; } = string.Empty;
/// <summary>
/// Misc tags. This is part of ComicInfo.xml draft v2.1
/// </summary>
/// See https://github.com/anansi-project/comicinfo/issues/1 for information about this tag
public string Tags { get; set; } = string.Empty;
/// <summary>
/// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple.
/// </summary>
public string Writer { get; set; } = string.Empty;
public string Penciller { get; set; } = string.Empty;
public string Inker { get; set; } = string.Empty;
public string Colorist { get; set; } = string.Empty;
public string Letterer { get; set; } = string.Empty;
public string CoverArtist { get; set; } = string.Empty;
public string Editor { get; set; } = string.Empty;
public string Publisher { get; set; } = string.Empty;
public string Characters { get; set; } = string.Empty;
public static AgeRating ConvertAgeRatingToEnum(string value)
{
if (string.IsNullOrEmpty(value)) return Entities.Enums.AgeRating.Unknown;
return Enum.GetValues<AgeRating>()
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
}
public static void CleanComicInfo(ComicInfo info)
{
if (info == null) return;
info.Series = info.Series.Trim();
info.SeriesSort = info.SeriesSort.Trim();
info.LocalizedSeries = info.LocalizedSeries.Trim();
info.Writer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Writer);
info.Colorist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Colorist);
info.Editor = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Editor);
info.Inker = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Inker);
info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer);
info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller);
info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher);
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
}
if (string.IsNullOrEmpty(value)) return Entities.Enums.AgeRating.Unknown;
return Enum.GetValues<AgeRating>()
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
}
public static void CleanComicInfo(ComicInfo info)
{
if (info == null) return;
info.Series = info.Series.Trim();
info.SeriesSort = info.SeriesSort.Trim();
info.LocalizedSeries = info.LocalizedSeries.Trim();
info.Writer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Writer);
info.Colorist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Colorist);
info.Editor = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Editor);
info.Inker = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Inker);
info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer);
info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller);
info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher);
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
}
}

View file

@ -1,168 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using API.Services;
using Kavita.Common;
namespace API.Data
{
/// <summary>
/// A Migration to migrate config related files to the config/ directory for installs prior to v0.4.9.
/// </summary>
public static class MigrateConfigFiles
{
private static readonly List<string> LooseLeafFiles = new List<string>()
{
"appsettings.json",
"appsettings.Development.json",
"kavita.db",
};
private static readonly List<string> AppFolders = new List<string>()
{
"covers",
"stats",
"logs",
"backups",
"cache",
"temp"
};
/// <summary>
/// In v0.4.8 we moved all config files to config/ to match with how docker was setup. This will move all config files from current directory
/// to config/
/// </summary>
public static void Migrate(bool isDocker, IDirectoryService directoryService)
{
Console.WriteLine("Checking if migration to config/ is needed");
if (isDocker)
{
if (Configuration.LogPath.Contains("config"))
{
Console.WriteLine("Migration to config/ not needed");
return;
}
Console.WriteLine(
"Migrating files from pre-v0.4.8. All Kavita config files are now located in config/");
CopyAppFolders(directoryService);
DeleteAppFolders(directoryService);
UpdateConfiguration();
Console.WriteLine("Migration complete. All config files are now in config/ directory");
return;
}
if (new FileInfo(Configuration.AppSettingsFilename).Exists)
{
Console.WriteLine("Migration to config/ not needed");
return;
}
Console.WriteLine(
"Migrating files from pre-v0.4.8. All Kavita config files are now located in config/");
Console.WriteLine($"Creating {directoryService.ConfigDirectory}");
directoryService.ExistOrCreate(directoryService.ConfigDirectory);
try
{
CopyLooseLeafFiles(directoryService);
CopyAppFolders(directoryService);
// Then we need to update the config file to point to the new DB file
UpdateConfiguration();
}
catch (Exception)
{
Console.WriteLine("There was an exception during migration. Please move everything manually.");
return;
}
// Finally delete everything in the source directory
Console.WriteLine("Removing old files");
DeleteLooseFiles(directoryService);
DeleteAppFolders(directoryService);
Console.WriteLine("Removing old files...DONE");
Console.WriteLine("Migration complete. All config files are now in config/ directory");
}
private static void DeleteAppFolders(IDirectoryService directoryService)
{
foreach (var folderToDelete in AppFolders)
{
if (!new DirectoryInfo(Path.Join(Directory.GetCurrentDirectory(), folderToDelete)).Exists) continue;
directoryService.ClearAndDeleteDirectory(Path.Join(Directory.GetCurrentDirectory(), folderToDelete));
}
}
private static void DeleteLooseFiles(IDirectoryService directoryService)
{
var configFiles = LooseLeafFiles.Select(file => new FileInfo(Path.Join(Directory.GetCurrentDirectory(), file)))
.Where(f => f.Exists);
directoryService.DeleteFiles(configFiles.Select(f => f.FullName));
}
private static void CopyAppFolders(IDirectoryService directoryService)
{
Console.WriteLine("Moving folders to config");
foreach (var folderToMove in AppFolders)
{
if (new DirectoryInfo(Path.Join(directoryService.ConfigDirectory, folderToMove)).Exists) continue;
try
{
directoryService.CopyDirectoryToDirectory(
Path.Join(directoryService.FileSystem.Directory.GetCurrentDirectory(), folderToMove),
Path.Join(directoryService.ConfigDirectory, folderToMove));
}
catch (Exception)
{
/* Swallow Exception */
}
}
Console.WriteLine("Moving folders to config...DONE");
}
private static void CopyLooseLeafFiles(IDirectoryService directoryService)
{
var configFiles = LooseLeafFiles.Select(file => new FileInfo(Path.Join(directoryService.FileSystem.Directory.GetCurrentDirectory(), file)))
.Where(f => f.Exists);
// First step is to move all the files
Console.WriteLine("Moving files to config/");
foreach (var fileInfo in configFiles)
{
try
{
fileInfo.CopyTo(Path.Join(directoryService.ConfigDirectory, fileInfo.Name));
}
catch (Exception)
{
/* Swallow exception when already exists */
}
}
Console.WriteLine("Moving files to config...DONE");
}
private static void UpdateConfiguration()
{
Console.WriteLine("Updating appsettings.json to new paths");
Configuration.DatabasePath = "config//kavita.db";
Configuration.LogPath = "config//logs/kavita.log";
Console.WriteLine("Updating appsettings.json to new paths...DONE");
}
}
}

View file

@ -7,176 +7,175 @@ using API.Helpers;
using API.Services;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data;
/// <summary>
/// A data structure to migrate Cover Images from byte[] to files.
/// </summary>
internal class CoverMigration
{
/// <summary>
/// A data structure to migrate Cover Images from byte[] to files.
/// </summary>
internal class CoverMigration
{
public string Id { get; set; }
public byte[] CoverImage { get; set; }
public string ParentId { get; set; }
}
/// <summary>
/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work.
/// </summary>
public static class MigrateCoverImages
{
private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new ();
/// <summary>
/// Run first. Will extract byte[]s from DB and write them to the cover directory.
/// </summary>
public static void ExtractToImages(DbContext context, IDirectoryService directoryService, IImageService imageService)
{
Console.WriteLine("Migrating Cover Images to disk. Expect delay.");
directoryService.ExistOrCreate(directoryService.CoverImageDirectory);
Console.WriteLine("Extracting cover images for Series");
var lockedSeries = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From Series Where CoverImage IS NOT NULL", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = "0"
});
foreach (var series in lockedSeries)
{
if (series.CoverImage == null || !series.CoverImage.Any()) continue;
if (File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(int.Parse(series.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(series.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, ImageService.GetSeriesFormat(int.Parse(series.Id)), directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Chapters");
var chapters = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage, VolumeId From Chapter Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = x[2] + string.Empty
});
foreach (var chapter in chapters)
{
if (chapter.CoverImage == null || !chapter.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}.png"))) continue;
try
{
var stream = new MemoryStream(chapter.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Collection Tags");
var tags = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From CollectionTag Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1] ,
ParentId = "0"
});
foreach (var tag in tags)
{
if (tag.CoverImage == null || !tag.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(tag.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
/// <summary>
/// Run after <see cref="ExtractToImages"/>. Will update the DB with names of files that were extracted.
/// </summary>
/// <param name="context"></param>
public static async Task UpdateDatabaseWithImages(DataContext context, IDirectoryService directoryService)
{
Console.WriteLine("Updating Series entities");
var seriesCovers = await context.Series.Where(s => !string.IsNullOrEmpty(s.CoverImage)).ToListAsync();
foreach (var series in seriesCovers)
{
if (!directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(series.Id)}.png"))) continue;
series.CoverImage = $"{ImageService.GetSeriesFormat(series.Id)}.png";
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Chapter entities");
var chapters = await context.Chapter.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var chapter in chapters)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png")))
{
chapter.CoverImage = $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Volume entities");
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
foreach (var volume in volumes)
{
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting);
if (firstChapter == null) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
{
volume.CoverImage = $"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Collection Tag entities");
var tags = await context.CollectionTag.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var tag in tags)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(tag.Id)}.png")))
{
tag.CoverImage = $"{ImageService.GetCollectionTagFormat(tag.Id)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Cover Image Migration completed");
}
}
public string Id { get; set; }
public byte[] CoverImage { get; set; }
public string ParentId { get; set; }
}
/// <summary>
/// In v0.4.6, Cover Images were migrated from byte[] in the DB to external files. This migration handles that work.
/// </summary>
public static class MigrateCoverImages
{
private static readonly ChapterSortComparerZeroFirst ChapterSortComparerForInChapterSorting = new ();
/// <summary>
/// Run first. Will extract byte[]s from DB and write them to the cover directory.
/// </summary>
public static void ExtractToImages(DbContext context, IDirectoryService directoryService, IImageService imageService)
{
Console.WriteLine("Migrating Cover Images to disk. Expect delay.");
directoryService.ExistOrCreate(directoryService.CoverImageDirectory);
Console.WriteLine("Extracting cover images for Series");
var lockedSeries = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From Series Where CoverImage IS NOT NULL", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = "0"
});
foreach (var series in lockedSeries)
{
if (series.CoverImage == null || !series.CoverImage.Any()) continue;
if (File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(int.Parse(series.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(series.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, ImageService.GetSeriesFormat(int.Parse(series.Id)), directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Chapters");
var chapters = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage, VolumeId From Chapter Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1],
ParentId = x[2] + string.Empty
});
foreach (var chapter in chapters)
{
if (chapter.CoverImage == null || !chapter.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}.png"))) continue;
try
{
var stream = new MemoryStream(chapter.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetChapterFormat(int.Parse(chapter.Id), int.Parse(chapter.ParentId))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Extracting cover images for Collection Tags");
var tags = SqlHelper.RawSqlQuery(context, "Select Id, CoverImage From CollectionTag Where CoverImage IS NOT NULL;", x =>
new CoverMigration()
{
Id = x[0] + string.Empty,
CoverImage = (byte[]) x[1] ,
ParentId = "0"
});
foreach (var tag in tags)
{
if (tag.CoverImage == null || !tag.CoverImage.Any()) continue;
if (directoryService.FileSystem.File.Exists(Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}.png"))) continue;
try
{
var stream = new MemoryStream(tag.CoverImage);
stream.Position = 0;
imageService.WriteCoverThumbnail(stream, $"{ImageService.GetCollectionTagFormat(int.Parse(tag.Id))}", directoryService.CoverImageDirectory);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
/// <summary>
/// Run after <see cref="ExtractToImages"/>. Will update the DB with names of files that were extracted.
/// </summary>
/// <param name="context"></param>
public static async Task UpdateDatabaseWithImages(DataContext context, IDirectoryService directoryService)
{
Console.WriteLine("Updating Series entities");
var seriesCovers = await context.Series.Where(s => !string.IsNullOrEmpty(s.CoverImage)).ToListAsync();
foreach (var series in seriesCovers)
{
if (!directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetSeriesFormat(series.Id)}.png"))) continue;
series.CoverImage = $"{ImageService.GetSeriesFormat(series.Id)}.png";
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Chapter entities");
var chapters = await context.Chapter.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var chapter in chapters)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png")))
{
chapter.CoverImage = $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Volume entities");
var volumes = await context.Volume.Include(v => v.Chapters).ToListAsync();
foreach (var volume in volumes)
{
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerForInChapterSorting);
if (firstChapter == null) continue;
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png")))
{
volume.CoverImage = $"{ImageService.GetChapterFormat(firstChapter.Id, firstChapter.VolumeId)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Updating Collection Tag entities");
var tags = await context.CollectionTag.ToListAsync();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var tag in tags)
{
if (directoryService.FileSystem.File.Exists(directoryService.FileSystem.Path.Join(directoryService.CoverImageDirectory,
$"{ImageService.GetCollectionTagFormat(tag.Id)}.png")))
{
tag.CoverImage = $"{ImageService.GetCollectionTagFormat(tag.Id)}.png";
}
}
await context.SaveChangesAsync();
Console.WriteLine("Cover Image Migration completed");
}
}

View file

@ -1,21 +1,20 @@
namespace API.Data.Scanner
namespace API.Data.Scanner;
/// <summary>
/// Represents a set of Entities which is broken up and iterated on
/// </summary>
public class Chunk
{
/// <summary>
/// Represents a set of Entities which is broken up and iterated on
/// Total number of entities
/// </summary>
public class Chunk
{
/// <summary>
/// Total number of entities
/// </summary>
public int TotalSize { get; set; }
/// <summary>
/// Size of each chunk to iterate over
/// </summary>
public int ChunkSize { get; set; }
/// <summary>
/// Total chunks to iterate over
/// </summary>
public int TotalChunks { get; set; }
}
public int TotalSize { get; set; }
/// <summary>
/// Size of each chunk to iterate over
/// </summary>
public int ChunkSize { get; set; }
/// <summary>
/// Total chunks to iterate over
/// </summary>
public int TotalChunks { get; set; }
}

View file

@ -14,133 +14,130 @@ using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace API.Data
namespace API.Data;
public static class Seed
{
public static class Seed
/// <summary>
/// Generated on Startup. Seed.SeedSettings must run before
/// </summary>
public static ImmutableArray<ServerSetting> DefaultSettings;
public static readonly ImmutableArray<SiteTheme> DefaultThemes = ImmutableArray.Create(
new List<SiteTheme>
{
new()
{
Name = "Dark",
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize("Dark"),
Provider = ThemeProvider.System,
FileName = "dark.scss",
IsDefault = true,
}
}.ToArray());
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
{
/// <summary>
/// Generated on Startup. Seed.SeedSettings must run before
/// </summary>
public static ImmutableArray<ServerSetting> DefaultSettings;
var roles = typeof(PolicyConstants)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(f => f.FieldType == typeof(string))
.ToDictionary(f => f.Name,
f => (string) f.GetValue(null)).Values
.Select(policyName => new AppRole() {Name = policyName})
.ToList();
public static readonly ImmutableArray<SiteTheme> DefaultThemes = ImmutableArray.Create(
new List<SiteTheme>
{
new()
{
Name = "Dark",
NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize("Dark"),
Provider = ThemeProvider.System,
FileName = "dark.scss",
IsDefault = true,
}
}.ToArray());
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
foreach (var role in roles)
{
var roles = typeof(PolicyConstants)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(f => f.FieldType == typeof(string))
.ToDictionary(f => f.Name,
f => (string) f.GetValue(null)).Values
.Select(policyName => new AppRole() {Name = policyName})
.ToList();
foreach (var role in roles)
var exists = await roleManager.RoleExistsAsync(role.Name);
if (!exists)
{
var exists = await roleManager.RoleExistsAsync(role.Name);
if (!exists)
{
await roleManager.CreateAsync(role);
}
await roleManager.CreateAsync(role);
}
}
public static async Task SeedThemes(DataContext context)
{
await context.Database.EnsureCreatedAsync();
foreach (var theme in DefaultThemes)
{
var existing = context.SiteTheme.FirstOrDefault(s => s.Name.Equals(theme.Name));
if (existing == null)
{
await context.SiteTheme.AddAsync(theme);
}
}
await context.SaveChangesAsync();
}
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
{
await context.Database.EnsureCreatedAsync();
DefaultSettings = ImmutableArray.Create(new List<ServerSetting>()
{
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
new()
{
Key = ServerSettingKey.LoggingLevel, Value = "Information"
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
new()
{
Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)
},
new()
{
Key = ServerSettingKey.Port, Value = "5000"
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
new() {Key = ServerSettingKey.EnableOpds, Value = "false"},
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
}.ToArray());
foreach (var defaultSetting in DefaultSettings)
{
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
if (existing == null)
{
await context.ServerSetting.AddAsync(defaultSetting);
}
}
await context.SaveChangesAsync();
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
Configuration.Port + string.Empty;
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
Configuration.LogLevel + string.Empty;
context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value =
directoryService.CacheDirectory + string.Empty;
context.ServerSetting.First(s => s.Key == ServerSettingKey.BackupDirectory).Value =
DirectoryService.BackupDirectory + string.Empty;
await context.SaveChangesAsync();
}
public static async Task SeedUserApiKeys(DataContext context)
{
await context.Database.EnsureCreatedAsync();
var users = await context.AppUser.ToListAsync();
foreach (var user in users.Where(user => string.IsNullOrEmpty(user.ApiKey)))
{
user.ApiKey = HashUtil.ApiKey();
}
await context.SaveChangesAsync();
}
}
public static async Task SeedThemes(DataContext context)
{
await context.Database.EnsureCreatedAsync();
foreach (var theme in DefaultThemes)
{
var existing = context.SiteTheme.FirstOrDefault(s => s.Name.Equals(theme.Name));
if (existing == null)
{
await context.SiteTheme.AddAsync(theme);
}
}
await context.SaveChangesAsync();
}
public static async Task SeedSettings(DataContext context, IDirectoryService directoryService)
{
await context.Database.EnsureCreatedAsync();
DefaultSettings = ImmutableArray.Create(new List<ServerSetting>()
{
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
new()
{
Key = ServerSettingKey.LoggingLevel, Value = "Information"
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
new()
{
Key = ServerSettingKey.BackupDirectory, Value = Path.GetFullPath(DirectoryService.BackupDirectory)
},
new()
{
Key = ServerSettingKey.Port, Value = "5000"
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
new() {Key = ServerSettingKey.EnableOpds, Value = "false"},
new() {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
new() {Key = ServerSettingKey.BaseUrl, Value = "/"},
new() {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
new() {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
new() {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
new() {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
}.ToArray());
foreach (var defaultSetting in DefaultSettings)
{
var existing = context.ServerSetting.FirstOrDefault(s => s.Key == defaultSetting.Key);
if (existing == null)
{
await context.ServerSetting.AddAsync(defaultSetting);
}
}
await context.SaveChangesAsync();
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
Configuration.Port + string.Empty;
context.ServerSetting.First(s => s.Key == ServerSettingKey.CacheDirectory).Value =
directoryService.CacheDirectory + string.Empty;
context.ServerSetting.First(s => s.Key == ServerSettingKey.BackupDirectory).Value =
DirectoryService.BackupDirectory + string.Empty;
await context.SaveChangesAsync();
}
public static async Task SeedUserApiKeys(DataContext context)
{
await context.Database.EnsureCreatedAsync();
var users = await context.AppUser.ToListAsync();
foreach (var user in users.Where(user => string.IsNullOrEmpty(user.ApiKey)))
{
user.ApiKey = HashUtil.ApiKey();
}
await context.SaveChangesAsync();
}
}

View file

@ -1,10 +1,9 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
namespace API.Entities
namespace API.Entities;
public class AppRole : IdentityRole<int>
{
public class AppRole : IdentityRole<int>
{
public ICollection<AppUserRole> UserRoles { get; set; }
}
}
public ICollection<AppUserRole> UserRoles { get; set; }
}

View file

@ -5,45 +5,44 @@ using API.Entities.Interfaces;
using Microsoft.AspNetCore.Identity;
namespace API.Entities
namespace API.Entities;
public class AppUser : IdentityUser<int>, IHasConcurrencyToken
{
public class AppUser : IdentityUser<int>, IHasConcurrencyToken
public DateTime Created { get; set; } = DateTime.Now;
public DateTime LastActive { get; set; }
public ICollection<Library> Libraries { get; set; }
public ICollection<AppUserRole> UserRoles { get; set; }
public ICollection<AppUserProgress> Progresses { get; set; }
public ICollection<AppUserRating> Ratings { get; set; }
public AppUserPreferences UserPreferences { get; set; }
public ICollection<AppUserBookmark> Bookmarks { get; set; }
/// <summary>
/// Reading lists associated with this user
/// </summary>
public ICollection<ReadingList> ReadingLists { get; set; }
/// <summary>
/// A list of Series the user want's to read
/// </summary>
public ICollection<Series> WantToRead { get; set; }
/// <summary>
/// An API Key to interact with external services, like OPDS
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// The confirmation token for the user (invite). This will be set to null after the user confirms.
/// </summary>
public string ConfirmationToken { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
{
public DateTime Created { get; set; } = DateTime.Now;
public DateTime LastActive { get; set; }
public ICollection<Library> Libraries { get; set; }
public ICollection<AppUserRole> UserRoles { get; set; }
public ICollection<AppUserProgress> Progresses { get; set; }
public ICollection<AppUserRating> Ratings { get; set; }
public AppUserPreferences UserPreferences { get; set; }
public ICollection<AppUserBookmark> Bookmarks { get; set; }
/// <summary>
/// Reading lists associated with this user
/// </summary>
public ICollection<ReadingList> ReadingLists { get; set; }
/// <summary>
/// A list of Series the user want's to read
/// </summary>
public ICollection<Series> WantToRead { get; set; }
/// <summary>
/// An API Key to interact with external services, like OPDS
/// </summary>
public string ApiKey { get; set; }
/// <summary>
/// The confirmation token for the user (invite). This will be set to null after the user confirms.
/// </summary>
public string ConfirmationToken { get; set; }
/// <inheritdoc />
[ConcurrencyCheck]
public uint RowVersion { get; private set; }
/// <inheritdoc />
public void OnSavingChanges()
{
RowVersion++;
}
RowVersion++;
}
}

View file

@ -2,30 +2,29 @@
using System.Text.Json.Serialization;
using API.Entities.Interfaces;
namespace API.Entities
namespace API.Entities;
/// <summary>
/// Represents a saved page in a Chapter entity for a given user.
/// </summary>
public class AppUserBookmark : IEntityDate
{
public int Id { get; set; }
public int Page { get; set; }
public int VolumeId { get; set; }
public int SeriesId { get; set; }
public int ChapterId { get; set; }
/// <summary>
/// Represents a saved page in a Chapter entity for a given user.
/// Filename in the Bookmark Directory
/// </summary>
public class AppUserBookmark : IEntityDate
{
public int Id { get; set; }
public int Page { get; set; }
public int VolumeId { get; set; }
public int SeriesId { get; set; }
public int ChapterId { get; set; }
/// <summary>
/// Filename in the Bookmark Directory
/// </summary>
public string FileName { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
// Relationships
[JsonIgnore]
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
}
// Relationships
[JsonIgnore]
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
}

View file

@ -1,109 +1,108 @@
using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
namespace API.Entities
namespace API.Entities;
public class AppUserPreferences
{
public class AppUserPreferences
{
public int Id { get; set; }
/// <summary>
/// Manga Reader Option: What direction should the next/prev page buttons go
/// </summary>
public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight;
/// <summary>
/// Manga Reader Option: How should the image be scaled to screen
/// </summary>
public ScalingOption ScalingOption { get; set; } = ScalingOption.Automatic;
/// <summary>
/// Manga Reader Option: Which side of a split image should we show first
/// </summary>
public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.FitSplit;
/// <summary>
/// Manga Reader Option: How the manga reader should perform paging or reading of the file
/// <example>
/// Webtoon uses scrolling to page, MANGA_LR uses paging by clicking left/right side of reader, MANGA_UD uses paging
/// by clicking top/bottom sides of reader.
/// </example>
/// </summary>
public ReaderMode ReaderMode { get; set; }
public int Id { get; set; }
/// <summary>
/// Manga Reader Option: What direction should the next/prev page buttons go
/// </summary>
public ReadingDirection ReadingDirection { get; set; } = ReadingDirection.LeftToRight;
/// <summary>
/// Manga Reader Option: How should the image be scaled to screen
/// </summary>
public ScalingOption ScalingOption { get; set; } = ScalingOption.Automatic;
/// <summary>
/// Manga Reader Option: Which side of a split image should we show first
/// </summary>
public PageSplitOption PageSplitOption { get; set; } = PageSplitOption.FitSplit;
/// <summary>
/// Manga Reader Option: How the manga reader should perform paging or reading of the file
/// <example>
/// Webtoon uses scrolling to page, MANGA_LR uses paging by clicking left/right side of reader, MANGA_UD uses paging
/// by clicking top/bottom sides of reader.
/// </example>
/// </summary>
public ReaderMode ReaderMode { get; set; }
/// <summary>
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
/// </summary>
public bool AutoCloseMenu { get; set; } = true;
/// <summary>
/// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change
/// </summary>
public bool ShowScreenHints { get; set; } = true;
/// <summary>
/// Manga Reader Option: How many pages to display in the reader at once
/// </summary>
public LayoutMode LayoutMode { get; set; } = LayoutMode.Single;
/// <summary>
/// Manga Reader Option: Background color of the reader
/// </summary>
public string BackgroundColor { get; set; } = "#000000";
/// <summary>
/// Book Reader Option: Override extra Margin
/// </summary>
public int BookReaderMargin { get; set; } = 15;
/// <summary>
/// Book Reader Option: Override line-height
/// </summary>
public int BookReaderLineSpacing { get; set; } = 100;
/// <summary>
/// Book Reader Option: Override font size
/// </summary>
public int BookReaderFontSize { get; set; } = 100;
/// <summary>
/// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override
/// </summary>
public string BookReaderFontFamily { get; set; } = "default";
/// <summary>
/// Book Reader Option: Allows tapping on side of screens to paginate
/// </summary>
public bool BookReaderTapToPaginate { get; set; } = false;
/// <summary>
/// Book Reader Option: What direction should the next/prev page buttons go
/// </summary>
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
public SiteTheme Theme { get; set; }
/// <summary>
/// Book Reader Option: The color theme to decorate the book contents
/// </summary>
/// <remarks>Should default to Dark</remarks>
public string BookThemeName { get; set; } = "Dark";
/// <summary>
/// Book Reader Option: The way a page from a book is rendered. Default is as book dictates, 1 column is fit to height,
/// 2 column is fit to height, 2 columns
/// </summary>
/// <remarks>Defaults to Default</remarks>
public BookPageLayoutMode BookReaderLayoutMode { get; set; } = BookPageLayoutMode.Default;
/// <summary>
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BookReaderImmersiveMode { get; set; } = false;
/// <summary>
/// Global Site Option: If the UI should layout items as Cards or List items
/// </summary>
/// <remarks>Defaults to Cards</remarks>
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
/// <summary>
/// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BlurUnreadSummaries { get; set; } = false;
/// <summary>
/// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB.
/// </summary>
public bool PromptForDownloadSize { get; set; } = true;
/// <summary>
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
/// </summary>
public bool AutoCloseMenu { get; set; } = true;
/// <summary>
/// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change
/// </summary>
public bool ShowScreenHints { get; set; } = true;
/// <summary>
/// Manga Reader Option: How many pages to display in the reader at once
/// </summary>
public LayoutMode LayoutMode { get; set; } = LayoutMode.Single;
/// <summary>
/// Manga Reader Option: Background color of the reader
/// </summary>
public string BackgroundColor { get; set; } = "#000000";
/// <summary>
/// Book Reader Option: Override extra Margin
/// </summary>
public int BookReaderMargin { get; set; } = 15;
/// <summary>
/// Book Reader Option: Override line-height
/// </summary>
public int BookReaderLineSpacing { get; set; } = 100;
/// <summary>
/// Book Reader Option: Override font size
/// </summary>
public int BookReaderFontSize { get; set; } = 100;
/// <summary>
/// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override
/// </summary>
public string BookReaderFontFamily { get; set; } = "default";
/// <summary>
/// Book Reader Option: Allows tapping on side of screens to paginate
/// </summary>
public bool BookReaderTapToPaginate { get; set; } = false;
/// <summary>
/// Book Reader Option: What direction should the next/prev page buttons go
/// </summary>
public ReadingDirection BookReaderReadingDirection { get; set; } = ReadingDirection.LeftToRight;
/// <summary>
/// UI Site Global Setting: The UI theme the user should use.
/// </summary>
/// <remarks>Should default to Dark</remarks>
public SiteTheme Theme { get; set; }
/// <summary>
/// Book Reader Option: The color theme to decorate the book contents
/// </summary>
/// <remarks>Should default to Dark</remarks>
public string BookThemeName { get; set; } = "Dark";
/// <summary>
/// Book Reader Option: The way a page from a book is rendered. Default is as book dictates, 1 column is fit to height,
/// 2 column is fit to height, 2 columns
/// </summary>
/// <remarks>Defaults to Default</remarks>
public BookPageLayoutMode BookReaderLayoutMode { get; set; } = BookPageLayoutMode.Default;
/// <summary>
/// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this.
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BookReaderImmersiveMode { get; set; } = false;
/// <summary>
/// Global Site Option: If the UI should layout items as Cards or List items
/// </summary>
/// <remarks>Defaults to Cards</remarks>
public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards;
/// <summary>
/// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BlurUnreadSummaries { get; set; } = false;
/// <summary>
/// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB.
/// </summary>
public bool PromptForDownloadSize { get; set; } = true;
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
}
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }
}

View file

@ -2,56 +2,55 @@
using System;
using API.Entities.Interfaces;
namespace API.Entities
namespace API.Entities;
/// <summary>
/// Represents the progress a single user has on a given Chapter.
/// </summary>
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), nameof(AppUserId), IsUnique = true)]
public class AppUserProgress : IEntityDate
{
/// <summary>
/// Represents the progress a single user has on a given Chapter.
/// Id of Entity
/// </summary>
//[Index(nameof(SeriesId), nameof(VolumeId), nameof(ChapterId), nameof(AppUserId), IsUnique = true)]
public class AppUserProgress : IEntityDate
{
/// <summary>
/// Id of Entity
/// </summary>
public int Id { get; set; }
/// <summary>
/// Pages Read for given Chapter
/// </summary>
public int PagesRead { get; set; }
/// <summary>
/// Volume belonging to Chapter
/// </summary>
public int VolumeId { get; set; }
/// <summary>
/// Series belonging to Chapter
/// </summary>
public int SeriesId { get; set; }
/// <summary>
/// Chapter
/// </summary>
public int ChapterId { get; set; }
/// <summary>
/// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point
/// on next load
/// </summary>
public string BookScrollId { get; set; }
/// <summary>
/// When this was first created
/// </summary>
public DateTime Created { get; set; }
/// <summary>
/// Last date this was updated
/// </summary>
public DateTime LastModified { get; set; }
public int Id { get; set; }
/// <summary>
/// Pages Read for given Chapter
/// </summary>
public int PagesRead { get; set; }
/// <summary>
/// Volume belonging to Chapter
/// </summary>
public int VolumeId { get; set; }
/// <summary>
/// Series belonging to Chapter
/// </summary>
public int SeriesId { get; set; }
/// <summary>
/// Chapter
/// </summary>
public int ChapterId { get; set; }
/// <summary>
/// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point
/// on next load
/// </summary>
public string BookScrollId { get; set; }
/// <summary>
/// When this was first created
/// </summary>
public DateTime Created { get; set; }
/// <summary>
/// Last date this was updated
/// </summary>
public DateTime LastModified { get; set; }
// Relationships
/// <summary>
/// Navigational Property for EF. Links to a unique AppUser
/// </summary>
public AppUser AppUser { get; set; }
/// <summary>
/// User this progress belongs to
/// </summary>
public int AppUserId { get; set; }
}
// Relationships
/// <summary>
/// Navigational Property for EF. Links to a unique AppUser
/// </summary>
public AppUser AppUser { get; set; }
/// <summary>
/// User this progress belongs to
/// </summary>
public int AppUserId { get; set; }
}

Some files were not shown because too many files have changed in this diff Show more