Tech Debt + Series Sort bugfix (#1192)

* Code cleanup.

When copying files, if the target file already exists, append (1), (2), etc onto the file (this is enhancing existing implementation to allow multiple numbers)

* Added a ton of null checks to UpdateSeriesMetadata and made the code work on the rare case (not really possible) that SeriesMetadata doesn't exist.

* Updated Genre code to use strings to ensure a better, more fault tolerant update experience.

* More cleanup on the codebase

* Fixed a bug where Series SortName was getting emptied on file scan

* Fixed a bad copy

* Fixed unit tests
This commit is contained in:
Joseph Milazzo 2022-04-03 16:11:16 -05:00 committed by GitHub
parent a00e8f121f
commit 19678383b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 362 additions and 138 deletions

View File

@ -210,7 +210,6 @@ namespace API.Tests.Parser
[InlineData("._Love Hina/Love Hina/", true)] [InlineData("._Love Hina/Love Hina/", true)]
[InlineData("@Recently-Snapshot/Love Hina/", true)] [InlineData("@Recently-Snapshot/Love Hina/", true)]
[InlineData("@recycle/Love Hina/", true)] [InlineData("@recycle/Love Hina/", true)]
[InlineData("@recycle/Love Hina/", true)]
[InlineData("E:/Test/__MACOSX/Love Hina/", true)] [InlineData("E:/Test/__MACOSX/Love Hina/", true)]
public void HasBlacklistedFolderInPathTest(string inputPath, bool expected) public void HasBlacklistedFolderInPathTest(string inputPath, bool expected)
{ {

View File

@ -557,6 +557,24 @@ namespace API.Tests.Services
Assert.Equal(2, ds.GetFiles("/manga/output/").Count()); Assert.Equal(2, ds.GetFiles("/manga/output/").Count());
} }
[Fact]
public void CopyFilesToDirectory_ShouldAppendWhenTargetFileExists()
{
const string testDirectory = "/manga/";
var fileSystem = new MockFileSystem();
fileSystem.AddFile($"{testDirectory}file.zip", new MockFileData(""));
fileSystem.AddFile($"/manga/output/file (1).zip", new MockFileData(""));
fileSystem.AddFile($"/manga/output/file (2).zip", new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
ds.CopyFilesToDirectory(new []{$"{testDirectory}file.zip"}, "/manga/output/");
ds.CopyFilesToDirectory(new []{$"{testDirectory}file.zip"}, "/manga/output/");
var outputFiles = ds.GetFiles("/manga/output/").Select(API.Parser.Parser.NormalizePath).ToList();
Assert.Equal(4, outputFiles.Count()); // we have 2 already there and 2 copies
// For some reason, this has C:/ on directory even though everything is emulated
Assert.True(outputFiles.Contains(API.Parser.Parser.NormalizePath("/manga/output/file (3).zip")) || outputFiles.Contains(API.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip")));
}
#endregion #endregion
#region ListDirectory #region ListDirectory

View File

@ -6,8 +6,11 @@ using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Data.Repositories; using API.Data.Repositories;
using API.DTOs; using API.DTOs;
using API.DTOs.CollectionTags;
using API.DTOs.Metadata;
using API.Entities; using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions;
using API.Helpers; using API.Helpers;
using API.Services; using API.Services;
using API.SignalR; using API.SignalR;
@ -99,6 +102,9 @@ public class SeriesServiceTests
{ {
_context.Series.RemoveRange(_context.Series.ToList()); _context.Series.RemoveRange(_context.Series.ToList());
_context.AppUserRating.RemoveRange(_context.AppUserRating.ToList()); _context.AppUserRating.RemoveRange(_context.AppUserRating.ToList());
_context.Genre.RemoveRange(_context.Genre.ToList());
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
_context.Person.RemoveRange(_context.Person.ToList());
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
@ -569,4 +575,174 @@ public class SeriesServiceTests
} }
#endregion #endregion
#region UpdateSeriesMetadata
private void SetupUpdateSeriesMetadataDb()
{
_context.Series.Add(new Series()
{
Name = "Test",
Library = new Library() {
Name = "Test LIb",
Type = LibraryType.Book,
}
});
}
[Fact]
public async Task UpdateSeriesMetadata_ShouldCreateEmptyMetadata_IfDoesntExist()
{
await ResetDb();
_context.Series.Add(new Series()
{
Name = "Test",
Library = new Library() {
Name = "Test LIb",
Type = LibraryType.Book,
}
});
await _context.SaveChangesAsync();
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
{
SeriesMetadata = new SeriesMetadataDto()
{
SeriesId = 1,
Genres = new List<GenreTagDto>() {new GenreTagDto() {Id = 0, Title = "New Genre"}}
},
CollectionTags = new List<CollectionTagDto>()
});
Assert.True(success);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
Assert.NotNull(series.Metadata);
Assert.True(series.Metadata.Genres.Select(g => g.Title).Contains("New Genre".SentenceCase()));
}
[Fact]
public async Task UpdateSeriesMetadata_ShouldCreateNewTags_IfNoneExist()
{
await ResetDb();
_context.Series.Add(new Series()
{
Name = "Test",
Library = new Library() {
Name = "Test LIb",
Type = LibraryType.Book,
}
});
await _context.SaveChangesAsync();
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
{
SeriesMetadata = new SeriesMetadataDto()
{
SeriesId = 1,
Genres = new List<GenreTagDto>() {new GenreTagDto() {Id = 0, Title = "New Genre"}},
Tags = new List<TagDto>() {new TagDto() {Id = 0, Title = "New Tag"}},
Characters = new List<PersonDto>() {new PersonDto() {Id = 0, Name = "Joe Shmo", Role = PersonRole.Character}},
Colorists = new List<PersonDto>() {new PersonDto() {Id = 0, Name = "Joe Shmo", Role = PersonRole.Colorist}},
Pencillers = new List<PersonDto>() {new PersonDto() {Id = 0, Name = "Joe Shmo 2", Role = PersonRole.Penciller}},
},
CollectionTags = new List<CollectionTagDto>()
{
new CollectionTagDto() {Id = 0, Promoted = false, Summary = string.Empty, CoverImageLocked = false, Title = "New Collection"}
}
});
Assert.True(success);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
Assert.NotNull(series.Metadata);
Assert.True(series.Metadata.Genres.Select(g => g.Title).Contains("New Genre".SentenceCase()));
Assert.True(series.Metadata.People.All(g => g.Name is "Joe Shmo" or "Joe Shmo 2"));
Assert.True(series.Metadata.Tags.Select(g => g.Title).Contains("New Tag".SentenceCase()));
Assert.True(series.Metadata.CollectionTags.Select(g => g.Title).Contains("New Collection"));
}
[Fact]
public async Task UpdateSeriesMetadata_ShouldRemoveExistingTags()
{
await ResetDb();
var s = new Series()
{
Name = "Test",
Library = new Library()
{
Name = "Test LIb",
Type = LibraryType.Book,
},
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>())
};
var g = DbFactory.Genre("Existing Genre", false);
s.Metadata.Genres = new List<Genre>() {g};
_context.Series.Add(s);
_context.Genre.Add(g);
await _context.SaveChangesAsync();
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
{
SeriesMetadata = new SeriesMetadataDto()
{
SeriesId = 1,
Genres = new List<GenreTagDto>() {new () {Id = 0, Title = "New Genre"}},
},
CollectionTags = new List<CollectionTagDto>()
});
Assert.True(success);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
Assert.NotNull(series.Metadata);
Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "New Genre".SentenceCase()));
Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked
}
[Fact]
public async Task UpdateSeriesMetadata_ShouldLockIfTold()
{
await ResetDb();
var s = new Series()
{
Name = "Test",
Library = new Library()
{
Name = "Test LIb",
Type = LibraryType.Book,
},
Metadata = DbFactory.SeriesMetadata(new List<CollectionTag>())
};
var g = DbFactory.Genre("Existing Genre", false);
s.Metadata.Genres = new List<Genre>() {g};
s.Metadata.GenresLocked = true;
_context.Series.Add(s);
_context.Genre.Add(g);
await _context.SaveChangesAsync();
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto()
{
SeriesMetadata = new SeriesMetadataDto()
{
SeriesId = 1,
Genres = new List<GenreTagDto>() {new () {Id = 1, Title = "Existing Genre"}},
GenresLocked = true
},
CollectionTags = new List<CollectionTagDto>()
});
Assert.True(success);
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
Assert.NotNull(series.Metadata);
Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "Existing Genre".SentenceCase()));
Assert.True(series.Metadata.GenresLocked);
}
#endregion
} }

View File

@ -26,20 +26,18 @@ namespace API.Controllers
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly IDownloadService _downloadService; private readonly IDownloadService _downloadService;
private readonly IEventHub _eventHub; private readonly IEventHub _eventHub;
private readonly UserManager<AppUser> _userManager;
private readonly ILogger<DownloadController> _logger; private readonly ILogger<DownloadController> _logger;
private readonly IBookmarkService _bookmarkService; private readonly IBookmarkService _bookmarkService;
private const string DefaultContentType = "application/octet-stream"; private const string DefaultContentType = "application/octet-stream";
public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService,
IDownloadService downloadService, IEventHub eventHub, UserManager<AppUser> userManager, ILogger<DownloadController> logger, IBookmarkService bookmarkService) IDownloadService downloadService, IEventHub eventHub, ILogger<DownloadController> logger, IBookmarkService bookmarkService)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_archiveService = archiveService; _archiveService = archiveService;
_directoryService = directoryService; _directoryService = directoryService;
_downloadService = downloadService; _downloadService = downloadService;
_eventHub = eventHub; _eventHub = eventHub;
_userManager = userManager;
_logger = logger; _logger = logger;
_bookmarkService = bookmarkService; _bookmarkService = bookmarkService;
} }

View File

@ -782,6 +782,7 @@ public class OpdsController : BaseApiController
{ {
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"), CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"), CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
// We can't not include acc link in the feed, panels doesn't work with just page streaming option. We have to block download directly
accLink, accLink,
CreatePageStreamLink(seriesId, volumeId, chapterId, mangaFile, apiKey) CreatePageStreamLink(seriesId, volumeId, chapterId, mangaFile, apiKey)
}, },
@ -792,14 +793,6 @@ public class OpdsController : BaseApiController
} }
}; };
// We can't not show acc link in the feed, panels wont work like that. We have to block download directly
// var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
// if (await _downloadService.HasDownloadPermission(user))
// {
// entry.Links.Add(accLink);
// }
return entry; return entry;
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using API.DTOs.CollectionTags; using API.DTOs.CollectionTags;
using API.DTOs.Metadata; using API.DTOs.Metadata;
using API.Entities.Enums; using API.Entities.Enums;
@ -8,7 +9,7 @@ namespace API.DTOs
public class SeriesMetadataDto public class SeriesMetadataDto
{ {
public int Id { get; set; } public int Id { get; set; }
public string Summary { get; set; } public string Summary { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Collections the Series belongs to /// Collections the Series belongs to
/// </summary> /// </summary>

View File

@ -18,7 +18,7 @@ namespace API.Data
{ {
public static Series Series(string name) public static Series Series(string name)
{ {
return new () return new Series
{ {
Name = name, Name = name,
OriginalName = name, OriginalName = name,

View File

@ -378,7 +378,7 @@ public class SeriesRepository : ISeriesRepository
/// <summary> /// <summary>
/// Returns Volumes, Metadata, and Collection Tags /// Returns Volumes, Metadata (Incl Genres and People), and Collection Tags
/// </summary> /// </summary>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <returns></returns> /// <returns></returns>

View File

@ -60,6 +60,12 @@ namespace API.Parser
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]", private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
MatchOptions, RegexTimeout); MatchOptions, RegexTimeout);
/// <summary>
/// Recognizes the Special token only
/// </summary>
private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+",
MatchOptions, RegexTimeout);
private static readonly Regex[] MangaVolumeRegex = new[] private static readonly Regex[] MangaVolumeRegex = new[]
{ {
@ -976,9 +982,8 @@ namespace API.Parser
/// <returns></returns> /// <returns></returns>
public static string CleanSpecialTitle(string name) public static string CleanSpecialTitle(string name)
{ {
// TODO: Optimize this code & Test
if (string.IsNullOrEmpty(name)) return name; if (string.IsNullOrEmpty(name)) return name;
var cleaned = new Regex(@"SP\d+").Replace(name.Replace('_', ' '), string.Empty).Trim(); var cleaned = SpecialTokenRegex.Replace(name.Replace('_', ' '), string.Empty).Trim();
var lastIndex = cleaned.LastIndexOf('.'); var lastIndex = cleaned.LastIndexOf('.');
if (lastIndex > 0) if (lastIndex > 0)
{ {

View File

@ -71,6 +71,8 @@ namespace API.Services
private static readonly Regex ExcludeDirectories = new Regex( private static readonly Regex ExcludeDirectories = new Regex(
@"@eaDir|\.DS_Store|\.qpkg", @"@eaDir|\.DS_Store|\.qpkg",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex FileCopyAppend = new Regex(@"\(\d+\)",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups"); public static readonly string BackupDirectory = Path.Join(Directory.GetCurrentDirectory(), "config", "backups");
public DirectoryService(ILogger<DirectoryService> logger, IFileSystem fileSystem) public DirectoryService(ILogger<DirectoryService> logger, IFileSystem fileSystem)
@ -370,24 +372,11 @@ namespace API.Services
foreach (var file in filePaths) foreach (var file in filePaths)
{ {
currentFile = file; currentFile = file;
var fileInfo = FileSystem.FileInfo.FromFileName(file); var fileInfo = FileSystem.FileInfo.FromFileName(file);
if (fileInfo.Exists) var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(file, directoryPath, prepend));
{
// TODO: I need to handle if file already exists and allow either an overwrite or prepend (2) to it fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name));
try
{
fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, prepend + fileInfo.Name));
}
catch (IOException ex)
{
_logger.LogError(ex, "File copy, dest already exists. Appending (2)");
fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, prepend + FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name) + " (2)" + FileSystem.Path.GetExtension(fileInfo.Name)));
}
}
else
{
_logger.LogWarning("Tried to copy {File} but it doesn't exist", file);
}
} }
} }
catch (Exception ex) catch (Exception ex)
@ -399,6 +388,42 @@ namespace API.Services
return true; return true;
} }
/// <summary>
/// Generates the combined filepath given a prepend (optional), output directory path, and a full input file path.
/// If the output file already exists, will append (1), (2), etc until it can be written out
/// </summary>
/// <param name="fileToCopy"></param>
/// <param name="directoryPath"></param>
/// <param name="prepend"></param>
/// <returns></returns>
private string RenameFileForCopy(string fileToCopy, string directoryPath, string prepend = "")
{
var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy);
var filename = prepend + fileInfo.Name;
var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename));
if (!targetFile.Exists)
{
return targetFile.FullName;
}
var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name);
if (FileCopyAppend.IsMatch(noExtension))
{
var match = FileCopyAppend.Match(noExtension).Value;
var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty);
noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})");
}
else
{
noExtension += " (1)";
}
var newFilename = prepend + noExtension +
FileSystem.Path.GetExtension(fileInfo.Name);
return RenameFileForCopy(FileSystem.Path.Join(directoryPath, newFilename), directoryPath, prepend);
}
/// <summary> /// <summary>
/// Lists all directories in a root path. Will exclude Hidden or System directories. /// Lists all directories in a root path. Will exclude Hidden or System directories.
/// </summary> /// </summary>

View File

@ -52,103 +52,99 @@ public class SeriesService : ISeriesService
var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList(); var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).ToList();
var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList(); var allTags = (await _unitOfWork.TagRepository.GetAllTagsAsync()).ToList();
if (series.Metadata == null) series.Metadata ??= DbFactory.SeriesMetadata(updateSeriesMetadataDto.CollectionTags
.Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList());
if (series.Metadata.AgeRating != updateSeriesMetadataDto.SeriesMetadata.AgeRating)
{ {
series.Metadata = DbFactory.SeriesMetadata(updateSeriesMetadataDto.CollectionTags series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata.AgeRating;
.Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList()); series.Metadata.AgeRatingLocked = true;
} }
else
if (series.Metadata.PublicationStatus != updateSeriesMetadataDto.SeriesMetadata.PublicationStatus)
{ {
if (series.Metadata.AgeRating != updateSeriesMetadataDto.SeriesMetadata.AgeRating) series.Metadata.PublicationStatus = updateSeriesMetadataDto.SeriesMetadata.PublicationStatus;
{ series.Metadata.PublicationStatusLocked = true;
series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata.AgeRating;
series.Metadata.AgeRatingLocked = true;
}
if (series.Metadata.PublicationStatus != updateSeriesMetadataDto.SeriesMetadata.PublicationStatus)
{
series.Metadata.PublicationStatus = updateSeriesMetadataDto.SeriesMetadata.PublicationStatus;
series.Metadata.PublicationStatusLocked = true;
}
if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim())
{
series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim();
series.Metadata.SummaryLocked = true;
}
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata.Language)
{
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language;
series.Metadata.LanguageLocked = true;
}
series.Metadata.CollectionTags ??= new List<CollectionTag>();
UpdateRelatedList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) =>
{
series.Metadata.CollectionTags.Add(tag);
});
series.Metadata.Genres ??= new List<Genre>();
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) =>
{
series.Metadata.Tags.Add(tag);
}, () => series.Metadata.TagsLocked = true);
void HandleAddPerson(Person person)
{
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person);
allPeople.Add(person);
}
series.Metadata.People ??= new List<Person>();
UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata.Writers, series, allPeople,
HandleAddPerson, () => series.Metadata.WriterLocked = true);
UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople,
HandleAddPerson, () => series.Metadata.CharacterLocked = true);
UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople,
HandleAddPerson, () => series.Metadata.ColoristLocked = true);
UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople,
HandleAddPerson, () => series.Metadata.EditorLocked = true);
UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople,
HandleAddPerson, () => series.Metadata.InkerLocked = true);
UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople,
HandleAddPerson, () => series.Metadata.LettererLocked = true);
UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople,
HandleAddPerson, () => series.Metadata.PencillerLocked = true);
UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople,
HandleAddPerson, () => series.Metadata.PublisherLocked = true);
UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople,
HandleAddPerson, () => series.Metadata.TranslatorLocked = true);
UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople,
HandleAddPerson, () => series.Metadata.CoverArtistLocked = true);
if (!updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked) series.Metadata.AgeRatingLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked) series.Metadata.PublicationStatusLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.LanguageLocked) series.Metadata.LanguageLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.GenresLocked) series.Metadata.GenresLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.TagsLocked) series.Metadata.TagsLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.CharacterLocked) series.Metadata.CharacterLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.ColoristLocked) series.Metadata.ColoristLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.EditorLocked) series.Metadata.EditorLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.InkerLocked) series.Metadata.InkerLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.LettererLocked) series.Metadata.LettererLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PencillerLocked) series.Metadata.PencillerLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PublisherLocked) series.Metadata.PublisherLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked) series.Metadata.TranslatorLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked) series.Metadata.CoverArtistLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.WriterLocked) series.Metadata.WriterLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.SummaryLocked) series.Metadata.SummaryLocked = false;
} }
if (series.Metadata.Summary != updateSeriesMetadataDto.SeriesMetadata.Summary.Trim())
{
series.Metadata.Summary = updateSeriesMetadataDto.SeriesMetadata?.Summary.Trim();
series.Metadata.SummaryLocked = true;
}
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata.Language)
{
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language;
series.Metadata.LanguageLocked = true;
}
series.Metadata.CollectionTags ??= new List<CollectionTag>();
UpdateRelatedList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) =>
{
series.Metadata.CollectionTags.Add(tag);
});
series.Metadata.Genres ??= new List<Genre>();
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) =>
{
series.Metadata.Tags.Add(tag);
}, () => series.Metadata.TagsLocked = true);
void HandleAddPerson(Person person)
{
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person);
allPeople.Add(person);
}
series.Metadata.People ??= new List<Person>();
UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata.Writers, series, allPeople,
HandleAddPerson, () => series.Metadata.WriterLocked = true);
UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allPeople,
HandleAddPerson, () => series.Metadata.CharacterLocked = true);
UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allPeople,
HandleAddPerson, () => series.Metadata.ColoristLocked = true);
UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allPeople,
HandleAddPerson, () => series.Metadata.EditorLocked = true);
UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allPeople,
HandleAddPerson, () => series.Metadata.InkerLocked = true);
UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allPeople,
HandleAddPerson, () => series.Metadata.LettererLocked = true);
UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPeople,
HandleAddPerson, () => series.Metadata.PencillerLocked = true);
UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPeople,
HandleAddPerson, () => series.Metadata.PublisherLocked = true);
UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allPeople,
HandleAddPerson, () => series.Metadata.TranslatorLocked = true);
UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allPeople,
HandleAddPerson, () => series.Metadata.CoverArtistLocked = true);
if (!updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked) series.Metadata.AgeRatingLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked) series.Metadata.PublicationStatusLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.LanguageLocked) series.Metadata.LanguageLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.GenresLocked) series.Metadata.GenresLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.TagsLocked) series.Metadata.TagsLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.CharacterLocked) series.Metadata.CharacterLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.ColoristLocked) series.Metadata.ColoristLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.EditorLocked) series.Metadata.EditorLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.InkerLocked) series.Metadata.InkerLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.LettererLocked) series.Metadata.LettererLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PencillerLocked) series.Metadata.PencillerLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.PublisherLocked) series.Metadata.PublisherLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.TranslatorLocked) series.Metadata.TranslatorLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.CoverArtistLocked) series.Metadata.CoverArtistLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.WriterLocked) series.Metadata.WriterLocked = false;
if (!updateSeriesMetadataDto.SeriesMetadata.SummaryLocked) series.Metadata.SummaryLocked = false;
if (!_unitOfWork.HasChanges()) if (!_unitOfWork.HasChanges())
{ {
return true; return true;
@ -184,6 +180,7 @@ public class SeriesService : ISeriesService
private static void UpdateRelatedList(ICollection<CollectionTagDto> tags, Series series, IReadOnlyCollection<CollectionTag> allTags, private static void UpdateRelatedList(ICollection<CollectionTagDto> tags, Series series, IReadOnlyCollection<CollectionTag> allTags,
Action<CollectionTag> handleAdd) Action<CollectionTag> handleAdd)
{ {
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 // 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(); var existingTags = series.Metadata.CollectionTags.ToList();
foreach (var existing in existingTags) foreach (var existing in existingTags)
@ -216,11 +213,13 @@ public class SeriesService : ISeriesService
private static void UpdateGenreList(ICollection<GenreTagDto> tags, Series series, IReadOnlyCollection<Genre> allTags, Action<Genre> handleAdd, Action onModified) private static void UpdateGenreList(ICollection<GenreTagDto> tags, Series series, IReadOnlyCollection<Genre> allTags, Action<Genre> handleAdd, Action onModified)
{ {
if (tags == null) return;
var isModified = false; 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 // 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(); var existingTags = series.Metadata.Genres.ToList();
foreach (var existing in existingTags) 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) if (tags.SingleOrDefault(t => t.Id == existing.Id) == null)
{ {
// Remove tag // Remove tag
@ -232,10 +231,12 @@ public class SeriesService : ISeriesService
// At this point, all tags that aren't in dto have been removed. // At this point, all tags that aren't in dto have been removed.
foreach (var tagTitle in tags.Select(t => t.Title)) foreach (var tagTitle in tags.Select(t => t.Title))
{ {
var existingTag = allTags.SingleOrDefault(t => t.Title == tagTitle); // This should be normalized name
var normalizedTitle = Parser.Parser.Normalize(tagTitle);
var existingTag = allTags.SingleOrDefault(t => t.NormalizedTitle == normalizedTitle);
if (existingTag != null) if (existingTag != null)
{ {
if (series.Metadata.Genres.All(t => t.Title != tagTitle)) if (series.Metadata.Genres.All(t => t.NormalizedTitle != normalizedTitle))
{ {
handleAdd(existingTag); handleAdd(existingTag);
isModified = true; isModified = true;
@ -257,6 +258,8 @@ public class SeriesService : ISeriesService
private static void UpdateTagList(ICollection<TagDto> tags, Series series, IReadOnlyCollection<Tag> allTags, Action<Tag> handleAdd, Action 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; 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 // 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(); var existingTags = series.Metadata.Tags.ToList();
@ -300,6 +303,7 @@ public class SeriesService : ISeriesService
private static void UpdatePeopleList(PersonRole role, ICollection<PersonDto> tags, Series series, IReadOnlyCollection<Person> allTags, private static void UpdatePeopleList(PersonRole role, ICollection<PersonDto> tags, Series series, IReadOnlyCollection<Person> allTags,
Action<Person> handleAdd, Action onModified) Action<Person> handleAdd, Action onModified)
{ {
if (tags == null) return;
var isModified = false; 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 // 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(); var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList();

View File

@ -523,7 +523,14 @@ public class ScannerService : IScannerService
series.Format = parsedInfos[0].Format; series.Format = parsedInfos[0].Format;
} }
series.OriginalName ??= parsedInfos[0].Series; series.OriginalName ??= parsedInfos[0].Series;
if (!series.SortNameLocked) series.SortName = parsedInfos[0].SeriesSort; if (!series.SortNameLocked)
{
if (!string.IsNullOrEmpty(parsedInfos[0].SeriesSort))
{
series.SortName = parsedInfos[0].SeriesSort;
}
series.SortName = series.Name;
}
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name));

View File

@ -54,18 +54,16 @@ public class VersionUpdaterService : IVersionUpdaterService
{ {
private readonly ILogger<VersionUpdaterService> _logger; private readonly ILogger<VersionUpdaterService> _logger;
private readonly IEventHub _eventHub; private readonly IEventHub _eventHub;
private readonly IPresenceTracker _tracker;
private readonly Markdown _markdown = new MarkdownDeep.Markdown(); private readonly Markdown _markdown = new MarkdownDeep.Markdown();
#pragma warning disable S1075 #pragma warning disable S1075
private const string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest"; private const string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
private const string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases"; private const string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
#pragma warning restore S1075 #pragma warning restore S1075
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IEventHub eventHub, IPresenceTracker tracker) public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IEventHub eventHub)
{ {
_logger = logger; _logger = logger;
_eventHub = eventHub; _eventHub = eventHub;
_tracker = tracker;
FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli => FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());

View File

@ -36,9 +36,10 @@ public class EventHub : IEventHub
if (onlyAdmins) if (onlyAdmins)
{ {
var admins = await _presenceTracker.GetOnlineAdmins(); var admins = await _presenceTracker.GetOnlineAdmins();
_messageHub.Clients.Users(admins); users = _messageHub.Clients.Users(admins);
} }
await users.SendAsync(method, message); await users.SendAsync(method, message);
} }
} }

View File

@ -311,7 +311,6 @@ namespace API.SignalR
{ {
Name = CoverUpdate, Name = CoverUpdate,
Title = "Updating Cover", Title = "Updating Cover",
//SubTitle = series.Name, // TODO: Refactor this
Progress = ProgressType.None, Progress = ProgressType.None,
Body = new Body = new
{ {

View File

@ -34,6 +34,6 @@ namespace API.SignalR
/// <summary> /// <summary>
/// When event took place /// When event took place
/// </summary> /// </summary>
public DateTime EventTime = DateTime.Now; public readonly DateTime EventTime = DateTime.Now;
} }
} }

View File

@ -12,8 +12,8 @@
<h4> <h4>
<span id="member-name--{{idx}}">{{invite.username | titlecase}} </span> <span id="member-name--{{idx}}">{{invite.username | titlecase}} </span>
<div class="float-end"> <div class="float-end">
<button class="btn btn-danger me-2" (click)="deleteUser(invite)">Cancel</button> <button class="btn btn-danger btn-sm me-2" (click)="deleteUser(invite)">Cancel</button>
<button class="btn btn-secondary me-2" (click)="resendEmail(invite)">Resend</button> <button class="btn btn-secondary btn-sm" (click)="resendEmail(invite)">Resend</button>
</div> </div>
</h4> </h4>