.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
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue