.NET 7 + Spring Cleaning (#1677)
* Updated to net7.0 * Updated GA to .net 7 * Updated System.IO.Abstractions to use New factory. * Converted Regex into SourceGenerator in Parser. * Updated more regex to source generators. * Enabled Nullability and more regex changes throughout codebase. * Parser is 100% GeneratedRegexified * Lots of nullability code * Enabled nullability for all repositories. * Fixed another unit test * Refactored some code around and took care of some todos. * Updating code for nullability and cleaning up methods that aren't used anymore. Refctored all uses of Parser.Normalize() to use new extension * More nullability exercises. 500 warnings to go. * Fixed a bug where custom file uploads for entities wouldn't save in webP. * Nullability is done for all DTOs * Fixed all unit tests and nullability for the project. Only OPDS is left which will be done with an upcoming OPDS enhancement. * Use localization in book service after validating * Code smells * Switched to preview build of swashbuckle for .net7 support * Fixed up merge issues * Disable emulate comic book when on single page reader * Fixed a regression where double page renderer wouldn't layout the images correctly * Updated to swashbuckle which support .net 7 * Fixed a bad GA action * Some code cleanup * More code smells * Took care of most of nullable issues * Fixed a broken test due to having more than one test run in parallel * I'm really not sure why the unit tests are failing or are so extremely slow on .net 7 * Updated all dependencies * Fixed up build and removed hardcoded framework from build scripts. (this merge removes Regex Source generators). Unit tests are completely busted. * Unit tests and code cleanup. Needs shakeout now. * Adjusted Series model since a few fields are not-nullable. Removed dead imports on the project. * Refactored to use Builder pattern for all unit tests. * Switched nullability down to warnings. It wasn't possible to switch due to constraint issues in DB Migration.
This commit is contained in:
parent
76fe3fd64a
commit
5d1dd7b3f0
283 changed files with 4221 additions and 4593 deletions
|
@ -21,8 +21,9 @@ public interface IAccountService
|
|||
Task<IEnumerable<ApiException>> ValidatePassword(AppUser user, string password);
|
||||
Task<IEnumerable<ApiException>> ValidateUsername(string username);
|
||||
Task<IEnumerable<ApiException>> ValidateEmail(string email);
|
||||
Task<bool> HasBookmarkPermission(AppUser user);
|
||||
Task<bool> HasDownloadPermission(AppUser user);
|
||||
Task<bool> HasBookmarkPermission(AppUser? user);
|
||||
Task<bool> HasDownloadPermission(AppUser? user);
|
||||
Task<bool> HasChangeRestrictionRole(AppUser? user);
|
||||
Task<bool> CheckIfAccessible(HttpRequest request);
|
||||
Task<string> GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true);
|
||||
}
|
||||
|
@ -137,8 +138,9 @@ public class AccountService : IAccountService
|
|||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> HasBookmarkPermission(AppUser user)
|
||||
public async Task<bool> HasBookmarkPermission(AppUser? user)
|
||||
{
|
||||
if (user == null) return false;
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
return roles.Contains(PolicyConstants.BookmarkRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||
}
|
||||
|
@ -148,8 +150,9 @@ public class AccountService : IAccountService
|
|||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> HasDownloadPermission(AppUser user)
|
||||
public async Task<bool> HasDownloadPermission(AppUser? user)
|
||||
{
|
||||
if (user == null) return false;
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||
}
|
||||
|
@ -159,8 +162,9 @@ public class AccountService : IAccountService
|
|||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> HasChangeRestrictionRole(AppUser user)
|
||||
public async Task<bool> HasChangeRestrictionRole(AppUser? user)
|
||||
{
|
||||
if (user == null) return false;
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
return roles.Contains(PolicyConstants.ChangePasswordRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ public interface IArchiveService
|
|||
int GetNumberOfPagesFromArchive(string archivePath);
|
||||
string GetCoverImage(string archivePath, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
bool IsValidArchive(string archivePath);
|
||||
ComicInfo GetComicInfo(string archivePath);
|
||||
ComicInfo? GetComicInfo(string archivePath);
|
||||
ArchiveLibrary CanOpen(string archivePath);
|
||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||
/// <summary>
|
||||
|
@ -129,7 +129,7 @@ public class ArchiveService : IArchiveService
|
|||
/// </summary>
|
||||
/// <param name="entryFullNames"></param>
|
||||
/// <returns>Entry name of match, null if no match</returns>
|
||||
public static string FindFolderEntry(IEnumerable<string> entryFullNames)
|
||||
public static string? FindFolderEntry(IEnumerable<string> entryFullNames)
|
||||
{
|
||||
var result = entryFullNames
|
||||
.Where(path => !(Path.EndsInDirectorySeparator(path) || Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith)))
|
||||
|
@ -163,7 +163,7 @@ public class ArchiveService : IArchiveService
|
|||
|
||||
// Check the first folder and sort within that to see if we can find a file, else fallback to first file with basic sort.
|
||||
// Get first folder, then sort within that
|
||||
var firstDirectoryFile = fullNames.OrderByNatural(Path.GetDirectoryName).FirstOrDefault();
|
||||
var firstDirectoryFile = fullNames.OrderByNatural(Path.GetDirectoryName!).FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(firstDirectoryFile))
|
||||
{
|
||||
var firstDirectory = Path.GetDirectoryName(firstDirectoryFile);
|
||||
|
@ -249,7 +249,7 @@ public class ArchiveService : IArchiveService
|
|||
/// <param name="archivePath"></param>
|
||||
/// <param name="entryNames"></param>
|
||||
/// <returns></returns>
|
||||
public static string FindCoverImageFilename(string archivePath, IEnumerable<string> entryNames)
|
||||
public static string? FindCoverImageFilename(string archivePath, IEnumerable<string> entryNames)
|
||||
{
|
||||
var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames, Path.GetFileName(archivePath));
|
||||
return entryName;
|
||||
|
@ -281,7 +281,7 @@ public class ArchiveService : IArchiveService
|
|||
var dateString = DateTime.UtcNow.ToShortDateString().Replace("/", "_");
|
||||
|
||||
var tempLocation = Path.Join(_directoryService.TempDirectory, $"{tempFolder}_{dateString}");
|
||||
var potentialExistingFile = _directoryService.FileSystem.FileInfo.FromFileName(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip"));
|
||||
var potentialExistingFile = _directoryService.FileSystem.FileInfo.New(Path.Join(_directoryService.TempDirectory, $"kavita_{tempFolder}_{dateString}.zip"));
|
||||
if (potentialExistingFile.Exists)
|
||||
{
|
||||
// A previous download exists, just return it immediately
|
||||
|
@ -331,8 +331,9 @@ public class ArchiveService : IArchiveService
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool IsComicInfoArchiveEntry(string fullName, string name)
|
||||
private static bool IsComicInfoArchiveEntry(string? fullName, string name)
|
||||
{
|
||||
if (fullName == null) return false;
|
||||
return !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(fullName)
|
||||
&& name.EndsWith(ComicInfoFilename, StringComparison.OrdinalIgnoreCase)
|
||||
&& !name.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith);
|
||||
|
@ -364,7 +365,7 @@ public class ArchiveService : IArchiveService
|
|||
{
|
||||
using var stream = entry.Open();
|
||||
var serializer = new XmlSerializer(typeof(ComicInfo));
|
||||
var info = (ComicInfo) serializer.Deserialize(stream);
|
||||
var info = (ComicInfo?) serializer.Deserialize(stream);
|
||||
ComicInfo.CleanComicInfo(info);
|
||||
return info;
|
||||
}
|
||||
|
@ -382,7 +383,7 @@ public class ArchiveService : IArchiveService
|
|||
{
|
||||
using var stream = entry.OpenEntryStream();
|
||||
var serializer = new XmlSerializer(typeof(ComicInfo));
|
||||
var info = (ComicInfo) serializer.Deserialize(stream);
|
||||
var info = (ComicInfo?) serializer.Deserialize(stream);
|
||||
ComicInfo.CleanComicInfo(info);
|
||||
return info;
|
||||
}
|
||||
|
|
|
@ -33,8 +33,17 @@ public interface IBookService
|
|||
{
|
||||
int GetNumberOfPages(string filePath);
|
||||
string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
ComicInfo GetComicInfo(string filePath);
|
||||
ParserInfo ParseInfo(string filePath);
|
||||
ComicInfo? GetComicInfo(string filePath);
|
||||
ParserInfo? ParseInfo(string filePath);
|
||||
/// <summary>
|
||||
/// Scopes styles to .reading-section and replaces img src to the passed apiBase
|
||||
/// </summary>
|
||||
/// <param name="stylesheetHtml"></param>
|
||||
/// <param name="apiBase"></param>
|
||||
/// <param name="filename">If the stylesheetHtml contains Import statements, when scoping the filename, scope needs to be wrt filepath.</param>
|
||||
/// <param name="book">Book Reference, needed for if you expect Import statements</param>
|
||||
/// <returns></returns>
|
||||
Task<string> ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book);
|
||||
/// <summary>
|
||||
/// Extracts a PDF file's pages as images to an target directory
|
||||
/// </summary>
|
||||
|
@ -64,6 +73,27 @@ public class BookService : IBookService
|
|||
}
|
||||
};
|
||||
|
||||
// Use when Rosyln fixed
|
||||
// [GeneratedRegex(@"/\*[\d\D]*?\*/", RegexOptions.Compiled)]
|
||||
// private static partial Regex CssComment();
|
||||
//
|
||||
// [GeneratedRegex(@"[a-zA-Z]+#", RegexOptions.Compiled)]
|
||||
// private static partial Regex WhiteSpace1();
|
||||
// [GeneratedRegex(@"[\n\r]+\s*", RegexOptions.Compiled)]
|
||||
// private static partial Regex WhiteSpace2();
|
||||
// [GeneratedRegex(@"\s+", RegexOptions.Compiled)]
|
||||
// private static partial Regex WhiteSpace3();
|
||||
// [GeneratedRegex(@"\s?([:,;{}])\s?", RegexOptions.Compiled)]
|
||||
// private static partial Regex WhiteSpace4();
|
||||
// [GeneratedRegex(@"([\s:]0)(px|pt|%|em)", RegexOptions.Compiled)]
|
||||
// private static partial Regex UnitPadding();
|
||||
//
|
||||
// [GeneratedRegex(@"<script(.*)(/>)", RegexOptions.Compiled)]
|
||||
// private static partial Regex StartingScriptTag();
|
||||
// [GeneratedRegex(@"<title(.*)(/>)", RegexOptions.Compiled)]
|
||||
// private static partial Regex StartingTitleTag();
|
||||
|
||||
|
||||
public BookService(ILogger<BookService> logger, IDirectoryService directoryService, IImageService imageService)
|
||||
{
|
||||
_logger = logger;
|
||||
|
@ -163,6 +193,7 @@ public class BookService : IBookService
|
|||
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be Scoped
|
||||
var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty;
|
||||
var importBuilder = new StringBuilder();
|
||||
//foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml))
|
||||
foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
|
@ -200,7 +231,7 @@ public class BookService : IBookService
|
|||
foreach (var styleRule in stylesheet.StyleRules)
|
||||
{
|
||||
if (styleRule.Selector.Text == CssScopeClass) continue;
|
||||
if (styleRule.Selector.Text.Contains(","))
|
||||
if (styleRule.Selector.Text.Contains(','))
|
||||
{
|
||||
styleRule.Text = styleRule.Text.Replace(styleRule.SelectorText,
|
||||
string.Join(", ",
|
||||
|
@ -214,6 +245,7 @@ public class BookService : IBookService
|
|||
|
||||
private static void EscapeCssImportReferences(ref string stylesheetHtml, string apiBase, string prepend)
|
||||
{
|
||||
//foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml))
|
||||
foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
|
@ -224,6 +256,7 @@ public class BookService : IBookService
|
|||
|
||||
private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend)
|
||||
{
|
||||
//foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex().Matches(stylesheetHtml))
|
||||
foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex.Matches(stylesheetHtml))
|
||||
{
|
||||
if (!match.Success) continue;
|
||||
|
@ -234,6 +267,7 @@ public class BookService : IBookService
|
|||
|
||||
private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book)
|
||||
{
|
||||
//var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex().Matches(stylesheetHtml);
|
||||
var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex.Matches(stylesheetHtml);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
|
@ -260,7 +294,7 @@ public class BookService : IBookService
|
|||
foreach (var image in images)
|
||||
{
|
||||
|
||||
string key = null;
|
||||
string? key = null;
|
||||
if (image.Attributes["src"] != null)
|
||||
{
|
||||
key = "src";
|
||||
|
@ -388,7 +422,7 @@ public class BookService : IBookService
|
|||
}
|
||||
}
|
||||
|
||||
public ComicInfo GetComicInfo(string filePath)
|
||||
public ComicInfo? GetComicInfo(string filePath)
|
||||
{
|
||||
if (!IsValidFile(filePath) || Tasks.Scanner.Parser.Parser.IsPdf(filePath)) return null;
|
||||
|
||||
|
@ -474,7 +508,7 @@ public class BookService : IBookService
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static (int year, int month, int day) GetPublicationDate(string publicationDate)
|
||||
{
|
||||
var dateParsed = DateTime.TryParse(publicationDate, out var date);
|
||||
|
@ -496,23 +530,19 @@ public class BookService : IBookService
|
|||
return (year, month, day);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static string ValidateLanguage(string? language)
|
||||
{
|
||||
if (string.IsNullOrEmpty(language)) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
CultureInfo.GetCultureInfo(language);
|
||||
return CultureInfo.GetCultureInfo(language).ToString();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return language;
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
private bool IsValidFile(string filePath)
|
||||
{
|
||||
|
@ -553,6 +583,8 @@ public class BookService : IBookService
|
|||
|
||||
private static string EscapeTags(string content)
|
||||
{
|
||||
// content = StartingScriptTag().Replace(content, "<script$1></script>");
|
||||
// content = StartingTitleTag().Replace(content, "<title$1></title>");
|
||||
content = Regex.Replace(content, @"<script(.*)(/>)", "<script$1></script>", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
content = Regex.Replace(content, @"<title(.*)(/>)", "<title$1></title>", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
return content;
|
||||
|
@ -588,7 +620,7 @@ public class BookService : IBookService
|
|||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public ParserInfo ParseInfo(string filePath)
|
||||
public ParserInfo? ParseInfo(string filePath)
|
||||
{
|
||||
if (!Tasks.Scanner.Parser.Parser.IsEpub(filePath)) return null;
|
||||
|
||||
|
@ -656,7 +688,7 @@ public class BookService : IBookService
|
|||
Edition = string.Empty,
|
||||
Format = MangaFormat.Epub,
|
||||
Filename = Path.GetFileName(filePath),
|
||||
Title = specialName?.Trim(),
|
||||
Title = specialName?.Trim() ?? string.Empty,
|
||||
FullFilePath = filePath,
|
||||
IsSpecial = false,
|
||||
Series = series.Trim(),
|
||||
|
@ -1037,6 +1069,12 @@ public class BookService : IBookService
|
|||
}
|
||||
|
||||
// Remove comments from CSS
|
||||
// body = CssComment().Replace(body, string.Empty);
|
||||
//
|
||||
// body = WhiteSpace1().Replace(body, "#");
|
||||
// body = WhiteSpace2().Replace(body, string.Empty);
|
||||
// body = WhiteSpace3().Replace(body, " ");
|
||||
// body = WhiteSpace4().Replace(body, "$1");
|
||||
body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty, RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
|
||||
body = Regex.Replace(body, @"[a-zA-Z]+#", "#", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
|
@ -1052,6 +1090,7 @@ public class BookService : IBookService
|
|||
//Swallow exception. Some css don't have style rules ending in ';'
|
||||
}
|
||||
|
||||
//body = UnitPadding().Replace(body, "$1");
|
||||
body = Regex.Replace(body, @"([\s:]0)(px|pt|%|em)", "$1", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
|
@ -50,21 +49,23 @@ public class BookmarkService : IBookmarkService
|
|||
/// Deletes the files associated with the list of Bookmarks passed. Will clean up empty folders.
|
||||
/// </summary>
|
||||
/// <param name="bookmarks"></param>
|
||||
public async Task DeleteBookmarkFiles(IEnumerable<AppUserBookmark> bookmarks)
|
||||
public async Task DeleteBookmarkFiles(IEnumerable<AppUserBookmark?> bookmarks)
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
|
||||
var bookmarkFilesToDelete = bookmarks.Select(b => Tasks.Scanner.Parser.Parser.NormalizePath(
|
||||
_directoryService.FileSystem.Path.Join(bookmarkDirectory,
|
||||
b.FileName))).ToList();
|
||||
var bookmarkFilesToDelete = bookmarks
|
||||
.Where(b => b != null)
|
||||
.Select(b => Tasks.Scanner.Parser.Parser.NormalizePath(
|
||||
_directoryService.FileSystem.Path.Join(bookmarkDirectory, b!.FileName)))
|
||||
.ToList();
|
||||
|
||||
if (bookmarkFilesToDelete.Count == 0) return;
|
||||
|
||||
_directoryService.DeleteFiles(bookmarkFilesToDelete);
|
||||
|
||||
// Delete any leftover folders
|
||||
foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, "", SearchOption.AllDirectories))
|
||||
foreach (var directory in _directoryService.FileSystem.Directory.GetDirectories(bookmarkDirectory, string.Empty, SearchOption.AllDirectories))
|
||||
{
|
||||
if (_directoryService.FileSystem.Directory.GetFiles(directory, "", SearchOption.AllDirectories).Length == 0 &&
|
||||
_directoryService.FileSystem.Directory.GetDirectories(directory).Length == 0)
|
||||
|
@ -92,7 +93,7 @@ public class BookmarkService : IBookmarkService
|
|||
return true;
|
||||
}
|
||||
|
||||
var fileInfo = _directoryService.FileSystem.FileInfo.FromFileName(imageToBookmark);
|
||||
var fileInfo = _directoryService.FileSystem.FileInfo.New(imageToBookmark);
|
||||
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||
var targetFolderStem = BookmarkStem(userWithBookmarks.Id, bookmarkDto.SeriesId, bookmarkDto.ChapterId);
|
||||
var targetFilepath = Path.Join(settings.BookmarksDirectory, targetFolderStem);
|
||||
|
@ -136,7 +137,6 @@ public class BookmarkService : IBookmarkService
|
|||
/// <returns></returns>
|
||||
public async Task<bool> RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto)
|
||||
{
|
||||
if (userWithBookmarks.Bookmarks == null) return true;
|
||||
var bookmarkToDelete = userWithBookmarks.Bookmarks.SingleOrDefault(x =>
|
||||
x.ChapterId == bookmarkDto.ChapterId && x.Page == bookmarkDto.Page);
|
||||
try
|
||||
|
@ -215,6 +215,8 @@ public class BookmarkService : IBookmarkService
|
|||
var count = 1F;
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapter.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, chapter.CoverImage, coverDirectory);
|
||||
chapter.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
|
|
|
@ -4,7 +4,6 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
|
@ -25,7 +24,7 @@ public interface ICacheService
|
|||
/// <param name="chapterId"></param>
|
||||
/// <param name="extractPdfToImages">Extracts a PDF into images for a different reading experience</param>
|
||||
/// <returns>Chapter for the passed chapterId. Side-effect from ensuring cache.</returns>
|
||||
Task<Chapter> Ensure(int chapterId, bool extractPdfToImages = false);
|
||||
Task<Chapter?> Ensure(int chapterId, bool extractPdfToImages = false);
|
||||
/// <summary>
|
||||
/// Clears cache directory of all volumes. This can be invoked from deleting a library or a series.
|
||||
/// </summary>
|
||||
|
@ -132,14 +131,21 @@ public class CacheService : ICacheService
|
|||
{
|
||||
var extractPath = GetCachePath(chapter.Id);
|
||||
var path = Path.Join(extractPath, _directoryService.FileSystem.Path.GetFileName(chapter.Files.First().FilePath));
|
||||
if (!(_directoryService.FileSystem.FileInfo.FromFileName(path).Exists))
|
||||
if (!(_directoryService.FileSystem.FileInfo.New(path).Exists))
|
||||
{
|
||||
path = chapter.Files.First().FilePath;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public async Task<Chapter> Ensure(int chapterId, bool extractPdfToImages = false)
|
||||
|
||||
/// <summary>
|
||||
/// Caches the files for the given chapter to CacheDirectory
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="extractPdfToImages">Defaults to false. Extract pdf file into images rather than copying just the pdf file</param>
|
||||
/// <returns>This will always return the Chapter for the chapterId</returns>
|
||||
public async Task<Chapter?> Ensure(int chapterId, bool extractPdfToImages = false)
|
||||
{
|
||||
_directoryService.ExistOrCreate(_directoryService.CacheDirectory);
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
|
@ -160,12 +166,13 @@ public class CacheService : ICacheService
|
|||
/// <param name="files"></param>
|
||||
/// <param name="extractPdfImages">Defaults to false, if true, will extract the images from the PDF renderer and not move the pdf file</param>
|
||||
/// <returns></returns>
|
||||
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile> files, bool extractPdfImages = false)
|
||||
public void ExtractChapterFiles(string extractPath, IReadOnlyList<MangaFile>? files, bool extractPdfImages = false)
|
||||
{
|
||||
if (files == null) return;
|
||||
var removeNonImages = true;
|
||||
var fileCount = files.Count;
|
||||
var extraPath = string.Empty;
|
||||
var extractDi = _directoryService.FileSystem.DirectoryInfo.FromDirectoryName(extractPath);
|
||||
var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath);
|
||||
|
||||
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
||||
{
|
||||
|
|
|
@ -3,12 +3,12 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.Entities;
|
||||
using API.Entities.Metadata;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
|
@ -17,10 +17,10 @@ public interface ICollectionTagService
|
|||
{
|
||||
Task<bool> TagExistsByName(string name);
|
||||
Task<bool> UpdateTag(CollectionTagDto dto);
|
||||
Task<bool> AddTagToSeries(CollectionTag tag, IEnumerable<int> seriesIds);
|
||||
Task<bool> RemoveTagFromSeries(CollectionTag tag, IEnumerable<int> seriesIds);
|
||||
Task<bool> AddTagToSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
||||
Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds);
|
||||
Task<CollectionTag> GetTagOrCreate(int tagId, string title);
|
||||
void AddTagToSeriesMetadata(CollectionTag tag, SeriesMetadata metadata);
|
||||
void AddTagToSeriesMetadata(CollectionTag? tag, SeriesMetadata metadata);
|
||||
CollectionTag CreateTag(string title);
|
||||
Task<bool> RemoveTagsWithoutSeries();
|
||||
}
|
||||
|
@ -93,8 +93,9 @@ public class CollectionTagService : ICollectionTagService
|
|||
/// <param name="tag">A full Tag</param>
|
||||
/// <param name="seriesIds"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> AddTagToSeries(CollectionTag tag, IEnumerable<int> seriesIds)
|
||||
public async Task<bool> AddTagToSeries(CollectionTag? tag, IEnumerable<int> seriesIds)
|
||||
{
|
||||
if (tag == null) return false;
|
||||
var metadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(seriesIds);
|
||||
foreach (var metadata in metadatas)
|
||||
{
|
||||
|
@ -112,8 +113,9 @@ public class CollectionTagService : ICollectionTagService
|
|||
/// <param name="tag"></param>
|
||||
/// <param name="metadata"></param>
|
||||
/// <returns></returns>
|
||||
public void AddTagToSeriesMetadata(CollectionTag tag, SeriesMetadata metadata)
|
||||
public void AddTagToSeriesMetadata(CollectionTag? tag, SeriesMetadata metadata)
|
||||
{
|
||||
if (tag == null) return;
|
||||
metadata.CollectionTags ??= new List<CollectionTag>();
|
||||
if (metadata.CollectionTags.Any(t => t.NormalizedTitle.Equals(tag.NormalizedTitle, StringComparison.InvariantCulture))) return;
|
||||
|
||||
|
@ -124,8 +126,9 @@ public class CollectionTagService : ICollectionTagService
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveTagFromSeries(CollectionTag tag, IEnumerable<int> seriesIds)
|
||||
public async Task<bool> RemoveTagFromSeries(CollectionTag? tag, IEnumerable<int> seriesIds)
|
||||
{
|
||||
if (tag == null) return false;
|
||||
foreach (var seriesIdToRemove in seriesIds)
|
||||
{
|
||||
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
|
||||
|
@ -150,7 +153,7 @@ public class CollectionTagService : ICollectionTagService
|
|||
/// <returns></returns>
|
||||
public async Task<CollectionTag> GetTagOrCreate(int tagId, string title)
|
||||
{
|
||||
return await _unitOfWork.CollectionTagRepository.GetFullTagAsync(tagId) ?? CreateTag(title);
|
||||
return await _unitOfWork.CollectionTagRepository.GetTagAsync(tagId, CollectionTagIncludes.SeriesMetadata) ?? CreateTag(title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -8,7 +8,6 @@ using API.DTOs.Email;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.Device;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -16,8 +15,8 @@ namespace API.Services;
|
|||
|
||||
public interface IDeviceService
|
||||
{
|
||||
Task<Device> Create(CreateDeviceDto dto, AppUser userWithDevices);
|
||||
Task<Device> Update(UpdateDeviceDto dto, AppUser userWithDevices);
|
||||
Task<Device?> Create(CreateDeviceDto dto, AppUser userWithDevices);
|
||||
Task<Device?> Update(UpdateDeviceDto dto, AppUser userWithDevices);
|
||||
Task<bool> Delete(AppUser userWithDevices, int deviceId);
|
||||
Task<bool> SendTo(IReadOnlyList<int> chapterIds, int deviceId);
|
||||
}
|
||||
|
@ -34,13 +33,13 @@ public class DeviceService : IDeviceService
|
|||
_logger = logger;
|
||||
_emailService = emailService;
|
||||
}
|
||||
#nullable enable
|
||||
|
||||
public async Task<Device?> Create(CreateDeviceDto dto, AppUser userWithDevices)
|
||||
{
|
||||
try
|
||||
{
|
||||
userWithDevices.Devices ??= new List<Device>();
|
||||
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name.Equals(dto.Name));
|
||||
var existingDevice = userWithDevices.Devices.SingleOrDefault(d => d.Name!.Equals(dto.Name));
|
||||
if (existingDevice != null) throw new KavitaException("A device with this name already exists");
|
||||
|
||||
existingDevice = DbFactory.Device(dto.Name);
|
||||
|
@ -85,7 +84,6 @@ public class DeviceService : IDeviceService
|
|||
|
||||
return null;
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
public async Task<bool> Delete(AppUser userWithDevices, int deviceId)
|
||||
{
|
||||
|
@ -119,7 +117,7 @@ public class DeviceService : IDeviceService
|
|||
await _unitOfWork.CommitAsync();
|
||||
var success = await _emailService.SendFilesToEmail(new SendToDto()
|
||||
{
|
||||
DestinationEmail = device.EmailAddress,
|
||||
DestinationEmail = device.EmailAddress!,
|
||||
FilePaths = files.Select(m => m.FilePath)
|
||||
});
|
||||
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.System;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using Kavita.Common.Helpers;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
@ -47,33 +44,25 @@ public interface IDirectoryService
|
|||
void ClearDirectory(string directoryPath);
|
||||
void ClearAndDeleteDirectory(string directoryPath);
|
||||
string[] GetFilesWithExtension(string path, string searchPatternExpression = "");
|
||||
bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = "");
|
||||
|
||||
bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = "");
|
||||
Dictionary<string, string> FindHighestDirectoriesFromFiles(IEnumerable<string> libraryFolders,
|
||||
IList<string> filePaths);
|
||||
|
||||
IEnumerable<string> GetFoldersTillRoot(string rootPath, string fullPath);
|
||||
|
||||
IEnumerable<string> GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||
|
||||
bool ExistOrCreate(string directoryPath);
|
||||
void DeleteFiles(IEnumerable<string> files);
|
||||
void RemoveNonImages(string directoryName);
|
||||
void Flatten(string directoryName);
|
||||
Task<bool> CheckWriteAccess(string directoryName);
|
||||
|
||||
IEnumerable<string> GetFilesWithCertainExtensions(string path,
|
||||
string searchPatternExpression = "",
|
||||
SearchOption searchOption = SearchOption.TopDirectoryOnly);
|
||||
|
||||
IEnumerable<string> GetDirectories(string folderPath);
|
||||
IEnumerable<string> GetDirectories(string folderPath, GlobMatcher matcher);
|
||||
IEnumerable<string> GetDirectories(string folderPath, GlobMatcher? matcher);
|
||||
string GetParentDirectoryName(string fileOrFolder);
|
||||
#nullable enable
|
||||
IList<string> ScanFiles(string folderPath, GlobMatcher? matcher = null);
|
||||
DateTime GetLastWriteTime(string folderPath);
|
||||
GlobMatcher CreateMatcherFromFile(string filePath);
|
||||
#nullable disable
|
||||
GlobMatcher? CreateMatcherFromFile(string filePath);
|
||||
}
|
||||
public class DirectoryService : IDirectoryService
|
||||
{
|
||||
|
@ -87,6 +76,16 @@ public class DirectoryService : IDirectoryService
|
|||
public string BookmarkDirectory { get; }
|
||||
public string SiteThemeDirectory { get; }
|
||||
private readonly ILogger<DirectoryService> _logger;
|
||||
private const int RegexTimeoutMs = 5000000;
|
||||
private const RegexOptions MatchOptions = RegexOptions.Compiled | RegexOptions.IgnoreCase;
|
||||
|
||||
// [GeneratedRegex(@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle",
|
||||
// MatchOptions, matchTimeoutMilliseconds: RegexTimeoutMs)]
|
||||
// private static partial Regex ExcludeDirectoriesRegex();
|
||||
//
|
||||
// [GeneratedRegex(@"\(\d+\)",
|
||||
// MatchOptions, matchTimeoutMilliseconds: RegexTimeoutMs)]
|
||||
// private static partial Regex FileCopyAppendRegex();
|
||||
|
||||
private static readonly Regex ExcludeDirectories = new Regex(
|
||||
@"@eaDir|\.DS_Store|\.qpkg|__MACOSX|@Recently-Snapshot|@recycle|\.@__thumb",
|
||||
|
@ -130,7 +129,7 @@ public class DirectoryService : IDirectoryService
|
|||
SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||
{
|
||||
if (!FileSystem.Directory.Exists(path)) return ImmutableList<string>.Empty;
|
||||
var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase);
|
||||
var reSearchPattern = new Regex(searchPatternExpression, RegexOptions.IgnoreCase, Tasks.Scanner.Parser.Parser.RegexTimeout);
|
||||
|
||||
return FileSystem.Directory.EnumerateFiles(path, "*", searchOption)
|
||||
.Where(file =>
|
||||
|
@ -172,7 +171,7 @@ public class DirectoryService : IDirectoryService
|
|||
|
||||
while (FileSystem.Path.GetDirectoryName(path) != Path.GetDirectoryName(root))
|
||||
{
|
||||
var folder = FileSystem.DirectoryInfo.FromDirectoryName(path).Name;
|
||||
var folder = FileSystem.DirectoryInfo.New(path).Name;
|
||||
paths.Add(folder);
|
||||
path = path.Substring(0, path.LastIndexOf(separator));
|
||||
}
|
||||
|
@ -187,7 +186,7 @@ public class DirectoryService : IDirectoryService
|
|||
/// <returns></returns>
|
||||
public bool Exists(string directory)
|
||||
{
|
||||
var di = FileSystem.DirectoryInfo.FromDirectoryName(directory);
|
||||
var di = FileSystem.DirectoryInfo.New(directory);
|
||||
return di.Exists;
|
||||
}
|
||||
|
||||
|
@ -230,7 +229,7 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
try
|
||||
{
|
||||
var fileInfo = FileSystem.FileInfo.FromFileName(fullFilePath);
|
||||
var fileInfo = FileSystem.FileInfo.New(fullFilePath);
|
||||
if (!fileInfo.Exists) return;
|
||||
|
||||
ExistOrCreate(targetDirectory);
|
||||
|
@ -250,12 +249,12 @@ public class DirectoryService : IDirectoryService
|
|||
/// <param name="searchPattern">Defaults to all files</param>
|
||||
/// <returns>If was successful</returns>
|
||||
/// <exception cref="DirectoryNotFoundException">Thrown when source directory does not exist</exception>
|
||||
public bool CopyDirectoryToDirectory(string sourceDirName, string destDirName, string searchPattern = "")
|
||||
public bool CopyDirectoryToDirectory(string? sourceDirName, string destDirName, string searchPattern = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(sourceDirName)) return false;
|
||||
|
||||
// Get the subdirectories for the specified directory.
|
||||
var dir = FileSystem.DirectoryInfo.FromDirectoryName(sourceDirName);
|
||||
var dir = FileSystem.DirectoryInfo.New(sourceDirName);
|
||||
|
||||
if (!dir.Exists)
|
||||
{
|
||||
|
@ -270,7 +269,7 @@ public class DirectoryService : IDirectoryService
|
|||
ExistOrCreate(destDirName);
|
||||
|
||||
// Get the files in the directory and copy them to the new location.
|
||||
var files = GetFilesWithExtension(dir.FullName, searchPattern).Select(n => FileSystem.FileInfo.FromFileName(n));
|
||||
var files = GetFilesWithExtension(dir.FullName, searchPattern).Select(n => FileSystem.FileInfo.New(n));
|
||||
foreach (var file in files)
|
||||
{
|
||||
var tempPath = FileSystem.Path.Combine(destDirName, file.Name);
|
||||
|
@ -294,7 +293,7 @@ public class DirectoryService : IDirectoryService
|
|||
/// <returns></returns>
|
||||
public bool IsDriveMounted(string path)
|
||||
{
|
||||
return FileSystem.DirectoryInfo.FromDirectoryName(FileSystem.Path.GetPathRoot(path) ?? string.Empty).Exists;
|
||||
return FileSystem.DirectoryInfo.New(FileSystem.Path.GetPathRoot(path) ?? string.Empty).Exists;
|
||||
}
|
||||
|
||||
|
||||
|
@ -325,7 +324,7 @@ public class DirectoryService : IDirectoryService
|
|||
/// <returns>Total bytes</returns>
|
||||
public long GetTotalSize(IEnumerable<string> paths)
|
||||
{
|
||||
return paths.Sum(path => FileSystem.FileInfo.FromFileName(path).Length);
|
||||
return paths.Sum(path => FileSystem.FileInfo.New(path).Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -335,7 +334,7 @@ public class DirectoryService : IDirectoryService
|
|||
/// <returns></returns>
|
||||
public bool ExistOrCreate(string directoryPath)
|
||||
{
|
||||
var di = FileSystem.DirectoryInfo.FromDirectoryName(directoryPath);
|
||||
var di = FileSystem.DirectoryInfo.New(directoryPath);
|
||||
if (di.Exists) return true;
|
||||
try
|
||||
{
|
||||
|
@ -356,7 +355,7 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
if (!FileSystem.Directory.Exists(directoryPath)) return;
|
||||
|
||||
var di = FileSystem.DirectoryInfo.FromDirectoryName(directoryPath);
|
||||
var di = FileSystem.DirectoryInfo.New(directoryPath);
|
||||
|
||||
ClearDirectory(directoryPath);
|
||||
|
||||
|
@ -370,7 +369,7 @@ public class DirectoryService : IDirectoryService
|
|||
/// <returns></returns>
|
||||
public void ClearDirectory(string directoryPath)
|
||||
{
|
||||
var di = FileSystem.DirectoryInfo.FromDirectoryName(directoryPath);
|
||||
var di = FileSystem.DirectoryInfo.New(directoryPath);
|
||||
if (!di.Exists) return;
|
||||
try
|
||||
{
|
||||
|
@ -401,7 +400,7 @@ public class DirectoryService : IDirectoryService
|
|||
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "")
|
||||
{
|
||||
ExistOrCreate(directoryPath);
|
||||
string currentFile = null;
|
||||
string? currentFile = null;
|
||||
try
|
||||
{
|
||||
foreach (var file in filePaths)
|
||||
|
@ -413,8 +412,8 @@ public class DirectoryService : IDirectoryService
|
|||
_logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath);
|
||||
continue;
|
||||
}
|
||||
var fileInfo = FileSystem.FileInfo.FromFileName(file);
|
||||
var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(file, directoryPath, prepend));
|
||||
var fileInfo = FileSystem.FileInfo.New(file);
|
||||
var targetFile = FileSystem.FileInfo.New(RenameFileForCopy(file, directoryPath, prepend));
|
||||
|
||||
fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name));
|
||||
}
|
||||
|
@ -439,7 +438,7 @@ public class DirectoryService : IDirectoryService
|
|||
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, IList<string> newFilenames)
|
||||
{
|
||||
ExistOrCreate(directoryPath);
|
||||
string currentFile = null;
|
||||
string? currentFile = null;
|
||||
var index = 0;
|
||||
try
|
||||
{
|
||||
|
@ -452,8 +451,8 @@ public class DirectoryService : IDirectoryService
|
|||
_logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath);
|
||||
continue;
|
||||
}
|
||||
var fileInfo = FileSystem.FileInfo.FromFileName(file);
|
||||
var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(newFilenames[index] + fileInfo.Extension, directoryPath));
|
||||
var fileInfo = FileSystem.FileInfo.New(file);
|
||||
var targetFile = FileSystem.FileInfo.New(RenameFileForCopy(newFilenames[index] + fileInfo.Extension, directoryPath));
|
||||
|
||||
fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name));
|
||||
index++;
|
||||
|
@ -480,18 +479,20 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
while (true)
|
||||
{
|
||||
var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy);
|
||||
var fileInfo = FileSystem.FileInfo.New(fileToCopy);
|
||||
var filename = prepend + fileInfo.Name;
|
||||
|
||||
var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename));
|
||||
var targetFile = FileSystem.FileInfo.New(FileSystem.Path.Join(directoryPath, filename));
|
||||
if (!targetFile.Exists)
|
||||
{
|
||||
return targetFile.FullName;
|
||||
}
|
||||
|
||||
var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name);
|
||||
//if (FileCopyAppendRegex().IsMatch(noExtension))
|
||||
if (FileCopyAppend.IsMatch(noExtension))
|
||||
{
|
||||
//var match = FileCopyAppendRegex().Match(noExtension).Value;
|
||||
var match = FileCopyAppend.Match(noExtension).Value;
|
||||
var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty);
|
||||
noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})");
|
||||
|
@ -515,7 +516,7 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
if (!FileSystem.Directory.Exists(rootPath)) return ImmutableList<DirectoryDto>.Empty;
|
||||
|
||||
var di = FileSystem.DirectoryInfo.FromDirectoryName(rootPath);
|
||||
var di = FileSystem.DirectoryInfo.New(rootPath);
|
||||
var dirs = di.GetDirectories()
|
||||
.Where(dir => !(dir.Attributes.HasFlag(FileAttributes.Hidden) || dir.Attributes.HasFlag(FileAttributes.System)))
|
||||
.Select(d => new DirectoryDto()
|
||||
|
@ -595,13 +596,13 @@ public class DirectoryService : IDirectoryService
|
|||
/// <param name="folderPath"></param>
|
||||
/// <param name="matcher">A set of glob rules that will filter directories out</param>
|
||||
/// <returns>List of directory paths, empty if path doesn't exist</returns>
|
||||
public IEnumerable<string> GetDirectories(string folderPath, GlobMatcher matcher)
|
||||
public IEnumerable<string> GetDirectories(string folderPath, GlobMatcher? matcher)
|
||||
{
|
||||
if (matcher == null) return GetDirectories(folderPath);
|
||||
|
||||
return GetDirectories(folderPath)
|
||||
.Where(folder => !matcher.ExcludeMatches(
|
||||
$"{FileSystem.DirectoryInfo.FromDirectoryName(folder).Name}{FileSystem.Path.AltDirectorySeparatorChar}"));
|
||||
$"{FileSystem.DirectoryInfo.New(folder).Name}{FileSystem.Path.AltDirectorySeparatorChar}"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -681,7 +682,7 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
var foundFiles = GetFilesWithCertainExtensions(folderPath,
|
||||
Tasks.Scanner.Parser.Parser.SupportedExtensions)
|
||||
.Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.FromFileName(file).Name));
|
||||
.Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name));
|
||||
files.AddRange(foundFiles);
|
||||
}
|
||||
|
||||
|
@ -707,7 +708,7 @@ public class DirectoryService : IDirectoryService
|
|||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <returns></returns>
|
||||
public GlobMatcher CreateMatcherFromFile(string filePath)
|
||||
public GlobMatcher? CreateMatcherFromFile(string filePath)
|
||||
{
|
||||
if (!FileSystem.File.Exists(filePath))
|
||||
{
|
||||
|
@ -831,7 +832,7 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
try
|
||||
{
|
||||
FileSystem.FileInfo.FromFileName(file).Delete();
|
||||
FileSystem.FileInfo.New(file).Delete();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -934,7 +935,7 @@ public class DirectoryService : IDirectoryService
|
|||
{
|
||||
if (string.IsNullOrEmpty(directoryName) || !FileSystem.Directory.Exists(directoryName)) return;
|
||||
|
||||
var directory = FileSystem.DirectoryInfo.FromDirectoryName(directoryName);
|
||||
var directory = FileSystem.DirectoryInfo.New(directoryName);
|
||||
|
||||
var index = 0;
|
||||
FlattenDirectory(directory, directory, ref index);
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
|
||||
namespace API.Services;
|
||||
|
@ -53,7 +51,7 @@ public class DownloadService : IDownloadService
|
|||
};
|
||||
}
|
||||
|
||||
return contentType;
|
||||
return contentType!;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -78,13 +78,13 @@ public class EmailService : IEmailService
|
|||
|
||||
public async Task<bool> IsDefaultEmailService()
|
||||
{
|
||||
return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value
|
||||
return (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value!
|
||||
.Equals(DefaultApiUrl);
|
||||
}
|
||||
|
||||
public async Task SendEmailChangeEmail(ConfirmationEmailDto data)
|
||||
{
|
||||
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;
|
||||
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))!.Value;
|
||||
var success = await SendEmailWithPost(emailLink + "/api/account/email-change", data);
|
||||
if (!success)
|
||||
{
|
||||
|
|
|
@ -17,9 +17,10 @@ public interface IImageService
|
|||
/// </summary>
|
||||
/// <param name="encodedImage">base64 encoded image</param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="saveAsWebP">Convert and save as webp</param>
|
||||
/// <param name="thumbnailWidth">Width of thumbnail</param>
|
||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||
string CreateThumbnailFromBase64(string encodedImage, string fileName, int thumbnailWidth = 0);
|
||||
string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = 320);
|
||||
|
||||
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
/// <summary>
|
||||
|
@ -42,7 +43,6 @@ public class ImageService : IImageService
|
|||
public const string CollectionTagCoverImageRegex = @"tag\d+";
|
||||
public const string ReadingListCoverImageRegex = @"readinglist\d+";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Width of the Thumbnail generation
|
||||
/// </summary>
|
||||
|
@ -58,8 +58,9 @@ public class ImageService : IImageService
|
|||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
public void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1)
|
||||
public void ExtractImages(string? fileFilePath, string targetDirectory, int fileCount = 1)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileFilePath)) return;
|
||||
_directoryService.ExistOrCreate(targetDirectory);
|
||||
if (fileCount == 1)
|
||||
{
|
||||
|
@ -67,7 +68,7 @@ public class ImageService : IImageService
|
|||
}
|
||||
else
|
||||
{
|
||||
_directoryService.CopyDirectoryToDirectory(Path.GetDirectoryName(fileFilePath), targetDirectory,
|
||||
_directoryService.CopyDirectoryToDirectory(_directoryService.FileSystem.Path.GetDirectoryName(fileFilePath), targetDirectory,
|
||||
Tasks.Scanner.Parser.Parser.ImageFileExtensions);
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +116,7 @@ public class ImageService : IImageService
|
|||
|
||||
public Task<string> ConvertToWebP(string filePath, string outputPath)
|
||||
{
|
||||
var file = _directoryService.FileSystem.FileInfo.FromFileName(filePath);
|
||||
var file = _directoryService.FileSystem.FileInfo.New(filePath);
|
||||
var fileName = file.Name.Replace(file.Extension, string.Empty);
|
||||
var outputFile = Path.Join(outputPath, fileName + ".webp");
|
||||
|
||||
|
@ -143,12 +144,12 @@ public class ImageService : IImageService
|
|||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CreateThumbnailFromBase64(string encodedImage, string fileName, int thumbnailWidth = ThumbnailWidth)
|
||||
public string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = ThumbnailWidth)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), ThumbnailWidth);
|
||||
var filename = fileName + ".png";
|
||||
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName + ".png"));
|
||||
return filename;
|
||||
}
|
||||
|
|
|
@ -5,18 +5,11 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Metadata;
|
||||
using API.Data.Repositories;
|
||||
using API.Data.Scanner;
|
||||
using API.DTOs.Metadata;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Services.Tasks.Metadata;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
@ -74,11 +67,13 @@ public class MetadataService : IMetadataService
|
|||
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, bool convertToWebPOnWrite)
|
||||
{
|
||||
var firstFile = chapter.Files.MinBy(x => x.Chapter);
|
||||
if (firstFile == null) return Task.FromResult(false);
|
||||
|
||||
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked))
|
||||
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage),
|
||||
firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked))
|
||||
return Task.FromResult(false);
|
||||
|
||||
if (firstFile == null) return Task.FromResult(false);
|
||||
|
||||
|
||||
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath);
|
||||
|
||||
|
@ -102,7 +97,7 @@ public class MetadataService : IMetadataService
|
|||
/// </summary>
|
||||
/// <param name="volume"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
private Task<bool> UpdateVolumeCoverImage(Volume volume, bool forceUpdate)
|
||||
private Task<bool> UpdateVolumeCoverImage(Volume? volume, bool forceUpdate)
|
||||
{
|
||||
// We need to check if Volume coverImage matches first chapters if forceUpdate is false
|
||||
if (volume == null || !_cacheHelper.ShouldUpdateCoverImage(
|
||||
|
@ -125,7 +120,7 @@ public class MetadataService : IMetadataService
|
|||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
private Task UpdateSeriesCoverImage(Series series, bool forceUpdate)
|
||||
private Task UpdateSeriesCoverImage(Series? series, bool forceUpdate)
|
||||
{
|
||||
if (series == null) return Task.CompletedTask;
|
||||
|
||||
|
@ -199,6 +194,7 @@ public class MetadataService : IMetadataService
|
|||
public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return;
|
||||
_logger.LogInformation("[MetadataService] Beginning cover generation refresh of {LibraryName}", library.Name);
|
||||
|
||||
_updateEvents.Clear();
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -105,6 +104,7 @@ public class ReaderService : IReaderService
|
|||
{
|
||||
var seenVolume = new Dictionary<int, bool>();
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
if (series == null) throw new KavitaException("Series suddenly doesn't exist, cannot mark as read");
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||
|
@ -128,14 +128,14 @@ public class ReaderService : IReaderService
|
|||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages));
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages));
|
||||
|
||||
// Send out volume events for each distinct volume
|
||||
if (!seenVolume.ContainsKey(chapter.VolumeId))
|
||||
{
|
||||
seenVolume[chapter.VolumeId] = true;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId,
|
||||
chapter.VolumeId, 0, chapters.Where(c => c.VolumeId == chapter.VolumeId).Sum(c => c.Pages)));
|
||||
}
|
||||
|
||||
|
@ -164,14 +164,14 @@ public class ReaderService : IReaderService
|
|||
userProgress.VolumeId = chapter.VolumeId;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0));
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0));
|
||||
|
||||
// Send out volume events for each distinct volume
|
||||
if (!seenVolume.ContainsKey(chapter.VolumeId))
|
||||
{
|
||||
seenVolume[chapter.VolumeId] = true;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId,
|
||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName!, seriesId,
|
||||
chapter.VolumeId, 0, 0));
|
||||
}
|
||||
}
|
||||
|
@ -184,9 +184,9 @@ public class ReaderService : IReaderService
|
|||
/// <param name="user">Must have Progresses populated</param>
|
||||
/// <param name="chapter"></param>
|
||||
/// <returns></returns>
|
||||
private static AppUserProgress GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||
private static AppUserProgress? GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||
{
|
||||
AppUserProgress userProgress = null;
|
||||
AppUserProgress? userProgress = null;
|
||||
|
||||
if (user.Progresses == null)
|
||||
{
|
||||
|
@ -227,7 +227,7 @@ public class ReaderService : IReaderService
|
|||
|
||||
try
|
||||
{
|
||||
// TODO: Rewrite this code to just pull user object with progress for that particiular appuserprogress, else create it
|
||||
// TODO: Rewrite this code to just pull user object with progress for that particular appuserprogress, else create it
|
||||
var userProgress =
|
||||
await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId);
|
||||
|
||||
|
@ -237,6 +237,7 @@ public class ReaderService : IReaderService
|
|||
// Create a user object
|
||||
var userWithProgress =
|
||||
await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.Progress);
|
||||
if (userWithProgress == null) return false;
|
||||
userWithProgress.Progresses ??= new List<AppUserProgress>();
|
||||
userWithProgress.Progresses.Add(new AppUserProgress
|
||||
{
|
||||
|
@ -263,7 +264,7 @@ public class ReaderService : IReaderService
|
|||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||
MessageFactory.UserProgressUpdateEvent(userId, user.UserName, progressDto.SeriesId, progressDto.VolumeId, progressDto.ChapterId, progressDto.PageNum));
|
||||
MessageFactory.UserProgressUpdateEvent(userId, user!.UserName!, progressDto.SeriesId, progressDto.VolumeId, progressDto.ChapterId, progressDto.PageNum));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,11 @@ namespace API.Services;
|
|||
|
||||
public interface IReadingItemService
|
||||
{
|
||||
ComicInfo GetComicInfo(string filePath);
|
||||
ComicInfo? GetComicInfo(string filePath);
|
||||
int GetNumberOfPages(string filePath, MangaFormat format);
|
||||
string GetCoverImage(string filePath, string fileName, MangaFormat format, bool saveAsWebP);
|
||||
void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
|
||||
ParserInfo Parse(string path, string rootPath, LibraryType type);
|
||||
ParserInfo ParseFile(string path, string rootPath, LibraryType type);
|
||||
ParserInfo? ParseFile(string path, string rootPath, LibraryType type);
|
||||
}
|
||||
|
||||
public class ReadingItemService : IReadingItemService
|
||||
|
@ -60,7 +59,7 @@ public class ReadingItemService : IReadingItemService
|
|||
/// <param name="path">Path of a file</param>
|
||||
/// <param name="rootPath"></param>
|
||||
/// <param name="type">Library type to determine parsing to perform</param>
|
||||
public ParserInfo ParseFile(string path, string rootPath, LibraryType type)
|
||||
public ParserInfo? ParseFile(string path, string rootPath, LibraryType type)
|
||||
{
|
||||
var info = Parse(path, rootPath, type);
|
||||
if (info == null)
|
||||
|
@ -216,7 +215,7 @@ public class ReadingItemService : IReadingItemService
|
|||
/// <param name="rootPath"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public ParserInfo Parse(string path, string rootPath, LibraryType type)
|
||||
private ParserInfo? Parse(string path, string rootPath, LibraryType type)
|
||||
{
|
||||
return Tasks.Scanner.Parser.Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
@ -7,7 +6,6 @@ using System.Threading.Tasks;
|
|||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.ReadingLists;
|
||||
using API.DTOs.ReadingLists.CBL;
|
||||
using API.Entities;
|
||||
|
@ -182,6 +180,7 @@ public class ReadingListService : IReadingListService
|
|||
_unitOfWork.ReadingListRepository.BulkRemove(listItems);
|
||||
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
|
||||
if (readingList == null) return true;
|
||||
await CalculateReadingListAgeRating(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
@ -205,8 +204,11 @@ public class ReadingListService : IReadingListService
|
|||
{
|
||||
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemsByIdAsync(dto.ReadingListId)).ToList();
|
||||
var item = items.Find(r => r.Id == dto.ReadingListItemId);
|
||||
items.Remove(item);
|
||||
items.Insert(dto.ToPosition, item);
|
||||
if (item != null)
|
||||
{
|
||||
items.Remove(item);
|
||||
items.Insert(dto.ToPosition, item);
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
|
@ -226,6 +228,7 @@ public class ReadingListService : IReadingListService
|
|||
public async Task<bool> DeleteReadingListItem(UpdateReadingListPosition dto)
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(dto.ReadingListId);
|
||||
if (readingList == null) return false;
|
||||
readingList.Items = readingList.Items.Where(r => r.Id != dto.ReadingListItemId).OrderBy(r => r.Order).ToList();
|
||||
|
||||
var index = 0;
|
||||
|
@ -260,7 +263,8 @@ public class ReadingListService : IReadingListService
|
|||
private async Task CalculateReadingListAgeRating(ReadingList readingList, IEnumerable<int> seriesIds)
|
||||
{
|
||||
var ageRating = await _unitOfWork.SeriesRepository.GetMaxAgeRatingFromSeriesAsync(seriesIds);
|
||||
readingList.AgeRating = ageRating;
|
||||
if (ageRating == null) readingList.AgeRating = AgeRating.Unknown;
|
||||
else readingList.AgeRating = (AgeRating) ageRating;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -274,7 +278,7 @@ public class ReadingListService : IReadingListService
|
|||
// We need full reading list with items as this is used by many areas that manipulate items
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username,
|
||||
AppUserIncludes.ReadingListsWithItems);
|
||||
if (!await UserHasReadingListAccess(readingListId, user))
|
||||
if (user == null || !await UserHasReadingListAccess(readingListId, user))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@ -302,6 +306,7 @@ public class ReadingListService : IReadingListService
|
|||
public async Task<bool> DeleteReadingList(int readingListId, AppUser user)
|
||||
{
|
||||
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(readingListId);
|
||||
if (readingList == null) return true;
|
||||
user.ReadingLists.Remove(readingList);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
|
@ -322,7 +327,7 @@ public class ReadingListService : IReadingListService
|
|||
var lastOrder = 0;
|
||||
if (readingList.Items.Any())
|
||||
{
|
||||
lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli.Order);
|
||||
lastOrder = readingList.Items.DefaultIfEmpty().Max(rli => rli!.Order);
|
||||
}
|
||||
|
||||
var existingChapterExists = readingList.Items.Select(rli => rli.ChapterId).ToHashSet();
|
||||
|
|
|
@ -8,14 +8,12 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Metadata;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
@ -52,7 +50,7 @@ public class SeriesService : ISeriesService
|
|||
/// <param name="series"></param>
|
||||
/// <param name="isBookLibrary"></param>
|
||||
/// <returns></returns>
|
||||
public static Chapter GetFirstChapterForMetadata(Series series, bool isBookLibrary)
|
||||
public static Chapter? GetFirstChapterForMetadata(Series series, bool isBookLibrary)
|
||||
{
|
||||
return series.Volumes.OrderBy(v => v.Number, ChapterSortComparer.Default)
|
||||
.SelectMany(v => v.Chapters.OrderBy(c => float.Parse(c.Number), ChapterSortComparer.Default))
|
||||
|
@ -65,12 +63,13 @@ public class SeriesService : ISeriesService
|
|||
{
|
||||
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
if (series == null) return false;
|
||||
var allCollectionTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList();
|
||||
var allGenres = (await _unitOfWork.GenreRepository.GetAllGenresAsync()).ToList();
|
||||
var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList();
|
||||
var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList();
|
||||
|
||||
series.Metadata ??= DbFactory.SeriesMetadata(updateSeriesMetadataDto.CollectionTags
|
||||
series.Metadata ??= DbFactory.SeriesMetadata((updateSeriesMetadataDto.CollectionTags ?? new List<CollectionTagDto>())
|
||||
.Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList());
|
||||
|
||||
if (series.Metadata.AgeRating != updateSeriesMetadataDto.SeriesMetadata.AgeRating)
|
||||
|
@ -104,13 +103,13 @@ public class SeriesService : ISeriesService
|
|||
|
||||
if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim())
|
||||
{
|
||||
series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim();
|
||||
series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim() ?? string.Empty;
|
||||
series.Metadata.SummaryLocked = true;
|
||||
}
|
||||
|
||||
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language)
|
||||
{
|
||||
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language;
|
||||
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language ?? string.Empty;
|
||||
series.Metadata.LanguageLocked = true;
|
||||
}
|
||||
|
||||
|
@ -121,13 +120,13 @@ public class SeriesService : ISeriesService
|
|||
});
|
||||
|
||||
series.Metadata.Genres ??= new List<Genre>();
|
||||
UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) =>
|
||||
GenreHelper.UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) =>
|
||||
{
|
||||
series.Metadata.Genres.Add(genre);
|
||||
}, () => series.Metadata.GenresLocked = true);
|
||||
|
||||
series.Metadata.Tags ??= new List<Tag>();
|
||||
UpdateTagList(updateSeriesMetadataDto.SeriesMetadata.Tags, series, allTags, (tag) =>
|
||||
TagHelper.UpdateTagList(updateSeriesMetadataDto.SeriesMetadata?.Tags, series, allTags, (tag) =>
|
||||
{
|
||||
series.Metadata.Tags.Add(tag);
|
||||
}, () => series.Metadata.TagsLocked = true);
|
||||
|
@ -139,25 +138,25 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
|
||||
series.Metadata.People ??= new List<Person>();
|
||||
UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata.Writers, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata!.Writers, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.WriterLocked = true);
|
||||
UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.CharacterLocked = true);
|
||||
UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.ColoristLocked = true);
|
||||
UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.EditorLocked = true);
|
||||
UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.InkerLocked = true);
|
||||
UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.LettererLocked = true);
|
||||
UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.PencillerLocked = true);
|
||||
UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.PublisherLocked = true);
|
||||
UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.TranslatorLocked = true);
|
||||
UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople,
|
||||
PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople,
|
||||
HandleAddPerson, () => series.Metadata.CoverArtistLocked = true);
|
||||
|
||||
series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked;
|
||||
|
@ -183,7 +182,7 @@ public class SeriesService : ISeriesService
|
|||
return true;
|
||||
}
|
||||
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
if (await _unitOfWork.CommitAsync() && updateSeriesMetadataDto.CollectionTags != null)
|
||||
{
|
||||
foreach (var tag in updateSeriesMetadataDto.CollectionTags)
|
||||
{
|
||||
|
@ -210,10 +209,10 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
|
||||
|
||||
public static void UpdateCollectionsList(ICollection<CollectionTagDto> tags, Series series, IReadOnlyCollection<CollectionTag> allTags,
|
||||
public static void UpdateCollectionsList(ICollection<CollectionTagDto>? tags, Series series, IReadOnlyCollection<CollectionTag> allTags,
|
||||
Action<CollectionTag> handleAdd)
|
||||
{
|
||||
// TODO: Move UpdateRelatedList to a helper so we can easily test
|
||||
// TODO: Move UpdateCollectionsList to a helper so we can easily test
|
||||
if (tags == null) return;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = series.Metadata.CollectionTags.ToList();
|
||||
|
@ -245,142 +244,13 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
}
|
||||
|
||||
private static void UpdateGenreList(ICollection<GenreTagDto> tags, Series series, IReadOnlyCollection<Genre> allTags, Action<Genre> handleAdd, Action onModified)
|
||||
{
|
||||
if (tags == null) return;
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = series.Metadata.Genres.ToList();
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
// NOTE: Why don't I use a NormalizedName here (outside of memory pressure from string creation)?
|
||||
if (tags.SingleOrDefault(t => t.Id == existing.Id) == null)
|
||||
{
|
||||
// Remove tag
|
||||
series.Metadata.Genres.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
{
|
||||
var normalizedTitle = Tasks.Scanner.Parser.Parser.Normalize(tagTitle);
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle == normalizedTitle);
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (series.Metadata.Genres.All(t => t.NormalizedTitle != normalizedTitle))
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(DbFactory.Genre(tagTitle));
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateTagList(ICollection<TagDto> tags, Series series, IReadOnlyCollection<Tag> allTags, Action<Tag> handleAdd, Action onModified)
|
||||
{
|
||||
if (tags == null) return;
|
||||
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = series.Metadata.Tags.ToList();
|
||||
foreach (var existing in existingTags.Where(existing => tags.SingleOrDefault(t => t.Id == existing.Id) == null))
|
||||
{
|
||||
// Remove tag
|
||||
series.Metadata.Tags.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tagTitle in tags.Select(t => t.Title))
|
||||
{
|
||||
var normalizedTitle = Tasks.Scanner.Parser.Parser.Normalize(tagTitle);
|
||||
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle.Equals(normalizedTitle));
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (series.Metadata.Tags.All(t => t.NormalizedTitle != normalizedTitle))
|
||||
{
|
||||
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(DbFactory.Tag(tagTitle));
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdatePeopleList(PersonRole role, ICollection<PersonDto> tags, Series series, IReadOnlyCollection<Person> allTags,
|
||||
Action<Person> handleAdd, Action onModified)
|
||||
{
|
||||
if (tags == null) return;
|
||||
var isModified = false;
|
||||
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
|
||||
var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList();
|
||||
foreach (var existing in existingTags)
|
||||
{
|
||||
if (tags.SingleOrDefault(t => t.Id == existing.Id) == null) // This needs to check against role
|
||||
{
|
||||
// Remove tag
|
||||
series.Metadata.People.Remove(existing);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, all tags that aren't in dto have been removed.
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
var existingTag = allTags.SingleOrDefault(t => t.Name == tag.Name && t.Role == tag.Role);
|
||||
if (existingTag != null)
|
||||
{
|
||||
if (series.Metadata.People.Where(t => t.Role == tag.Role).All(t => !t.Name.Equals(tag.Name)))
|
||||
{
|
||||
handleAdd(existingTag);
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new tag
|
||||
handleAdd(DbFactory.Person(tag.Name, role));
|
||||
isModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isModified)
|
||||
{
|
||||
onModified();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="user">User with Ratings includes</param>
|
||||
/// <param name="updateSeriesRatingDto"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> UpdateRating(AppUser user, UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
public async Task<bool> UpdateRating(AppUser? user, UpdateSeriesRatingDto updateSeriesRatingDto)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
|
@ -478,10 +348,10 @@ public class SeriesService : ISeriesService
|
|||
throw new UnauthorizedAccessException("User does not have access to the library this series belongs to");
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
if (user.AgeRestriction != AgeRating.NotApplicable)
|
||||
if (user!.AgeRestriction != AgeRating.NotApplicable)
|
||||
{
|
||||
var seriesMetadata = await _unitOfWork.SeriesRepository.GetSeriesMetadata(seriesId);
|
||||
if (seriesMetadata.AgeRating > user.AgeRestriction)
|
||||
if (seriesMetadata!.AgeRating > user.AgeRestriction)
|
||||
throw new UnauthorizedAccessException("User is not allowed to view this series due to age restrictions");
|
||||
}
|
||||
|
||||
|
@ -592,8 +462,10 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
|
||||
|
||||
private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string chapterTitle, bool withHash)
|
||||
private static string FormatChapterTitle(bool isSpecial, LibraryType libraryType, string? chapterTitle, bool withHash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapterTitle)) throw new ArgumentException("Chapter Title cannot be null");
|
||||
|
||||
if (isSpecial)
|
||||
{
|
||||
return Tasks.Scanner.Parser.Parser.CleanSpecialTitle(chapterTitle);
|
||||
|
@ -649,6 +521,7 @@ public class SeriesService : ISeriesService
|
|||
public async Task<bool> UpdateRelatedSeries(UpdateRelatedSeriesDto dto)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(dto.SeriesId, SeriesIncludes.Related);
|
||||
if (series == null) return false;
|
||||
|
||||
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);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
|
@ -12,7 +11,6 @@ using API.Extensions;
|
|||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
|
@ -593,13 +591,14 @@ public class StatisticService : IStatisticService
|
|||
user[userChapter.User.Id] = libraryTimes;
|
||||
}
|
||||
|
||||
|
||||
return user.Keys.Select(userId => new TopReadDto()
|
||||
{
|
||||
UserId = userId,
|
||||
Username = users.First(u => u.Id == userId).UserName,
|
||||
BooksTime = user[userId].ContainsKey(LibraryType.Book) ? user[userId][LibraryType.Book] : 0,
|
||||
ComicsTime = user[userId].ContainsKey(LibraryType.Comic) ? user[userId][LibraryType.Comic] : 0,
|
||||
MangaTime = user[userId].ContainsKey(LibraryType.Manga) ? user[userId][LibraryType.Manga] : 0,
|
||||
BooksTime = user[userId].TryGetValue(LibraryType.Book, out var bookTime) ? bookTime : 0,
|
||||
ComicsTime = user[userId].TryGetValue(LibraryType.Comic, out var comicTime) ? comicTime : 0,
|
||||
MangaTime = user[userId].TryGetValue(LibraryType.Manga, out var mangaTime) ? mangaTime : 0,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace API.Services;
|
|||
|
||||
public interface ITachiyomiService
|
||||
{
|
||||
Task<ChapterDto> GetLatestChapter(int seriesId, int userId);
|
||||
Task<ChapterDto?> GetLatestChapter(int seriesId, int userId);
|
||||
Task<bool> MarkChaptersUntilAsRead(AppUser userWithProgress, int seriesId, float chapterNumber);
|
||||
}
|
||||
|
||||
|
@ -49,10 +49,8 @@ public class TachiyomiService : ITachiyomiService
|
|||
/// If its a chapter, return the chapterDto as is.
|
||||
/// If it's a volume, the volume number gets returned in the 'Number' attribute of a chapterDto encoded.
|
||||
/// The volume number gets divided by 10,000 because that's how Tachiyomi interprets volumes</returns>
|
||||
public async Task<ChapterDto> GetLatestChapter(int seriesId, int userId)
|
||||
public async Task<ChapterDto?> GetLatestChapter(int seriesId, int userId)
|
||||
{
|
||||
|
||||
|
||||
var currentChapter = await _readerService.GetContinuePoint(seriesId, userId);
|
||||
|
||||
var prevChapterId =
|
||||
|
@ -95,7 +93,8 @@ public class TachiyomiService : ITachiyomiService
|
|||
}
|
||||
|
||||
// There is progress, we now need to figure out the highest volume or chapter and return that.
|
||||
var prevChapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId);
|
||||
var prevChapter = (await _unitOfWork.ChapterRepository.GetChapterDtoAsync(prevChapterId))!;
|
||||
|
||||
var volumeWithProgress = await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(prevChapter.VolumeId, userId);
|
||||
// We only encode for single-file volumes
|
||||
if (volumeWithProgress.Number != 0 && volumeWithProgress.Chapters.Count == 1)
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities.Enums;
|
||||
|
@ -322,6 +321,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
public async Task CheckForUpdate()
|
||||
{
|
||||
var update = await _versionUpdaterService.CheckForUpdate();
|
||||
if (update == null) return;
|
||||
await _versionUpdaterService.PushUpdate(update);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,9 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Logging;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services.Tasks;
|
||||
|
@ -62,7 +59,7 @@ public class BackupService : IBackupService
|
|||
public IEnumerable<string> GetLogFiles(bool rollFiles = LogLevelOptions.LogRollingEnabled)
|
||||
{
|
||||
var multipleFileRegex = rollFiles ? @"\d*" : string.Empty;
|
||||
var fi = _directoryService.FileSystem.FileInfo.FromFileName(LogLevelOptions.LogFile);
|
||||
var fi = _directoryService.FileSystem.FileInfo.New(LogLevelOptions.LogFile);
|
||||
|
||||
var files = rollFiles
|
||||
? _directoryService.GetFiles(_directoryService.LogDirectory,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
|
@ -11,7 +10,6 @@ using API.Entities.Enums;
|
|||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services.Tasks;
|
||||
|
@ -175,7 +173,7 @@ public class CleanupService : ICleanupService
|
|||
|
||||
var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold));
|
||||
var allBackups = _directoryService.GetFiles(backupDirectory).ToList();
|
||||
var expiredBackups = allBackups.Select(filename => _directoryService.FileSystem.FileInfo.FromFileName(filename))
|
||||
var expiredBackups = allBackups.Select(filename => _directoryService.FileSystem.FileInfo.New(filename))
|
||||
.Where(f => f.CreationTime < deltaTime)
|
||||
.ToList();
|
||||
|
||||
|
@ -198,7 +196,7 @@ public class CleanupService : ICleanupService
|
|||
var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalLogs;
|
||||
var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold));
|
||||
var allLogs = _directoryService.GetFiles(_directoryService.LogDirectory).ToList();
|
||||
var expiredLogs = allLogs.Select(filename => _directoryService.FileSystem.FileInfo.FromFileName(filename))
|
||||
var expiredLogs = allLogs.Select(filename => _directoryService.FileSystem.FileInfo.New(filename))
|
||||
.Where(f => f.CreationTime < deltaTime)
|
||||
.ToList();
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
|
@ -50,7 +49,8 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||
public async Task ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId);
|
||||
if (library == null) return;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.WordCountAnalyzerProgressEvent(libraryId, 0F, ProgressEventType.Started, string.Empty));
|
||||
|
|
|
@ -18,24 +18,24 @@ public class ParsedSeries
|
|||
/// <summary>
|
||||
/// Name of the Series
|
||||
/// </summary>
|
||||
public string Name { get; init; }
|
||||
public required string Name { get; init; }
|
||||
/// <summary>
|
||||
/// Normalized Name of the Series
|
||||
/// </summary>
|
||||
public string NormalizedName { get; init; }
|
||||
public required string NormalizedName { get; init; }
|
||||
/// <summary>
|
||||
/// Format of the Series
|
||||
/// </summary>
|
||||
public MangaFormat Format { get; init; }
|
||||
public required MangaFormat Format { get; init; }
|
||||
}
|
||||
|
||||
public class SeriesModified
|
||||
{
|
||||
public string FolderPath { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public required string FolderPath { get; set; }
|
||||
public required string SeriesName { get; set; }
|
||||
public DateTime LastScanned { get; set; }
|
||||
public MangaFormat Format { get; set; }
|
||||
public IEnumerable<string> LibraryRoots { get; set; }
|
||||
public IEnumerable<string> LibraryRoots { get; set; } = ArraySegment<string>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -166,16 +166,16 @@ public class ParseScannedFiles
|
|||
/// </summary>
|
||||
/// <param name="scannedSeries">A localized list of a series' parsed infos</param>
|
||||
/// <param name="info"></param>
|
||||
private void TrackSeries(ConcurrentDictionary<ParsedSeries, List<ParserInfo>> scannedSeries, ParserInfo info)
|
||||
private void TrackSeries(ConcurrentDictionary<ParsedSeries, List<ParserInfo>> scannedSeries, ParserInfo? info)
|
||||
{
|
||||
if (info.Series == string.Empty) return;
|
||||
if (info == null || info.Series == string.Empty) return;
|
||||
|
||||
// Check if normalized info.Series already exists and if so, update info to use that name instead
|
||||
info.Series = MergeName(scannedSeries, info);
|
||||
|
||||
var normalizedSeries = Parser.Parser.Normalize(info.Series);
|
||||
var normalizedSortSeries = Parser.Parser.Normalize(info.SeriesSort);
|
||||
var normalizedLocalizedSeries = Parser.Parser.Normalize(info.LocalizedSeries);
|
||||
var normalizedSeries = info.Series.ToNormalized();
|
||||
var normalizedSortSeries = info.SeriesSort.ToNormalized();
|
||||
var normalizedLocalizedSeries = info.LocalizedSeries.ToNormalized();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -224,19 +224,19 @@ public class ParseScannedFiles
|
|||
/// <returns>Series Name to group this info into</returns>
|
||||
private string MergeName(ConcurrentDictionary<ParsedSeries, List<ParserInfo>> scannedSeries, ParserInfo info)
|
||||
{
|
||||
var normalizedSeries = Parser.Parser.Normalize(info.Series);
|
||||
var normalizedLocalSeries = Parser.Parser.Normalize(info.LocalizedSeries);
|
||||
var normalizedSeries = info.Series.ToNormalized();
|
||||
var normalizedLocalSeries = info.LocalizedSeries.ToNormalized();
|
||||
|
||||
try
|
||||
{
|
||||
var existingName =
|
||||
scannedSeries.SingleOrDefault(p =>
|
||||
(Parser.Parser.Normalize(p.Key.NormalizedName).Equals(normalizedSeries) ||
|
||||
Parser.Parser.Normalize(p.Key.NormalizedName).Equals(normalizedLocalSeries)) &&
|
||||
(p.Key.NormalizedName.ToNormalized().Equals(normalizedSeries) ||
|
||||
p.Key.NormalizedName.ToNormalized().Equals(normalizedLocalSeries)) &&
|
||||
p.Key.Format == info.Format)
|
||||
.Key;
|
||||
|
||||
if (existingName != null && !string.IsNullOrEmpty(existingName.Name))
|
||||
if (!string.IsNullOrEmpty(existingName.Name))
|
||||
{
|
||||
return existingName.Name;
|
||||
}
|
||||
|
@ -245,8 +245,8 @@ public class ParseScannedFiles
|
|||
{
|
||||
_logger.LogCritical(ex, "[ScannerService] Multiple series detected for {SeriesName} ({File})! This is critical to fix! There should only be 1", info.Series, info.FullFilePath);
|
||||
var values = scannedSeries.Where(p =>
|
||||
(Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries ||
|
||||
Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) &&
|
||||
(p.Key.NormalizedName.ToNormalized() == normalizedSeries ||
|
||||
p.Key.NormalizedName.ToNormalized() == normalizedLocalSeries) &&
|
||||
p.Key.Format == info.Format);
|
||||
foreach (var pair in values)
|
||||
{
|
||||
|
@ -272,7 +272,7 @@ public class ParseScannedFiles
|
|||
/// <returns></returns>
|
||||
public async Task ScanLibrariesForSeries(LibraryType libraryType,
|
||||
IEnumerable<string> folders, string libraryName, bool isLibraryScan,
|
||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<Tuple<bool, IList<ParserInfo>>, Task> processSeriesInfos, bool forceCheck = false)
|
||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<Tuple<bool, IList<ParserInfo>>, Task>? processSeriesInfos, bool forceCheck = false)
|
||||
{
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", libraryName, ProgressEventType.Started));
|
||||
|
@ -287,7 +287,8 @@ public class ParseScannedFiles
|
|||
Series = fp.SeriesName,
|
||||
Format = fp.Format,
|
||||
}).ToList();
|
||||
await processSeriesInfos.Invoke(new Tuple<bool, IList<ParserInfo>>(true, parsedInfos));
|
||||
if (processSeriesInfos != null)
|
||||
await processSeriesInfos.Invoke(new Tuple<bool, IList<ParserInfo>>(true, parsedInfos));
|
||||
_logger.LogDebug("[ScannerService] Skipped File Scan for {Folder} as it hasn't changed since last scan", folder);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.FileScanProgressEvent("Skipped " + normalizedFolder, libraryName, ProgressEventType.Updated));
|
||||
|
@ -310,7 +311,7 @@ public class ParseScannedFiles
|
|||
.ToList();
|
||||
|
||||
|
||||
MergeLocalizedSeriesWithSeries(infos);
|
||||
MergeLocalizedSeriesWithSeries(infos!);
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
|
@ -322,7 +323,7 @@ public class ParseScannedFiles
|
|||
{
|
||||
_logger.LogError(ex,
|
||||
"[ScannerService] There was an exception that occurred during tracking {FilePath}. Skipping this file",
|
||||
info.FullFilePath);
|
||||
info?.FullFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -390,7 +391,7 @@ public class ParseScannedFiles
|
|||
if (string.IsNullOrEmpty(localizedSeries)) return;
|
||||
|
||||
// NOTE: If we have multiple series in a folder with a localized title, then this will fail. It will group into one series. User needs to fix this themselves.
|
||||
string nonLocalizedSeries;
|
||||
string? nonLocalizedSeries;
|
||||
// Normalize this as many of the cases is a capitalization difference
|
||||
var nonLocalizedSeriesFound = infos
|
||||
.Where(i => !i.IsSpecial)
|
||||
|
@ -409,11 +410,11 @@ public class ParseScannedFiles
|
|||
nonLocalizedSeries = nonLocalizedSeriesFound.FirstOrDefault(s => !s.Equals(localizedSeries));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(nonLocalizedSeries)) return;
|
||||
if (nonLocalizedSeries == null) return;
|
||||
|
||||
var normalizedNonLocalizedSeries = Parser.Parser.Normalize(nonLocalizedSeries);
|
||||
var normalizedNonLocalizedSeries = nonLocalizedSeries.ToNormalized();
|
||||
foreach (var infoNeedingMapping in infos.Where(i =>
|
||||
!Parser.Parser.Normalize(i.Series).Equals(normalizedNonLocalizedSeries)))
|
||||
!i.Series.ToNormalized().Equals(normalizedNonLocalizedSeries)))
|
||||
{
|
||||
infoNeedingMapping.Series = nonLocalizedSeries;
|
||||
infoNeedingMapping.LocalizedSeries = localizedSeries;
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace API.Services.Tasks.Scanner.Parser;
|
|||
|
||||
public interface IDefaultParser
|
||||
{
|
||||
ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga);
|
||||
ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga);
|
||||
void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret);
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ public class DefaultParser : IDefaultParser
|
|||
/// <param name="rootPath">Root folder</param>
|
||||
/// <param name="type">Defaults to Manga. Allows different Regex to be used for parsing.</param>
|
||||
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
|
||||
public ParserInfo Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga)
|
||||
public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga)
|
||||
{
|
||||
var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
|
||||
// TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this.
|
||||
|
@ -134,7 +134,7 @@ public class DefaultParser : IDefaultParser
|
|||
|
||||
if (fallbackFolders.Count == 0)
|
||||
{
|
||||
var rootFolderName = _directoryService.FileSystem.DirectoryInfo.FromDirectoryName(rootPath).Name;
|
||||
var rootFolderName = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
|
||||
var series = Parser.ParseSeries(rootFolderName);
|
||||
|
||||
if (string.IsNullOrEmpty(series))
|
||||
|
|
|
@ -11,11 +11,13 @@ public static class Parser
|
|||
{
|
||||
public const string DefaultChapter = "0";
|
||||
public const string DefaultVolume = "0";
|
||||
private const int RegexTimeoutMs = 5000000; // 500 ms
|
||||
public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif)";
|
||||
public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt";
|
||||
private const string BookFileExtensions = @"\.epub|\.pdf";
|
||||
private const string XmlRegexExtensions = @"\.xml";
|
||||
public const string MacOsMetadataFileStartsWith = @"._";
|
||||
|
||||
public const string SupportedExtensions =
|
||||
|
@ -24,6 +26,37 @@ public static class Parser
|
|||
private const RegexOptions MatchOptions =
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant;
|
||||
|
||||
private static readonly ImmutableArray<string> FormatTagSpecialKeywords = ImmutableArray.Create(
|
||||
"Special", "Reference", "Director's Cut", "Box Set", "Box-Set", "Annual", "Anthology", "Epilogue",
|
||||
"One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel",
|
||||
"GN", "FCBD");
|
||||
|
||||
private static readonly char[] LeadingZeroesTrimChars = new[] { '0' };
|
||||
|
||||
private static readonly char[] SpacesAndSeparators = { '\0', '\t', '\r', ' ', '-', ','};
|
||||
|
||||
|
||||
private const string Number = @"\d+(\.\d)?";
|
||||
private const string NumberRange = Number + @"(-" + Number + @")?";
|
||||
|
||||
/// <summary>
|
||||
/// non greedy matching of a string where parenthesis are balanced
|
||||
/// </summary>
|
||||
public const string BalancedParen = @"(?:[^()]|(?<open>\()|(?<-open>\)))*?(?(open)(?!))";
|
||||
/// <summary>
|
||||
/// non greedy matching of a string where square brackets are balanced
|
||||
/// </summary>
|
||||
public const string BalancedBracket = @"(?:[^\[\]]|(?<open>\[)|(?<-open>\]))*?(?(open)(?!))";
|
||||
/// <summary>
|
||||
/// Matches [Complete], release tags like [kmts] but not [ Complete ] or [kmts ]
|
||||
/// </summary>
|
||||
private const string TagsInBrackets = $@"\[(?!\s){BalancedBracket}(?<!\s)\]";
|
||||
/// <summary>
|
||||
/// Common regex patterns present in both Comics and Mangas
|
||||
/// </summary>
|
||||
private const string CommonSpecial = @"Specials?|One[- ]?Shot|Extra(?:\sChapter)?(?=\s)|Art Collection|Side Stories|Bonus";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Matches against font-family css syntax. Does not match if url import has data: starting, as that is binary data
|
||||
/// </summary>
|
||||
|
@ -44,7 +77,6 @@ public static class Parser
|
|||
MatchOptions, RegexTimeout);
|
||||
|
||||
|
||||
private const string XmlRegexExtensions = @"\.xml";
|
||||
private static readonly Regex ImageRegex = new Regex(ImageFileExtensions,
|
||||
MatchOptions, RegexTimeout);
|
||||
private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions,
|
||||
|
@ -67,14 +99,6 @@ public static class Parser
|
|||
private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
private const string Number = @"\d+(\.\d)?";
|
||||
private const string NumberRange = Number + @"(-" + Number + @")?";
|
||||
|
||||
// Some generic reusage regex patterns:
|
||||
// - non greedy matching of a string where parenthesis are balanced
|
||||
public const string BalancedParen = @"(?:[^()]|(?<open>\()|(?<-open>\)))*?(?(open)(?!))";
|
||||
// - non greedy matching of a string where square brackets are balanced
|
||||
public const string BalancedBrack = @"(?:[^\[\]]|(?<open>\[)|(?<-open>\]))*?(?(open)(?!))";
|
||||
|
||||
private static readonly Regex[] MangaVolumeRegex = new[]
|
||||
{
|
||||
|
@ -86,7 +110,6 @@ public static class Parser
|
|||
new Regex(
|
||||
@"(?<Series>.*)(\b|_)(?!\[)(vol\.?)(?<Volume>\d+(-\d+)?)(?!\])",
|
||||
MatchOptions, RegexTimeout),
|
||||
// TODO: In .NET 7, update this to use raw literal strings and apply the NumberRange everywhere
|
||||
// Historys Strongest Disciple Kenichi_v11_c90-98.zip or Dance in the Vampire Bund v16-17
|
||||
new Regex(
|
||||
@"(?<Series>.*)(\b|_)(?!\[)v(?<Volume>" + NumberRange + @")(?!\])",
|
||||
|
@ -576,18 +599,12 @@ public static class Parser
|
|||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
// Matches [Complete], release tags like [kmts] but not [ Complete ] or [kmts ]
|
||||
private const string TagsInBrackets = $@"\[(?!\s){BalancedBrack}(?<!\s)\]";
|
||||
|
||||
// Matches anything between balanced parenthesis, tags between brackets, {} and {Complete}
|
||||
private static readonly Regex CleanupRegex = new Regex(
|
||||
$@"(?:\({BalancedParen}\)|{TagsInBrackets}|\{{\}}|\{{Complete\}})",
|
||||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
// Common regex patterns present in both Comics and Mangas
|
||||
private const string CommonSpecial = @"Specials?|One[- ]?Shot|Extra(?:\sChapter)?(?=\s)|Art Collection|Side Stories|Bonus";
|
||||
|
||||
private static readonly Regex MangaSpecialRegex = new Regex(
|
||||
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
|
||||
$@"\b(?:{CommonSpecial}|Omake)\b",
|
||||
|
@ -601,11 +618,12 @@ public static class Parser
|
|||
);
|
||||
|
||||
private static readonly Regex EuropeanComicRegex = new Regex(
|
||||
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
|
||||
// All Keywords, does not account for checking if contains volume/chapter identification. Parser.Parse() will handle.
|
||||
@"\b(?:Bd[-\s]Fr)\b",
|
||||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
|
||||
// If SP\d+ is in the filename, we force treat it as a special regardless if volume or chapter might have been found.
|
||||
private static readonly Regex SpecialMarkerRegex = new Regex(
|
||||
@"SP\d+",
|
||||
|
@ -617,14 +635,7 @@ public static class Parser
|
|||
MatchOptions, RegexTimeout
|
||||
);
|
||||
|
||||
private static readonly ImmutableArray<string> FormatTagSpecialKeywords = ImmutableArray.Create(
|
||||
"Special", "Reference", "Director's Cut", "Box Set", "Box-Set", "Annual", "Anthology", "Epilogue",
|
||||
"One Shot", "One-Shot", "Prologue", "TPB", "Trade Paper Back", "Omnibus", "Compendium", "Absolute", "Graphic Novel",
|
||||
"GN", "FCBD");
|
||||
|
||||
private static readonly char[] LeadingZeroesTrimChars = new[] { '0' };
|
||||
|
||||
private static readonly char[] SpacesAndSeparators = { '\0', '\t', '\r', ' ', '-', ','};
|
||||
|
||||
public static MangaFormat ParseFormat(string filePath)
|
||||
{
|
||||
|
@ -669,11 +680,10 @@ public static class Parser
|
|||
foreach (var regex in MangaSeriesRegex)
|
||||
{
|
||||
var matches = regex.Matches(filename);
|
||||
foreach (var group in matches.Select(match => match.Groups["Series"])
|
||||
.Where(group => group.Success && group != Match.Empty))
|
||||
{
|
||||
return CleanTitle(group.Value);
|
||||
}
|
||||
var group = matches
|
||||
.Select(match => match.Groups["Series"])
|
||||
.FirstOrDefault(group => group.Success && group != Match.Empty);
|
||||
if (group != null) return CleanTitle(group.Value);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
@ -683,11 +693,10 @@ public static class Parser
|
|||
foreach (var regex in ComicSeriesRegex)
|
||||
{
|
||||
var matches = regex.Matches(filename);
|
||||
foreach (var group in matches.Select(match => match.Groups["Series"])
|
||||
.Where(group => group.Success && group != Match.Empty))
|
||||
{
|
||||
return CleanTitle(group.Value, true);
|
||||
}
|
||||
var group = matches
|
||||
.Select(match => match.Groups["Series"])
|
||||
.FirstOrDefault(group => group.Success && group != Match.Empty);
|
||||
if (group != null) return CleanTitle(group.Value, true);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
@ -1028,9 +1037,9 @@ public static class Parser
|
|||
/// <example>/manga/1\1 -> /manga/1/1</example>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static string NormalizePath(string path)
|
||||
public static string NormalizePath(string? path)
|
||||
{
|
||||
return path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
return string.IsNullOrEmpty(path) ? string.Empty : path.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
.Replace(@"//", Path.AltDirectorySeparatorChar + string.Empty);
|
||||
}
|
||||
|
||||
|
@ -1044,5 +1053,8 @@ public static class Parser
|
|||
return FormatTagSpecialKeywords.Contains(comicInfoFormat);
|
||||
}
|
||||
|
||||
private static string ReplaceUnderscores(string name) => name?.Replace("_", " ");
|
||||
private static string ReplaceUnderscores(string name)
|
||||
{
|
||||
return string.IsNullOrEmpty(name) ? string.Empty : name.Replace('_', ' ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ public class ParserInfo
|
|||
/// <summary>
|
||||
/// Represents the parsed series from the file or folder
|
||||
/// </summary>
|
||||
public string Series { get; set; } = string.Empty;
|
||||
public required string Series { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// This can be filled in from ComicInfo.xml/Epub during scanning. Will update the SortName field on <see cref="Entities.Series"/>
|
||||
/// </summary>
|
||||
|
@ -80,14 +80,14 @@ public class ParserInfo
|
|||
/// This will contain any EXTRA comicInfo information parsed from the epub or archive. If there is an archive with comicInfo.xml AND it contains
|
||||
/// series, volume information, that will override what we parsed.
|
||||
/// </summary>
|
||||
public ComicInfo ComicInfo { get; set; }
|
||||
public ComicInfo? ComicInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Merges non empty/null properties from info2 into this entity.
|
||||
/// </summary>
|
||||
/// <remarks>This does not merge ComicInfo as they should always be the same</remarks>
|
||||
/// <param name="info2"></param>
|
||||
public void Merge(ParserInfo info2)
|
||||
public void Merge(ParserInfo? info2)
|
||||
{
|
||||
if (info2 == null) return;
|
||||
Chapters = string.IsNullOrEmpty(Chapters) || Chapters == "0" ? info2.Chapters: Chapters;
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Data.Metadata;
|
||||
|
@ -68,6 +67,11 @@ public class ProcessSeries : IProcessSeries
|
|||
_metadataService = metadataService;
|
||||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||
_collectionTagService = collectionTagService;
|
||||
|
||||
_genres = new Dictionary<string, Genre>();
|
||||
_people = new List<Person>();
|
||||
_tags = new Dictionary<string, Tag>();
|
||||
_collectionTags = new Dictionary<string, CollectionTag>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -96,7 +100,7 @@ public class ProcessSeries : IProcessSeries
|
|||
|
||||
// Check if there is a Series
|
||||
var firstInfo = parsedInfos.First();
|
||||
Series series;
|
||||
Series? series;
|
||||
try
|
||||
{
|
||||
series =
|
||||
|
@ -131,7 +135,7 @@ public class ProcessSeries : IProcessSeries
|
|||
UpdateVolumes(series, parsedInfos, forceUpdate);
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
|
||||
series.NormalizedName = Parser.Parser.Normalize(series.Name);
|
||||
series.NormalizedName = series.Name.ToNormalized();
|
||||
series.OriginalName ??= firstParsedInfo.Series;
|
||||
if (series.Format == MangaFormat.Unknown)
|
||||
{
|
||||
|
@ -156,7 +160,7 @@ public class ProcessSeries : IProcessSeries
|
|||
if (!series.LocalizedNameLocked && !string.IsNullOrEmpty(localizedSeries))
|
||||
{
|
||||
series.LocalizedName = localizedSeries;
|
||||
series.NormalizedLocalizedName = Parser.Parser.Normalize(series.LocalizedName);
|
||||
series.NormalizedLocalizedName = series.LocalizedName.ToNormalized();
|
||||
}
|
||||
|
||||
UpdateSeriesMetadata(series, library);
|
||||
|
@ -299,17 +303,17 @@ public class ProcessSeries : IProcessSeries
|
|||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(firstChapter.Summary) && !series.Metadata.SummaryLocked)
|
||||
if (!string.IsNullOrEmpty(firstChapter?.Summary) && !series.Metadata.SummaryLocked)
|
||||
{
|
||||
series.Metadata.Summary = firstChapter.Summary;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(firstChapter.Language) && !series.Metadata.LanguageLocked)
|
||||
if (!string.IsNullOrEmpty(firstChapter?.Language) && !series.Metadata.LanguageLocked)
|
||||
{
|
||||
series.Metadata.Language = firstChapter.Language;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(firstChapter.SeriesGroup) && library.ManageCollections)
|
||||
if (!string.IsNullOrEmpty(firstChapter?.SeriesGroup) && library.ManageCollections)
|
||||
{
|
||||
_logger.LogDebug("Collection tag(s) found for {SeriesName}, updating collections", series.Name);
|
||||
|
||||
|
@ -487,7 +491,7 @@ public class ProcessSeries : IProcessSeries
|
|||
foreach (var volumeNumber in distinctVolumes)
|
||||
{
|
||||
_logger.LogDebug("[ScannerService] Looking up volume for {VolumeNumber}", volumeNumber);
|
||||
Volume volume;
|
||||
Volume? volume;
|
||||
try
|
||||
{
|
||||
volume = series.Volumes.SingleOrDefault(s => s.Name == volumeNumber);
|
||||
|
@ -568,7 +572,7 @@ public class ProcessSeries : IProcessSeries
|
|||
{
|
||||
// Specials go into their own chapters with Range being their filename and IsSpecial = True. Non-Specials with Vol and Chap as 0
|
||||
// also are treated like specials for UI grouping.
|
||||
Chapter chapter;
|
||||
Chapter? chapter;
|
||||
try
|
||||
{
|
||||
chapter = volume.Chapters.GetChapterByRange(info);
|
||||
|
@ -625,7 +629,7 @@ public class ProcessSeries : IProcessSeries
|
|||
{
|
||||
chapter.Files ??= new List<MangaFile>();
|
||||
var existingFile = chapter.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||
var fileInfo = _directoryService.FileSystem.FileInfo.FromFileName(info.FullFilePath);
|
||||
var fileInfo = _directoryService.FileSystem.FileInfo.New(info.FullFilePath);
|
||||
if (existingFile != null)
|
||||
{
|
||||
existingFile.Format = info.Format;
|
||||
|
@ -645,7 +649,6 @@ public class ProcessSeries : IProcessSeries
|
|||
}
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private void UpdateChapterFromComicInfo(Chapter chapter, ComicInfo? info)
|
||||
{
|
||||
var firstFile = chapter.Files.MinBy(x => x.Chapter);
|
||||
|
@ -813,7 +816,6 @@ public class ProcessSeries : IProcessSeries
|
|||
}
|
||||
return ImmutableList<string>.Empty;
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of all existing people, this will check the new names and roles and if it doesn't exist in allPeople, will create and
|
||||
|
@ -830,9 +832,9 @@ public class ProcessSeries : IProcessSeries
|
|||
|
||||
foreach (var name in names)
|
||||
{
|
||||
var normalizedName = Parser.Parser.Normalize(name);
|
||||
var normalizedName = name.ToNormalized();
|
||||
var person = allPeopleTypeRole.FirstOrDefault(p =>
|
||||
p.NormalizedName.Equals(normalizedName));
|
||||
p.NormalizedName != null && p.NormalizedName.Equals(normalizedName));
|
||||
if (person == null)
|
||||
{
|
||||
person = DbFactory.Person(name, role);
|
||||
|
@ -855,7 +857,7 @@ public class ProcessSeries : IProcessSeries
|
|||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
var normalizedName = Parser.Parser.Normalize(name);
|
||||
var normalizedName = name.ToNormalized();
|
||||
if (string.IsNullOrEmpty(normalizedName)) continue;
|
||||
|
||||
_genres.TryGetValue(normalizedName, out var genre);
|
||||
|
@ -885,7 +887,7 @@ public class ProcessSeries : IProcessSeries
|
|||
{
|
||||
if (string.IsNullOrEmpty(name.Trim())) continue;
|
||||
|
||||
var normalizedName = Parser.Parser.Normalize(name);
|
||||
var normalizedName = name.ToNormalized();
|
||||
_tags.TryGetValue(normalizedName, out var tag);
|
||||
|
||||
var added = tag == null;
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.Data;
|
|||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Parser;
|
||||
using API.Services.Tasks.Metadata;
|
||||
|
@ -115,7 +116,7 @@ public class ScannerService : IScannerService
|
|||
|
||||
foreach (var file in missingExtensions)
|
||||
{
|
||||
var fileInfo = _directoryService.FileSystem.FileInfo.FromFileName(file.FilePath);
|
||||
var fileInfo = _directoryService.FileSystem.FileInfo.New(file.FilePath);
|
||||
if (!fileInfo.Exists)continue;
|
||||
file.Extension = fileInfo.Extension.ToLowerInvariant();
|
||||
file.Bytes = fileInfo.Length;
|
||||
|
@ -134,7 +135,7 @@ public class ScannerService : IScannerService
|
|||
/// <param name="folder"></param>
|
||||
public async Task ScanFolder(string folder)
|
||||
{
|
||||
Series series = null;
|
||||
Series? series = null;
|
||||
try
|
||||
{
|
||||
series = await _unitOfWork.SeriesRepository.GetSeriesByFolderPath(folder, SeriesIncludes.Library);
|
||||
|
@ -193,6 +194,7 @@ public class ScannerService : IScannerService
|
|||
if (series == null) return; // This can occur when UI deletes a series but doesn't update and user re-requests update
|
||||
var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId});
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders);
|
||||
if (library == null) return;
|
||||
var libraryPaths = library.Folders.Select(f => f.Path).ToList();
|
||||
if (await ShouldScanSeries(seriesId, library, libraryPaths, series, true) != ScanCancelReason.NoCancel)
|
||||
{
|
||||
|
@ -216,7 +218,7 @@ public class ScannerService : IScannerService
|
|||
folderPath = seriesDirs.Keys.FirstOrDefault();
|
||||
|
||||
// We should check if folderPath is a library folder path and if so, return early and tell user to correct their setup.
|
||||
if (libraryPaths.Contains(folderPath))
|
||||
if (!string.IsNullOrEmpty(folderPath) && libraryPaths.Contains(folderPath))
|
||||
{
|
||||
_logger.LogCritical("[ScannerSeries] {SeriesName} scan aborted. Files for series are not in a nested folder under library path. Correct this and rescan", series.Name);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Files for series are not in a nested folder under library path. Correct this and rescan."));
|
||||
|
@ -246,12 +248,12 @@ public class ScannerService : IScannerService
|
|||
var foundParsedSeries = new ParsedSeries()
|
||||
{
|
||||
Name = parsedFiles.First().Series,
|
||||
NormalizedName = Scanner.Parser.Parser.Normalize(parsedFiles.First().Series),
|
||||
NormalizedName = parsedFiles.First().Series.ToNormalized(),
|
||||
Format = parsedFiles.First().Format
|
||||
};
|
||||
|
||||
// For Scan Series, we need to filter out anything that isn't our Series
|
||||
if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(Scanner.Parser.Parser.Normalize(series.OriginalName)))
|
||||
if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(series.OriginalName?.ToNormalized()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -275,9 +277,7 @@ public class ScannerService : IScannerService
|
|||
if (parsedSeries.Count == 0)
|
||||
{
|
||||
var seriesFiles = (await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id));
|
||||
var anyFilesExist = seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath));
|
||||
|
||||
if (!anyFilesExist)
|
||||
if (!string.IsNullOrEmpty(series.FolderPath) && !seriesFiles.Where(f => f.FilePath.Contains(series.FolderPath)).Any(m => File.Exists(m.FilePath)))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -320,7 +320,8 @@ public class ScannerService : IScannerService
|
|||
private async Task<ScanCancelReason> ShouldScanSeries(int seriesId, Library library, IList<string> libraryPaths, Series series, bool bypassFolderChecks = false)
|
||||
{
|
||||
var seriesFolderPaths = (await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId))
|
||||
.Select(f => _directoryService.FileSystem.FileInfo.FromFileName(f.FilePath).Directory.FullName)
|
||||
.Select(f => _directoryService.FileSystem.FileInfo.New(f.FilePath).Directory?.FullName ?? string.Empty)
|
||||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
|
@ -463,7 +464,7 @@ public class ScannerService : IScannerService
|
|||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders);
|
||||
var libraryFolderPaths = library.Folders.Select(fp => fp.Path).ToList();
|
||||
var libraryFolderPaths = library!.Folders.Select(fp => fp.Path).ToList();
|
||||
if (!await CheckMounts(library.Name, libraryFolderPaths)) return;
|
||||
|
||||
|
||||
|
@ -589,7 +590,7 @@ public class ScannerService : IScannerService
|
|||
}
|
||||
|
||||
private async Task<long> ScanFiles(Library library, IEnumerable<string> dirs,
|
||||
bool isLibraryScan, Func<Tuple<bool, IList<ParserInfo>>, Task> processSeriesInfos = null, bool forceChecks = false)
|
||||
bool isLibraryScan, Func<Tuple<bool, IList<ParserInfo>>, Task>? processSeriesInfos = null, bool forceChecks = false)
|
||||
{
|
||||
var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService, _eventHub);
|
||||
var scanWatch = Stopwatch.StartNew();
|
||||
|
@ -602,26 +603,6 @@ public class ScannerService : IScannerService
|
|||
return scanElapsedTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove any user progress rows that no longer exist since scan library ran and deleted series/volumes/chapters
|
||||
/// </summary>
|
||||
private async Task CleanupAbandonedChapters()
|
||||
{
|
||||
var cleanedUp = await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
|
||||
_logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up any abandoned rows due to removals from Scan loop
|
||||
/// </summary>
|
||||
private async Task CleanupDbEntities()
|
||||
{
|
||||
await CleanupAbandonedChapters();
|
||||
var cleanedUp = await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||
_logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp);
|
||||
}
|
||||
|
||||
public static IEnumerable<Series> FindSeriesNotOnDisk(IEnumerable<Series> existingSeries, Dictionary<ParsedSeries, IList<ParserInfo>> parsedSeries)
|
||||
{
|
||||
return existingSeries.Where(es => !ParserInfoHelpers.SeriesHasMatchingParserInfoFormat(es, parsedSeries));
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums.Theme;
|
||||
using API.Extensions;
|
||||
using API.SignalR;
|
||||
using Kavita.Common;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -56,7 +57,7 @@ public class ThemeService : IThemeService
|
|||
var reservedNames = Seed.DefaultThemes.Select(t => t.NormalizedName).ToList();
|
||||
var themeFiles = _directoryService
|
||||
.GetFilesWithExtension(Scanner.Parser.Parser.NormalizePath(_directoryService.SiteThemeDirectory), @"\.css")
|
||||
.Where(name => !reservedNames.Contains(Scanner.Parser.Parser.Normalize(name))).ToList();
|
||||
.Where(name => !reservedNames.Contains(name.ToNormalized())).ToList();
|
||||
|
||||
var allThemes = (await _unitOfWork.SiteThemeRepository.GetThemes()).ToList();
|
||||
|
||||
|
@ -78,7 +79,7 @@ public class ThemeService : IThemeService
|
|||
foreach (var themeFile in themeFiles)
|
||||
{
|
||||
var themeName =
|
||||
Scanner.Parser.Parser.Normalize(_directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile));
|
||||
_directoryService.FileSystem.Path.GetFileNameWithoutExtension(themeFile).ToNormalized();
|
||||
if (allThemeNames.Contains(themeName)) continue;
|
||||
|
||||
_unitOfWork.SiteThemeRepository.Add(new SiteTheme()
|
||||
|
|
|
@ -161,11 +161,11 @@ public class StatsService : IStatsService
|
|||
|
||||
if (firstAdminUser != null)
|
||||
{
|
||||
var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName));
|
||||
var activeTheme = firstAdminUserPref.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault);
|
||||
var firstAdminUserPref = (await _unitOfWork.UserRepository.GetPreferencesAsync(firstAdminUser.UserName!));
|
||||
var activeTheme = firstAdminUserPref?.Theme ?? Seed.DefaultThemes.First(t => t.IsDefault);
|
||||
|
||||
serverInfo.ActiveSiteTheme = activeTheme.Name;
|
||||
serverInfo.MangaReaderMode = firstAdminUserPref.ReaderMode;
|
||||
if (firstAdminUserPref != null) serverInfo.MangaReaderMode = firstAdminUserPref.ReaderMode;
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
|
@ -242,7 +242,7 @@ public class StatsService : IStatsService
|
|||
// If first time flow, just return 0
|
||||
if (!await _context.Series.AnyAsync()) return 0;
|
||||
return await _context.Series
|
||||
.Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series).Count())
|
||||
.Select(s => _context.Library.Where(l => l.Id == s.LibraryId).SelectMany(l => l.Series!).Count())
|
||||
.MaxAsync();
|
||||
}
|
||||
|
||||
|
@ -254,7 +254,7 @@ public class StatsService : IStatsService
|
|||
.Select(v => new
|
||||
{
|
||||
v.SeriesId,
|
||||
Count = _context.Series.Where(s => s.Id == v.SeriesId).SelectMany(s => s.Volumes).Count()
|
||||
Count = _context.Series.Where(s => s.Id == v.SeriesId).SelectMany(s => s.Volumes!).Count()
|
||||
})
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
|
@ -268,9 +268,9 @@ public class StatsService : IStatsService
|
|||
return await _context.Series
|
||||
.AsNoTracking()
|
||||
.AsSplitQuery()
|
||||
.MaxAsync(s => s.Volumes
|
||||
.MaxAsync(s => s.Volumes!
|
||||
.Where(v => v.Number == 0)
|
||||
.SelectMany(v => v.Chapters)
|
||||
.SelectMany(v => v.Chapters!)
|
||||
.Count());
|
||||
}
|
||||
|
||||
|
@ -292,13 +292,14 @@ public class StatsService : IStatsService
|
|||
|
||||
private IEnumerable<FileFormatDto> AllFormats()
|
||||
{
|
||||
// TODO: Rewrite this with new migration code in feature/basic-stats
|
||||
var results = _context.MangaFile
|
||||
.AsNoTracking()
|
||||
.AsEnumerable()
|
||||
.Select(m => new FileFormatDto()
|
||||
{
|
||||
Format = m.Format,
|
||||
Extension = Path.GetExtension(m.FilePath)?.ToLowerInvariant()
|
||||
Extension = Path.GetExtension(m.FilePath)?.ToLowerInvariant()!
|
||||
})
|
||||
.DistinctBy(f => f.Extension)
|
||||
.ToList();
|
||||
|
|
|
@ -4,48 +4,46 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using API.DTOs.Update;
|
||||
using API.SignalR;
|
||||
using API.SignalR.Presence;
|
||||
using Flurl.Http;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Kavita.Common.Helpers;
|
||||
using MarkdownDeep;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services.Tasks;
|
||||
|
||||
internal class GithubReleaseMetadata
|
||||
internal abstract class GithubReleaseMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the Tag
|
||||
/// <example>v0.4.3</example>
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public string Tag_Name { get; init; }
|
||||
public required string Tag_Name { get; init; }
|
||||
/// <summary>
|
||||
/// Name of the Release
|
||||
/// </summary>
|
||||
public string Name { get; init; }
|
||||
public required string Name { get; init; }
|
||||
/// <summary>
|
||||
/// Body of the Release
|
||||
/// </summary>
|
||||
public string Body { get; init; }
|
||||
public required string Body { get; init; }
|
||||
/// <summary>
|
||||
/// Url of the release on Github
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public string Html_Url { get; init; }
|
||||
public required string Html_Url { get; init; }
|
||||
/// <summary>
|
||||
/// Date Release was Published
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public string Published_At { get; init; }
|
||||
public required string Published_At { get; init; }
|
||||
}
|
||||
|
||||
public interface IVersionUpdaterService
|
||||
{
|
||||
Task<UpdateNotificationDto> CheckForUpdate();
|
||||
Task<UpdateNotificationDto?> CheckForUpdate();
|
||||
Task PushUpdate(UpdateNotificationDto update);
|
||||
Task<IEnumerable<UpdateNotificationDto>> GetAllReleases();
|
||||
}
|
||||
|
@ -79,16 +77,17 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
{
|
||||
var update = await GetGithubRelease();
|
||||
var dto = CreateDto(update);
|
||||
if (dto == null) return null;
|
||||
return new Version(dto.UpdateVersion) <= new Version(dto.CurrentVersion) ? null : dto;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
|
||||
{
|
||||
var updates = await GetGithubReleases();
|
||||
return updates.Select(CreateDto);
|
||||
return updates.Select(CreateDto).Where(d => d != null)!;
|
||||
}
|
||||
|
||||
private UpdateNotificationDto CreateDto(GithubReleaseMetadata update)
|
||||
private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update)
|
||||
{
|
||||
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;
|
||||
var updateVersion = new Version(update.Tag_Name.Replace("v", string.Empty));
|
||||
|
@ -106,7 +105,7 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
};
|
||||
}
|
||||
|
||||
public async Task PushUpdate(UpdateNotificationDto update)
|
||||
public async Task PushUpdate(UpdateNotificationDto? update)
|
||||
{
|
||||
if (update == null) return;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace API.Services;
|
|||
public interface ITokenService
|
||||
{
|
||||
Task<string> CreateToken(AppUser user);
|
||||
Task<TokenRequestDto> ValidateRefreshToken(TokenRequestDto request);
|
||||
Task<TokenRequestDto?> ValidateRefreshToken(TokenRequestDto request);
|
||||
Task<string> CreateRefreshToken(AppUser user);
|
||||
}
|
||||
|
||||
|
@ -33,14 +33,14 @@ public class TokenService : ITokenService
|
|||
{
|
||||
|
||||
_userManager = userManager;
|
||||
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
|
||||
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"] ?? string.Empty));
|
||||
}
|
||||
|
||||
public async Task<string> CreateToken(AppUser user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Name, user.UserName),
|
||||
new Claim(JwtRegisteredClaimNames.Name, user.UserName!),
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
};
|
||||
|
||||
|
@ -71,11 +71,12 @@ public class TokenService : ITokenService
|
|||
return refreshToken;
|
||||
}
|
||||
|
||||
public async Task<TokenRequestDto> ValidateRefreshToken(TokenRequestDto request)
|
||||
public async Task<TokenRequestDto?> ValidateRefreshToken(TokenRequestDto request)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var tokenContent = tokenHandler.ReadJwtToken(request.Token);
|
||||
var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.NameId)?.Value;
|
||||
if (string.IsNullOrEmpty(username)) return null;
|
||||
var user = await _userManager.FindByNameAsync(username);
|
||||
if (user == null) return null; // This forces a logout
|
||||
await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, "RefreshToken", request.RefreshToken);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue