Disable Animations + Lots of bugfixes and Polish (#1561)

* Fixed inputs not showing inline validation due to a missing class

* Fixed some checks

* Increased the button size on manga reader (develop)

* Migrated a type cast to a pure pipe

* Sped up the check for if SendTo should render on the menu

* Don't allow user to bookmark in bookmark mode

* Fixed a bug where Scan Series would skip over Specials due to how new scan loop works.

* Fixed scroll to top button persisting when navigating between pages

* Edit Series modal now doesn't have a lock field for Series, which can't be locked as it is inheritently locked.

Added some validation to ensure Name and SortName are required.

* Fixed up some spacing

* Fixed actionable menu not opening submenu on mobile

* Cleaned up the layout of cover image on series detail

* Show all volume or chapters (if only one volume) for cover selection on series

* Don't open submenu to right if there is no space

* Fixed up cover image not allowing custom saves of existing series/chapter/volume images.

Fixed up logging so console output matches log file.

* Implemented the ability to turn off css transitions in the UI.

* Updated a note internally

* Code smells

* Added InstallId when pinging the email service to allow throughput tracking
This commit is contained in:
Joe Milazzo 2022-09-26 12:40:25 -05:00 committed by GitHub
parent ee7d109170
commit 28ab34c66d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2103 additions and 444 deletions

View file

@ -49,8 +49,8 @@ public class UploadController : BaseApiController
[HttpPost("upload-by-url")]
public async Task<ActionResult<string>> GetImageFromFile(UploadUrlDto dto)
{
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace("/", "_").Replace(":", "_");
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", "");
var dateString = $"{DateTime.Now.ToShortDateString()}_{DateTime.Now.ToLongTimeString()}".Replace('/', '_').Replace(':', '_');
var format = _directoryService.FileSystem.Path.GetExtension(dto.Url.Split('?')[0]).Replace(".", string.Empty);
try
{
var path = await dto.Url

View file

@ -102,6 +102,7 @@ public class UsersController : BaseApiController
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
_unitOfWork.UserRepository.Update(existingPreferences);

View file

@ -1,7 +1,4 @@
using System;
using System.ComponentModel.DataAnnotations;
using API.Data;
using API.DTOs.Theme;
using System.ComponentModel.DataAnnotations;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Enums.UserPreferences;
@ -117,4 +114,9 @@ public class UserPreferencesDto
/// </summary>
[Required]
public bool PromptForDownloadSize { get; set; } = true;
/// <summary>
/// UI Site Global Setting: Should Kavita disable CSS transitions
/// </summary>
[Required]
public bool NoTransitions { get; set; } = false;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class AddNoTransitions : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "NoTransitions",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "NoTransitions",
table: "AppUserPreferences");
}
}
}

View file

@ -219,6 +219,9 @@ namespace API.Data.Migrations
b.Property<int>("LayoutMode")
.HasColumnType("INTEGER");
b.Property<bool>("NoTransitions")
.HasColumnType("INTEGER");
b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER");

View file

@ -351,31 +351,6 @@ public class UserRepository : IUserRepository
|| EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")
);
// This doesn't work on bookmarks themselves, only the series. For now, I don't think there is much value add
// if (filter.SortOptions != null)
// {
// if (filter.SortOptions.IsAscending)
// {
// filterSeriesQuery = filter.SortOptions.SortField switch
// {
// SortField.SortName => filterSeriesQuery.OrderBy(s => s.series.SortName),
// SortField.CreatedDate => filterSeriesQuery.OrderBy(s => s.bookmark.Created),
// SortField.LastModifiedDate => filterSeriesQuery.OrderBy(s => s.bookmark.LastModified),
// _ => filterSeriesQuery
// };
// }
// else
// {
// filterSeriesQuery = filter.SortOptions.SortField switch
// {
// SortField.SortName => filterSeriesQuery.OrderByDescending(s => s.series.SortName),
// SortField.CreatedDate => filterSeriesQuery.OrderByDescending(s => s.bookmark.Created),
// SortField.LastModifiedDate => filterSeriesQuery.OrderByDescending(s => s.bookmark.LastModified),
// _ => filterSeriesQuery
// };
// }
// }
query = filterSeriesQuery.Select(o => o.bookmark);
}

