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:
parent
9f715cc35f
commit
d1a14f7e68
212 changed files with 16599 additions and 16834 deletions
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
namespace API.Data
|
||||
{
|
||||
public class LogLevelOptions
|
||||
{
|
||||
public const string Logging = "LogLevel";
|
||||
|
||||
public string Default { get; set; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
239
API/Data/Seed.cs
239
API/Data/Seed.cs
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue