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("@Recently-Snapshot/Love Hina/", true)]
[InlineData("@recycle/Love Hina/", true)]
[InlineData("@recycle/Love Hina/", true)]
[InlineData("E:/Test/__MACOSX/Love Hina/", true)]
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());
}
[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
#region ListDirectory

View File

@ -6,8 +6,11 @@ using System.Threading.Tasks;
using API.Data;
using API.Data.Repositories;
using API.DTOs;
using API.DTOs.CollectionTags;
using API.DTOs.Metadata;
using API.Entities;
using API.Entities.Enums;
using API.Extensions;
using API.Helpers;
using API.Services;
using API.SignalR;
@ -99,6 +102,9 @@ public class SeriesServiceTests
{
_context.Series.RemoveRange(_context.Series.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();
}
@ -569,4 +575,174 @@ public class SeriesServiceTests
}
#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 IDownloadService _downloadService;
private readonly IEventHub _eventHub;
private readonly UserManager<AppUser> _userManager;
private readonly ILogger<DownloadController> _logger;
private readonly IBookmarkService _bookmarkService;
private const string DefaultContentType = "application/octet-stream";
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;
_archiveService = archiveService;
_directoryService = directoryService;
_downloadService = downloadService;
_eventHub = eventHub;
_userManager = userManager;
_logger = logger;
_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.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,
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;
}

View File

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

View File

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

View File

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

View File

@ -60,6 +60,12 @@ namespace API.Parser
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]",
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[]
{
@ -976,9 +982,8 @@ namespace API.Parser
/// <returns></returns>
public static string CleanSpecialTitle(string name)
{
// TODO: Optimize this code & Test
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('.');
if (lastIndex > 0)
{

View File

@ -71,6 +71,8 @@ namespace API.Services
private static readonly Regex ExcludeDirectories = new Regex(
@"@eaDir|\.DS_Store|\.qpkg",
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 DirectoryService(ILogger<DirectoryService> logger, IFileSystem fileSystem)
@ -370,24 +372,11 @@ namespace API.Services
foreach (var file in filePaths)
{
currentFile = file;
var fileInfo = FileSystem.FileInfo.FromFileName(file);
if (fileInfo.Exists)
{
// TODO: I need to handle if file already exists and allow either an overwrite or prepend (2) to it
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);
}
var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(file, directoryPath, prepend));
fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name));
}
}
catch (Exception ex)
@ -399,6 +388,42 @@ namespace API.Services
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>
/// Lists all directories in a root path. Will exclude Hidden or System directories.
/// </summary>

View File

@ -52,103 +52,99 @@ public class SeriesService : ISeriesService
var allPeople = (await _unitOfWork.PersonRepository.GetAllPeople()).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
.Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList());
series.Metadata.AgeRating = updateSeriesMetadataDto.SeriesMetadata.AgeRating;
series.Metadata.AgeRatingLocked = true;
}
else
if (series.Metadata.PublicationStatus != updateSeriesMetadataDto.SeriesMetadata.PublicationStatus)
{
if (series.Metadata.AgeRating != updateSeriesMetadataDto.SeriesMetadata.AgeRating)
{
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;
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 (!_unitOfWork.HasChanges())
{
return true;
@ -184,6 +180,7 @@ public class SeriesService : ISeriesService
private static void UpdateRelatedList(ICollection<CollectionTagDto> tags, Series series, IReadOnlyCollection<CollectionTag> allTags,
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
var existingTags = series.Metadata.CollectionTags.ToList();
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)
{
if (tags == null) return;
var isModified = false;
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
var existingTags = series.Metadata.Genres.ToList();
foreach (var existing in existingTags)
{
// NOTE: Why don't I use a NormalizedName here (outside of memory pressure from string creation)?
if (tags.SingleOrDefault(t => t.Id == existing.Id) == null)
{
// Remove tag
@ -232,10 +231,12 @@ public class SeriesService : ISeriesService
// At this point, all tags that aren't in dto have been removed.
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 (series.Metadata.Genres.All(t => t.Title != tagTitle))
if (series.Metadata.Genres.All(t => t.NormalizedTitle != normalizedTitle))
{
handleAdd(existingTag);
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)
{
if (tags == null) return;
var isModified = false;
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
var existingTags = series.Metadata.Tags.ToList();
@ -300,6 +303,7 @@ public class SeriesService : ISeriesService
private static void UpdatePeopleList(PersonRole role, ICollection<PersonDto> tags, Series series, IReadOnlyCollection<Person> allTags,
Action<Person> handleAdd, Action onModified)
{
if (tags == null) return;
var isModified = false;
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
var existingTags = series.Metadata.People.Where(p => p.Role == role).ToList();

View File

@ -523,7 +523,14 @@ public class ScannerService : IScannerService
series.Format = parsedInfos[0].Format;
}
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));

View File

@ -54,18 +54,16 @@ public class VersionUpdaterService : IVersionUpdaterService
{
private readonly ILogger<VersionUpdaterService> _logger;
private readonly IEventHub _eventHub;
private readonly IPresenceTracker _tracker;
private readonly Markdown _markdown = new MarkdownDeep.Markdown();
#pragma warning disable S1075
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";
#pragma warning restore S1075
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IEventHub eventHub, IPresenceTracker tracker)
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IEventHub eventHub)
{
_logger = logger;
_eventHub = eventHub;
_tracker = tracker;
FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());

View File

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

View File

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

View File

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

View File

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