View file

@ -102,6 +102,10 @@ public class AppUserPreferences
/// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB.
/// </summary>
public bool PromptForDownloadSize { get; set; } = true;
/// <summary>
/// UI Site Global Setting: Should Kavita disable CSS transitions
/// </summary>
public bool NoTransitions { get; set; } = false;
public AppUser AppUser { get; set; }
public int AppUserId { get; set; }

View file

@ -33,9 +33,6 @@ public class Device : IEntityDate
/// </summary>
public DevicePlatform Platform { get; set; }
//public ICollection<string> SupportedExtensions { get; set; } // TODO: This requires some sort of information at mangaFile level (unless i repack)
public int AppUserId { get; set; }
public AppUser AppUser { get; set; }

View file

@ -12,7 +12,7 @@ public class Library : IEntityDate
public int Id { get; set; }
public string Name { get; set; }
/// <summary>
/// Update this summary with a way it's used, else let's remove it.
/// This is not used, but planned once we build out a Library detail page
/// </summary>
[Obsolete("This has never been coded for. Likely we can remove it.")]
public string CoverImage { get; set; }

View file

@ -1,232 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
namespace API.Helpers.Filters;
// NOTE: I'm leaving this in, but I don't think it's needed. Will validate in next release.
//[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
// public class ETagFromFilename : ActionFilterAttribute, IAsyncActionFilter
// {
// public override async Task OnActionExecutionAsync(ActionExecutingContext executingContext,
// ActionExecutionDelegate next)
// {
// var request = executingContext.HttpContext.Request;
//
// var executedContext = await next();
// var response = executedContext.HttpContext.Response;
//
// // Computing ETags for Response Caching on GET requests
// if (request.Method == HttpMethod.Get.Method && response.StatusCode == (int) HttpStatusCode.OK)
// {
// ValidateETagForResponseCaching(executedContext);
// }
// }
//
// private void ValidateETagForResponseCaching(ActionExecutedContext executedContext)
// {
// if (executedContext.Result == null)
// {
// return;
// }
//
// var request = executedContext.HttpContext.Request;
// var response = executedContext.HttpContext.Response;
//
// var objectResult = executedContext.Result as ObjectResult;
// if (objectResult == null) return;
// var result = (PhysicalFileResult) objectResult.Value;
//
// // generate ETag from LastModified property
// //var etag = GenerateEtagFromFilename(result.);
//
// // generates ETag from the entire response Content
// //var etag = GenerateEtagFromResponseBodyWithHash(result);
//
// if (request.Headers.ContainsKey(HeaderNames.IfNoneMatch))
// {
// // fetch etag from the incoming request header
// var incomingEtag = request.Headers[HeaderNames.IfNoneMatch].ToString();
//
// // if both the etags are equal
// // raise a 304 Not Modified Response
// if (incomingEtag.Equals(etag))
// {
// executedContext.Result = new StatusCodeResult((int) HttpStatusCode.NotModified);
// }
// }
//
// // add ETag response header
// response.Headers.Add(HeaderNames.ETag, new[] {etag});
// }
//
// private static string GenerateEtagFromFilename(HttpResponse response, string filename, int maxAge = 10)
// {
// if (filename is not {Length: > 0}) return string.Empty;
// var hashContent = filename + File.GetLastWriteTimeUtc(filename);
// using var sha1 = SHA256.Create();
// return string.Concat(sha1.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2")));
// }
// }
[AttributeUsage(AttributeTargets.Method)]
public class ETagFilterAttribute : Attribute, IActionFilter
{
private readonly int[] _statusCodes;
public ETagFilterAttribute(params int[] statusCodes)
{
_statusCodes = statusCodes;
if (statusCodes.Length == 0) _statusCodes = new[] { 200 };
}
public void OnActionExecuting(ActionExecutingContext context)
{
/* Nothing needs to be done here */
}
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Request.Method != "GET" || context.HttpContext.Request.Method != "HEAD") return;
if (!_statusCodes.Contains(context.HttpContext.Response.StatusCode)) return;
var etag = string.Empty;
//I just serialize the result to JSON, could do something less costly
if (context.Result is PhysicalFileResult fileResult)
{
// Do a cheap LastWriteTime etag gen
etag = ETagGenerator.GenerateEtagFromFilename(fileResult.FileName);
context.HttpContext.Response.Headers.LastModified = File.GetLastWriteTimeUtc(fileResult.FileName).ToLongDateString();
}
if (string.IsNullOrEmpty(etag))
{
var content = JsonConvert.SerializeObject(context.Result);
etag = ETagGenerator.GetETag(context.HttpContext.Request.Path.ToString(), Encoding.UTF8.GetBytes(content));
}
if (context.HttpContext.Request.Headers.IfNoneMatch.ToString() == etag)
{
context.Result = new StatusCodeResult(304);
}
//context.HttpContext.Response.Headers.ETag = etag;
}
}
// Helper class that generates the etag from a key (route) and content (response)
public static class ETagGenerator
{
public static string GetETag(string key, byte[] contentBytes)
{
var keyBytes = Encoding.UTF8.GetBytes(key);
var combinedBytes = Combine(keyBytes, contentBytes);
return GenerateETag(combinedBytes);
}
private static string GenerateETag(byte[] data)
{
using var md5 = MD5.Create();
var hash = md5.ComputeHash(data);
var hex = BitConverter.ToString(hash);
return hex.Replace("-", "");
}
private static byte[] Combine(byte[] a, byte[] b)
{
var c = new byte[a.Length + b.Length];
Buffer.BlockCopy(a, 0, c, 0, a.Length);
Buffer.BlockCopy(b, 0, c, a.Length, b.Length);
return c;
}
public static string GenerateEtagFromFilename(string filename)
{
if (filename is not {Length: > 0}) return string.Empty;
var hashContent = filename + File.GetLastWriteTimeUtc(filename);
using var md5 = MD5.Create();
return string.Concat(md5.ComputeHash(Encoding.UTF8.GetBytes(hashContent)).Select(x => x.ToString("X2")));
}
}
// /// <summary>
// /// Enables HTTP Response CacheControl management with ETag values.
// /// </summary>
// public class ClientCacheWithEtagAttribute : ActionFilterAttribute
// {
// private readonly TimeSpan _clientCache;
//
// private readonly HttpMethod[] _supportedRequestMethods = {
// HttpMethod.Get,
// HttpMethod.Head
// };
//
// /// <summary>
// /// Default constructor
// /// </summary>
// /// <param name="clientCacheInSeconds">Indicates for how long the client should cache the response. The value is in seconds</param>
// public ClientCacheWithEtagAttribute(int clientCacheInSeconds)
// {
// _clientCache = TimeSpan.FromSeconds(clientCacheInSeconds);
// }
//
// public override async Task OnActionExecutionAsync(ActionExecutingContext executingContext, ActionExecutionDelegate next)
// {
//
// if (executingContext.Response?.Content == null)
// {
// return;
// }
//
// var body = await executingContext.Response.Content.ReadAsStringAsync();
// if (body == null)
// {
// return;
// }
//
// var computedEntityTag = GetETag(Encoding.UTF8.GetBytes(body));
//
// if (actionExecutedContext.Request.Headers.IfNoneMatch.Any()
// && actionExecutedContext.Request.Headers.IfNoneMatch.First().Tag.Trim('"').Equals(computedEntityTag, StringComparison.InvariantCultureIgnoreCase))
// {
// actionExecutedContext.Response.StatusCode = HttpStatusCode.NotModified;
// actionExecutedContext.Response.Content = null;
// }
//
// var cacheControlHeader = new CacheControlHeaderValue
// {
// Private = true,
// MaxAge = _clientCache
// };
//
// actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{computedEntityTag}\"", false);
// actionExecutedContext.Response.Headers.CacheControl = cacheControlHeader;
// }
//
// private static string GetETag(byte[] contentBytes)
// {
// using (var md5 = MD5.Create())
// {
// var hash = md5.ComputeHash(contentBytes);
// string hex = BitConverter.ToString(hash);
// return hex.Replace("-", "");
// }
// }
// }

View file

@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting.Display;
namespace API.Logging;
@ -39,6 +40,7 @@ public static class LogLevelOptions
public static LoggerConfiguration CreateConfig(LoggerConfiguration configuration)
{
const string outputTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {ThreadId}] [{Level}] {SourceContext} {Message:lj}{NewLine}{Exception}";
return configuration
.MinimumLevel
.ControlledBy(LogLevelSwitch)
@ -51,11 +53,11 @@ public static class LogLevelOptions
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Error)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.WriteTo.Console()
.WriteTo.Console(new MessageTemplateTextFormatter(outputTemplate))
.WriteTo.File(LogFile,
shared: true,
rollingInterval: RollingInterval.Day,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {ThreadId}] [{Level}] {SourceContext} {Message:lj}{NewLine}{Exception}");
outputTemplate: outputTemplate);
}
public static void SwitchLogLevel(string level)

View file

@ -962,7 +962,7 @@ public class BookService : IBookService
}
catch (Exception)
{
/* Swallow exception. Some css don't have style rules ending in ; */
//Swallow exception. Some css don't have style rules ending in ';'
}
body = Regex.Replace(body, @"([\s:]0)(px|pt|%|em)", "$1");

View file

@ -46,6 +46,7 @@ public class EmailService : IEmailService
_unitOfWork = unitOfWork;
_downloadService = downloadService;
FlurlHttp.ConfigureClient(DefaultApiUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
}
@ -126,15 +127,17 @@ public class EmailService : IEmailService
return await SendEmailWithFiles(emailLink + "/api/sendto", data.FilePaths, data.DestinationEmail);
}
private static async Task<bool> SendEmailWithGet(string url, int timeoutSecs = 30)
private async Task<bool> SendEmailWithGet(string url, int timeoutSecs = 30)
{
try
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await (url)
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(timeoutSecs))
.GetStringAsync();
@ -152,15 +155,17 @@ public class EmailService : IEmailService
}
private static async Task<bool> SendEmailWithPost(string url, object data, int timeoutSecs = 30)
private async Task<bool> SendEmailWithPost(string url, object data, int timeoutSecs = 30)
{
try
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await (url)
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(timeoutSecs))
.PostJsonAsync(data);
@ -182,10 +187,12 @@ public class EmailService : IEmailService
{
try
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await (url)
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithTimeout(TimeSpan.FromSeconds(timeoutSecs))
.PostMultipartAsync(mp =>
{

View file

@ -2,6 +2,7 @@
using API.Data.Metadata;
using API.Entities.Enums;
using API.Parser;
using API.Services.Tasks.Scanner.Parser;
namespace API.Services;

View file

@ -150,7 +150,7 @@ public class LibraryWatcher : ILibraryWatcher
private void OnError(object sender, ErrorEventArgs e)
{
_logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many watches occured at once. Restarting Watchers");
_logger.LogError(e.GetException(), "[LibraryWatcher] An error occured, likely too many changes occured at once or the folder being watched was deleted. Restarting Watchers");
Task.Run(RestartWatching);
}

View file

@ -1,9 +1,9 @@
using System.IO;
using System.Linq;
using API.Entities.Enums;
using API.Services;
using API.Parser;
namespace API.Parser;
namespace API.Services.Tasks.Scanner.Parser;
public interface IDefaultParser
{
@ -36,81 +36,81 @@ public class DefaultParser : IDefaultParser
var fileName = _directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
ParserInfo ret;
if (Services.Tasks.Scanner.Parser.Parser.IsEpub(filePath))
if (Parser.IsEpub(filePath))
{
ret = new ParserInfo()
ret = new ParserInfo
{
Chapters = Services.Tasks.Scanner.Parser.Parser.ParseChapter(fileName) ?? Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(fileName),
Series = Services.Tasks.Scanner.Parser.Parser.ParseSeries(fileName) ?? Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(fileName),
Volumes = Services.Tasks.Scanner.Parser.Parser.ParseVolume(fileName) ?? Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(fileName),
Chapters = Parser.ParseChapter(fileName) ?? Parser.ParseComicChapter(fileName),
Series = Parser.ParseSeries(fileName) ?? Parser.ParseComicSeries(fileName),
Volumes = Parser.ParseVolume(fileName) ?? Parser.ParseComicVolume(fileName),
Filename = Path.GetFileName(filePath),
Format = Services.Tasks.Scanner.Parser.Parser.ParseFormat(filePath),
Format = Parser.ParseFormat(filePath),
FullFilePath = filePath
};
}
else
{
ret = new ParserInfo()
ret = new ParserInfo
{
Chapters = type == LibraryType.Comic ? Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(fileName) : Services.Tasks.Scanner.Parser.Parser.ParseChapter(fileName),
Series = type == LibraryType.Comic ? Services.Tasks.Scanner.Parser.Parser.ParseComicSeries(fileName) : Services.Tasks.Scanner.Parser.Parser.ParseSeries(fileName),
Volumes = type == LibraryType.Comic ? Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(fileName) : Services.Tasks.Scanner.Parser.Parser.ParseVolume(fileName),
Chapters = type == LibraryType.Comic ? Parser.ParseComicChapter(fileName) : Parser.ParseChapter(fileName),
Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName),
Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName),
Filename = Path.GetFileName(filePath),
Format = Services.Tasks.Scanner.Parser.Parser.ParseFormat(filePath),
Format = Parser.ParseFormat(filePath),
Title = Path.GetFileNameWithoutExtension(fileName),
FullFilePath = filePath
};
}
if (Services.Tasks.Scanner.Parser.Parser.IsImage(filePath) && Services.Tasks.Scanner.Parser.Parser.IsCoverImage(filePath)) return null;
if (Parser.IsCoverImage(filePath)) return null;
if (Services.Tasks.Scanner.Parser.Parser.IsImage(filePath))
if (Parser.IsImage(filePath))
{
// Reset Chapters, Volumes, and Series as images are not good to parse information out of. Better to use folders.
ret.Volumes = Services.Tasks.Scanner.Parser.Parser.DefaultVolume;
ret.Chapters = Services.Tasks.Scanner.Parser.Parser.DefaultChapter;
ret.Volumes = Parser.DefaultVolume;
ret.Chapters = Parser.DefaultChapter;
ret.Series = string.Empty;
}
if (ret.Series == string.Empty || Services.Tasks.Scanner.Parser.Parser.IsImage(filePath))
if (ret.Series == string.Empty || Parser.IsImage(filePath))
{
// Try to parse information out of each folder all the way to rootPath
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
}
var edition = Services.Tasks.Scanner.Parser.Parser.ParseEdition(fileName);
var edition = Parser.ParseEdition(fileName);
if (!string.IsNullOrEmpty(edition))
{
ret.Series = Services.Tasks.Scanner.Parser.Parser.CleanTitle(ret.Series.Replace(edition, ""), type is LibraryType.Comic);
ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, ""), type is LibraryType.Comic);
ret.Edition = edition;
}
var isSpecial = type == LibraryType.Comic ? Services.Tasks.Scanner.Parser.Parser.IsComicSpecial(fileName) : Services.Tasks.Scanner.Parser.Parser.IsMangaSpecial(fileName);
var isSpecial = type == LibraryType.Comic ? Parser.IsComicSpecial(fileName) : Parser.IsMangaSpecial(fileName);
// We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that
// could cause a problem as Omake is a special term, but there is valid volume/chapter information.
if (ret.Chapters == Services.Tasks.Scanner.Parser.Parser.DefaultChapter && ret.Volumes == Services.Tasks.Scanner.Parser.Parser.DefaultVolume && isSpecial)
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.DefaultVolume && isSpecial)
{
ret.IsSpecial = true;
ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder
}
// If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name
if (Services.Tasks.Scanner.Parser.Parser.HasSpecialMarker(fileName))
if (Parser.HasSpecialMarker(fileName))
{
ret.IsSpecial = true;
ret.Chapters = Services.Tasks.Scanner.Parser.Parser.DefaultChapter;
ret.Volumes = Services.Tasks.Scanner.Parser.Parser.DefaultVolume;
ret.Chapters = Parser.DefaultChapter;
ret.Volumes = Parser.DefaultVolume;
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
}
if (string.IsNullOrEmpty(ret.Series))
{
ret.Series = Services.Tasks.Scanner.Parser.Parser.CleanTitle(fileName, type is LibraryType.Comic);
ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic);
}
// Pdfs may have .pdf in the series name, remove that
if (Services.Tasks.Scanner.Parser.Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf"))
if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf"))
{
ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length);
}
@ -127,35 +127,55 @@ public class DefaultParser : IDefaultParser
/// <param name="ret">Expects a non-null ParserInfo which this method will populate</param>
public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret)
{
var fallbackFolders = _directoryService.GetFoldersTillRoot(rootPath, filePath).ToList();
var fallbackFolders = _directoryService.GetFoldersTillRoot(rootPath, filePath)
.Where(f => !Parser.IsMangaSpecial(f))
.ToList();
if (fallbackFolders.Count == 0)
{
var rootFolderName = _directoryService.FileSystem.DirectoryInfo.FromDirectoryName(rootPath).Name;
var series = Parser.ParseSeries(rootFolderName);
if (string.IsNullOrEmpty(series))
{
ret.Series = Parser.CleanTitle(rootFolderName, type is LibraryType.Comic);
return;
}
if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) || !rootFolderName.Contains(ret.Series)))
{
ret.Series = series;
return;
}
}
for (var i = 0; i < fallbackFolders.Count; i++)
{
var folder = fallbackFolders[i];
if (Services.Tasks.Scanner.Parser.Parser.IsMangaSpecial(folder)) continue;
var parsedVolume = type is LibraryType.Manga ? Services.Tasks.Scanner.Parser.Parser.ParseVolume(folder) : Services.Tasks.Scanner.Parser.Parser.ParseComicVolume(folder);
var parsedChapter = type is LibraryType.Manga ? Services.Tasks.Scanner.Parser.Parser.ParseChapter(folder) : Services.Tasks.Scanner.Parser.Parser.ParseComicChapter(folder);
var parsedVolume = type is LibraryType.Manga ? Parser.ParseVolume(folder) : Parser.ParseComicVolume(folder);
var parsedChapter = type is LibraryType.Manga ? Parser.ParseChapter(folder) : Parser.ParseComicChapter(folder);
if (!parsedVolume.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultVolume) || !parsedChapter.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
if (!parsedVolume.Equals(Parser.DefaultVolume) || !parsedChapter.Equals(Parser.DefaultChapter))
{
if ((string.IsNullOrEmpty(ret.Volumes) || ret.Volumes.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultVolume)) && !parsedVolume.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultVolume))
{
ret.Volumes = parsedVolume;
}
if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter)) && !parsedChapter.Equals(Services.Tasks.Scanner.Parser.Parser.DefaultChapter))
{
ret.Chapters = parsedChapter;
}
if ((string.IsNullOrEmpty(ret.Volumes) || ret.Volumes.Equals(Parser.DefaultVolume)) && !parsedVolume.Equals(Parser.DefaultVolume))
{
ret.Volumes = parsedVolume;
}
if ((string.IsNullOrEmpty(ret.Chapters) || ret.Chapters.Equals(Parser.DefaultChapter)) && !parsedChapter.Equals(Parser.DefaultChapter))
{
ret.Chapters = parsedChapter;
}
}
// Generally users group in series folders. Let's try to parse series from the top folder
if (!folder.Equals(ret.Series) && i == fallbackFolders.Count - 1)
{
var series = Services.Tasks.Scanner.Parser.Parser.ParseSeries(folder);
var series = Parser.ParseSeries(folder);
if (string.IsNullOrEmpty(series))
{
ret.Series = Services.Tasks.Scanner.Parser.Parser.CleanTitle(folder, type is LibraryType.Comic);
ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic);
break;
}

View file

@ -633,13 +633,11 @@ public class ProcessSeries : IProcessSeries
void AddGenre(Genre genre)
{
//chapter.Genres.Add(genre);
GenreHelper.AddGenreIfNotExists(chapter.Genres, genre);
}
void AddTag(Tag tag, bool added)
{
//chapter.Tags.Add(tag);
TagHelper.AddTagIfNotExists(chapter.Tags, tag);
}

View file

@ -206,8 +206,6 @@ public class ScannerService : IScannerService
var scanElapsedTime = await ScanFiles(library, new []{folderPath}, false, TrackFiles, true);
_logger.LogInformation("ScanFiles for {Series} took {Time}", series.Name, scanElapsedTime);
//await Task.WhenAll(processTasks);
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name));
// Remove any parsedSeries keys that don't belong to our series. This can occur when users store 2 series in the same folder