diff --git a/API.Tests/Parser/DefaultParserTests.cs b/API.Tests/Parser/DefaultParserTests.cs index a9c593de9..200a6b16a 100644 --- a/API.Tests/Parser/DefaultParserTests.cs +++ b/API.Tests/Parser/DefaultParserTests.cs @@ -44,7 +44,7 @@ public class DefaultParserTests [Theory] [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!~1~1")] [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!~1~2")] - [InlineData("/manga/Monster #8/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster #8~0~1")] + [InlineData("/manga/Monster/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster~0~1")] public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string expectedParseInfo) { const string rootDirectory = "/manga/"; @@ -56,6 +56,27 @@ public class DefaultParserTests Assert.Equal(tokens[2], actual.Chapters); } + [Theory] + [InlineData("/manga/Btooom!/Vol.1/Chapter 1/1.cbz", "Btooom!")] + [InlineData("/manga/Btooom!/Vol.1 Chapter 2/1.cbz", "Btooom!")] + [InlineData("/manga/Monster #8 (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] + [InlineData("/manga/Monster (Digital)/Ch. 001-016 [MangaPlus] [Digital] [amit34521]/Monster Ch. 001 [MangaPlus] [Digital] [amit34521]/13.jpg", "Monster")] + [InlineData("/manga/Foo 50/Specials/Foo 50 SP01.cbz", "Foo 50")] + [InlineData("/manga/Foo 50 (kiraa)/Specials/Foo 50 SP01.cbz", "Foo 50")] + [InlineData("/manga/Btooom!/Specials/Just a special SP01.cbz", "Btooom!")] + public void ParseFromFallbackFolders_ShouldUseExistingSeriesName(string inputFile, string expectedParseInfo) + { + const string rootDirectory = "/manga/"; + var fs = new MockFileSystem(); + fs.AddDirectory(rootDirectory); + fs.AddFile(inputFile, new MockFileData("")); + var ds = new DirectoryService(Substitute.For>(), fs); + var parser = new DefaultParser(ds); + var actual = parser.Parse(inputFile, rootDirectory); + _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); + Assert.Equal(expectedParseInfo, actual.Series); + } + #endregion @@ -243,6 +264,80 @@ public class DefaultParserTests } } + [Fact] + public void Parse_ParseInfo_Manga_WithSpecialsFolder() + { + const string rootPath = @"E:/Manga/"; + var filesystem = new MockFileSystem(); + filesystem.AddDirectory("E:/Manga"); + filesystem.AddDirectory("E:/Foo 50"); + filesystem.AddDirectory("E:/Foo 50/Specials"); + filesystem.AddFile(@"E:/Manga/Foo 50/Foo 50 v1.cbz", new MockFileData("")); + filesystem.AddFile(@"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz", new MockFileData("")); + + var ds = new DirectoryService(Substitute.For>(), filesystem); + var parser = new DefaultParser(ds); + + var filepath = @"E:/Manga/Foo 50/Foo 50 v1.cbz"; + // There is a bad parse for series like "Foo 50", so we have parsed chapter as 50 + var expected = new ParserInfo + { + Series = "Foo 50", Volumes = "1", + Chapters = "50", Filename = "Foo 50 v1.cbz", Format = MangaFormat.Archive, + FullFilePath = filepath + }; + + var actual = parser.Parse(filepath, rootPath); + + Assert.NotNull(actual); + _testOutputHelper.WriteLine($"Validating {filepath}"); + Assert.Equal(expected.Format, actual.Format); + _testOutputHelper.WriteLine("Format ✓"); + Assert.Equal(expected.Series, actual.Series); + _testOutputHelper.WriteLine("Series ✓"); + Assert.Equal(expected.Chapters, actual.Chapters); + _testOutputHelper.WriteLine("Chapters ✓"); + Assert.Equal(expected.Volumes, actual.Volumes); + _testOutputHelper.WriteLine("Volumes ✓"); + Assert.Equal(expected.Edition, actual.Edition); + _testOutputHelper.WriteLine("Edition ✓"); + Assert.Equal(expected.Filename, actual.Filename); + _testOutputHelper.WriteLine("Filename ✓"); + Assert.Equal(expected.FullFilePath, actual.FullFilePath); + _testOutputHelper.WriteLine("FullFilePath ✓"); + Assert.Equal(expected.IsSpecial, actual.IsSpecial); + _testOutputHelper.WriteLine("IsSpecial ✓"); + + filepath = @"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz"; + expected = new ParserInfo + { + Series = "Foo 50", Volumes = "0", IsSpecial = true, + Chapters = "50", Filename = "Foo 50 SP01.cbz", Format = MangaFormat.Archive, + FullFilePath = filepath + }; + + actual = parser.Parse(filepath, rootPath); + Assert.NotNull(actual); + _testOutputHelper.WriteLine($"Validating {filepath}"); + Assert.Equal(expected.Format, actual.Format); + _testOutputHelper.WriteLine("Format ✓"); + Assert.Equal(expected.Series, actual.Series); + _testOutputHelper.WriteLine("Series ✓"); + Assert.Equal(expected.Chapters, actual.Chapters); + _testOutputHelper.WriteLine("Chapters ✓"); + Assert.Equal(expected.Volumes, actual.Volumes); + _testOutputHelper.WriteLine("Volumes ✓"); + Assert.Equal(expected.Edition, actual.Edition); + _testOutputHelper.WriteLine("Edition ✓"); + Assert.Equal(expected.Filename, actual.Filename); + _testOutputHelper.WriteLine("Filename ✓"); + Assert.Equal(expected.FullFilePath, actual.FullFilePath); + _testOutputHelper.WriteLine("FullFilePath ✓"); + Assert.Equal(expected.IsSpecial, actual.IsSpecial); + _testOutputHelper.WriteLine("IsSpecial ✓"); + + } + [Fact] public void Parse_ParseInfo_Comic() { diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index e57f56928..ff1b4dc71 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -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) { diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index c0d49820b..1ec21efe8 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -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>(), 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 diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 42b586e1f..1b9f5fd3f 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -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() {new GenreTagDto() {Id = 0, Title = "New Genre"}} + }, + CollectionTags = new List() + }); + + 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() {new GenreTagDto() {Id = 0, Title = "New Genre"}}, + Tags = new List() {new TagDto() {Id = 0, Title = "New Tag"}}, + Characters = new List() {new PersonDto() {Id = 0, Name = "Joe Shmo", Role = PersonRole.Character}}, + Colorists = new List() {new PersonDto() {Id = 0, Name = "Joe Shmo", Role = PersonRole.Colorist}}, + Pencillers = new List() {new PersonDto() {Id = 0, Name = "Joe Shmo 2", Role = PersonRole.Penciller}}, + }, + CollectionTags = new List() + { + 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()) + }; + var g = DbFactory.Genre("Existing Genre", false); + s.Metadata.Genres = new List() {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() {new () {Id = 0, Title = "New Genre"}}, + }, + CollectionTags = new List() + }); + + 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()) + }; + var g = DbFactory.Genre("Existing Genre", false); + s.Metadata.Genres = new List() {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() {new () {Id = 1, Title = "Existing Genre"}}, + GenresLocked = true + }, + CollectionTags = new List() + }); + + 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 } diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 986f69d06..dfa30b18d 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -423,7 +423,6 @@ namespace API.Controllers var emailLink = GenerateEmailLink(token, "confirm-email", dto.Email); _logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink); - var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString(); var accessible = await _emailService.CheckIfAccessible(host); if (accessible) diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index 169c34bd9..b60fae6e8 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -26,20 +26,18 @@ namespace API.Controllers private readonly IDirectoryService _directoryService; private readonly IDownloadService _downloadService; private readonly IEventHub _eventHub; - private readonly UserManager _userManager; private readonly ILogger _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 userManager, ILogger logger, IBookmarkService bookmarkService) + IDownloadService downloadService, IEventHub eventHub, ILogger logger, IBookmarkService bookmarkService) { _unitOfWork = unitOfWork; _archiveService = archiveService; _directoryService = directoryService; _downloadService = downloadService; _eventHub = eventHub; - _userManager = userManager; _logger = logger; _bookmarkService = bookmarkService; } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 91607db7d..eaa778121 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -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; } diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 28751dbc1..3aad34d99 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -145,13 +145,20 @@ namespace API.Controllers if (series == null) return BadRequest("Series does not exist"); - if (series.Name != updateSeries.Name && await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name, series.Format)) + var seriesExists = + await _unitOfWork.SeriesRepository.DoesSeriesNameExistInLibrary(updateSeries.Name.Trim(), series.LibraryId, + series.Format); + if (series.Name != updateSeries.Name && seriesExists) { return BadRequest("A series already exists in this library with this name. Series Names must be unique to a library."); } series.Name = updateSeries.Name.Trim(); - series.SortName = updateSeries.SortName.Trim(); + if (!string.IsNullOrEmpty(updateSeries.SortName.Trim())) + { + series.SortName = updateSeries.SortName.Trim(); + } + series.LocalizedName = updateSeries.LocalizedName.Trim(); series.NameLocked = updateSeries.NameLocked; diff --git a/API/DTOs/SeriesMetadataDto.cs b/API/DTOs/SeriesMetadataDto.cs index 23e8f4e52..d5ad24610 100644 --- a/API/DTOs/SeriesMetadataDto.cs +++ b/API/DTOs/SeriesMetadataDto.cs @@ -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; /// /// Collections the Series belongs to /// diff --git a/API/Data/DbFactory.cs b/API/Data/DbFactory.cs index 81750fb72..46ebfbf10 100644 --- a/API/Data/DbFactory.cs +++ b/API/Data/DbFactory.cs @@ -18,7 +18,7 @@ namespace API.Data { public static Series Series(string name) { - return new () + return new Series { Name = name, OriginalName = name, diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index ef97dfc87..bf634e5c7 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -47,7 +47,7 @@ public interface ISeriesRepository void Update(Series series); void Remove(Series series); void Remove(IEnumerable series); - Task DoesSeriesNameExistInLibrary(string name, MangaFormat format); + Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format); /// /// Adds user information like progress, ratings, etc /// @@ -135,17 +135,12 @@ public class SeriesRepository : ISeriesRepository /// Name of series /// Format of series /// - public async Task DoesSeriesNameExistInLibrary(string name, MangaFormat format) + public async Task DoesSeriesNameExistInLibrary(string name, int libraryId, MangaFormat format) { - var libraries = _context.Series - .AsNoTracking() - .Where(x => x.Name.Equals(name) && x.Format == format) - .Select(s => s.LibraryId); - return await _context.Series .AsNoTracking() - .Where(s => libraries.Contains(s.LibraryId) && s.Name.Equals(name) && s.Format == format) - .CountAsync() > 1; + .Where(s => s.LibraryId == libraryId && s.Name.Equals(name) && s.Format == format) + .AnyAsync(); } @@ -378,7 +373,7 @@ public class SeriesRepository : ISeriesRepository /// - /// Returns Volumes, Metadata, and Collection Tags + /// Returns Volumes, Metadata (Incl Genres and People), and Collection Tags /// /// /// @@ -624,13 +619,13 @@ public class SeriesRepository : ISeriesRepository LastReadingProgress = _context.AppUserProgresses .Where(p => p.Id == progress.Id && p.AppUserId == userId) .Max(p => p.LastModified), - // BUG: This is only taking into account chapters that have progress on them, not all chapters in said series - LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created), - //LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created) + // This is only taking into account chapters that have progress on them, not all chapters in said series + //LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created), + LastChapterCreated = s.Volumes.SelectMany(v => v.Chapters).Max(c => c.Created) }); if (cutoffOnDate) { - query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint); + query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint || d.LastChapterCreated >= cutoffProgressPoint); } // I think I need another Join statement. The problem is the chapters are still limited to progress diff --git a/API/Entities/Metadata/SeriesMetadata.cs b/API/Entities/Metadata/SeriesMetadata.cs index 0ec7038fa..522f15d06 100644 --- a/API/Entities/Metadata/SeriesMetadata.cs +++ b/API/Entities/Metadata/SeriesMetadata.cs @@ -11,7 +11,7 @@ namespace API.Entities.Metadata { public int Id { get; set; } - public string Summary { get; set; } + public string Summary { get; set; } = string.Empty; public ICollection CollectionTags { get; set; } diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 395ffe62b..a4f51c67d 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -44,11 +44,6 @@ namespace API.Extensions services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Parser/DefaultParser.cs b/API/Parser/DefaultParser.cs index 23b5c1d58..d03bff199 100644 --- a/API/Parser/DefaultParser.cs +++ b/API/Parser/DefaultParser.cs @@ -142,18 +142,22 @@ public class DefaultParser } } - var series = Parser.ParseSeries(folder); - - if ((string.IsNullOrEmpty(series) && i == fallbackFolders.Count - 1)) + // 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) { - ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic); - break; - } + var series = Parser.ParseSeries(folder); - if (!string.IsNullOrEmpty(series)) - { - ret.Series = series; - break; + if (string.IsNullOrEmpty(series)) + { + ret.Series = Parser.CleanTitle(folder, type is LibraryType.Comic); + break; + } + + if (!string.IsNullOrEmpty(series) && (string.IsNullOrEmpty(ret.Series) || !folder.Contains(ret.Series))) + { + ret.Series = series; + break; + } } } } diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index cc237eae7..63db72658 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -60,6 +60,12 @@ namespace API.Parser private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+]", MatchOptions, RegexTimeout); + /// + /// Recognizes the Special token only + /// + private static readonly Regex SpecialTokenRegex = new Regex(@"SP\d+", + MatchOptions, RegexTimeout); + private static readonly Regex[] MangaVolumeRegex = new[] { @@ -976,9 +982,8 @@ namespace API.Parser /// 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) { diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index fb7675735..227aa9c29 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -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 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; } + /// + /// 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 + /// + /// + /// + /// + /// + 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); + } + /// /// Lists all directories in a root path. Will exclude Hidden or System directories. /// diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 3146ce6dc..25f736e99 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -52,103 +52,110 @@ 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(); - UpdateRelatedList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) => - { - series.Metadata.CollectionTags.Add(tag); - }); - - series.Metadata.Genres ??= new List(); - UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata.Genres, series, allGenres, (genre) => - { - series.Metadata.Genres.Add(genre); - }, () => series.Metadata.GenresLocked = true); - - series.Metadata.Tags ??= new List(); - 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(); - 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; } + // This shouldn't be needed post v0.5.3 release + if (string.IsNullOrEmpty(series.Metadata.Summary)) + { + series.Metadata.Summary = string.Empty; + } + + if (string.IsNullOrEmpty(updateSeriesMetadataDto.SeriesMetadata.Summary)) + { + updateSeriesMetadataDto.SeriesMetadata.Summary = string.Empty; + } + + 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(); + UpdateRelatedList(updateSeriesMetadataDto.CollectionTags, series, allCollectionTags, (tag) => + { + series.Metadata.CollectionTags.Add(tag); + }); + + series.Metadata.Genres ??= new List(); + UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata.Genres, series, allGenres, (genre) => + { + series.Metadata.Genres.Add(genre); + }, () => series.Metadata.GenresLocked = true); + + series.Metadata.Tags ??= new List(); + 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(); + 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 +191,7 @@ public class SeriesService : ISeriesService private static void UpdateRelatedList(ICollection tags, Series series, IReadOnlyCollection allTags, Action 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 +224,13 @@ public class SeriesService : ISeriesService private static void UpdateGenreList(ICollection tags, Series series, IReadOnlyCollection allTags, Action 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 +242,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 +269,8 @@ public class SeriesService : ISeriesService private static void UpdateTagList(ICollection tags, Series series, IReadOnlyCollection allTags, Action 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 +314,7 @@ public class SeriesService : ISeriesService private static void UpdatePeopleList(PersonRole role, ICollection tags, Series series, IReadOnlyCollection allTags, Action 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(); diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 08c13e338..85f01aa09 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -523,7 +523,18 @@ public class ScannerService : IScannerService series.Format = parsedInfos[0].Format; } series.OriginalName ??= parsedInfos[0].Series; - if (!series.SortNameLocked) series.SortName = parsedInfos[0].SeriesSort; + if (string.IsNullOrEmpty(series.SortName)) + { + series.SortName = series.Name; + } + if (!series.SortNameLocked) + { + series.SortName = series.Name; + if (!string.IsNullOrEmpty(parsedInfos[0].SeriesSort)) + { + series.SortName = parsedInfos[0].SeriesSort; + } + } await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.LibraryScanProgressEvent(library.Name, ProgressEventType.Ended, series.Name)); diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index d72f487b4..8d79b3a45 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -54,18 +54,16 @@ public class VersionUpdaterService : IVersionUpdaterService { private readonly ILogger _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 logger, IEventHub eventHub, IPresenceTracker tracker) + public VersionUpdaterService(ILogger logger, IEventHub eventHub) { _logger = logger; _eventHub = eventHub; - _tracker = tracker; FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli => cli.Settings.HttpClientFactory = new UntrustedCertClientFactory()); @@ -94,12 +92,7 @@ public class VersionUpdaterService : IVersionUpdaterService { if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null; var updateVersion = new Version(update.Tag_Name.Replace("v", string.Empty)); - var currentVersion = BuildInfo.Version.ToString(); - - if (updateVersion.Revision == -1) - { - currentVersion = currentVersion.Substring(0, currentVersion.LastIndexOf(".", StringComparison.Ordinal)); - } + var currentVersion = BuildInfo.Version.ToString(4); return new UpdateNotificationDto() { diff --git a/API/SignalR/EventHub.cs b/API/SignalR/EventHub.cs index fa92d9ec7..50ba20ccf 100644 --- a/API/SignalR/EventHub.cs +++ b/API/SignalR/EventHub.cs @@ -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); } } diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 485a2fc37..53833e3f3 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -311,7 +311,6 @@ namespace API.SignalR { Name = CoverUpdate, Title = "Updating Cover", - //SubTitle = series.Name, // TODO: Refactor this Progress = ProgressType.None, Body = new { diff --git a/API/SignalR/SignalRMessage.cs b/API/SignalR/SignalRMessage.cs index b71d0b813..d9564d027 100644 --- a/API/SignalR/SignalRMessage.cs +++ b/API/SignalR/SignalRMessage.cs @@ -34,6 +34,6 @@ namespace API.SignalR /// /// When event took place /// - public DateTime EventTime = DateTime.Now; + public readonly DateTime EventTime = DateTime.Now; } } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 9e72ce3ed..f16bbc695 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -4,7 +4,7 @@ net6.0 kavitareader.com Kavita - 0.5.2.0 + 0.5.2.3 en @@ -19,4 +19,4 @@ - + \ No newline at end of file diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 279f64bf3..c0b0ce2cd 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -2418,22 +2418,6 @@ "integrity": "sha512-wooUZiV92QyoeFxkhqIwH/cfiAAAn+l8fEEuaaEIfJtpjpbShvvlboEVsqb28soeGiFJfLcmsZM3mUFgsG4QBQ==", "dev": true }, - "@ngx-lite/nav-drawer": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/@ngx-lite/nav-drawer/-/nav-drawer-0.4.7.tgz", - "integrity": "sha512-OqXJhzE88RR5Vtgr0tcuvkKVkzsKZjeXxhjpWOJ9UiC2iCQPDL2rtvag5K/vPKN362Jx0htvr3cmFDjHg/kjdA==", - "requires": { - "tslib": "^2.0.0" - } - }, - "@ngx-lite/util": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@ngx-lite/util/-/util-0.0.1.tgz", - "integrity": "sha512-j7pBcF+5OEHExEUBNdlQT5x4sVvHIPwZeMvhlO1TAcAAz9frDsvYgJ1c3eXJYJKJq57o1rH1RESKSJ9YRNpAiw==", - "requires": { - "tslib": "^2.1.0" - } - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index 2dbba8f3f..235cbd85f 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -29,8 +29,6 @@ "@fortawesome/fontawesome-free": "^6.0.0", "@microsoft/signalr": "^6.0.2", "@ng-bootstrap/ng-bootstrap": "^12.0.0", - "@ngx-lite/nav-drawer": "^0.4.7", - "@ngx-lite/util": "0.0.1", "@popperjs/core": "^2.11.2", "@types/file-saver": "^2.0.5", "bootstrap": "^5.1.2", diff --git a/UI/Web/src/app/_interceptors/error.interceptor.ts b/UI/Web/src/app/_interceptors/error.interceptor.ts index a0b792045..8c3cdca61 100644 --- a/UI/Web/src/app/_interceptors/error.interceptor.ts +++ b/UI/Web/src/app/_interceptors/error.interceptor.ts @@ -116,6 +116,11 @@ export class ErrorInterceptor implements HttpInterceptor { } private handleAuthError(error: any) { + + // Special hack for register url, to not care about auth + if (location.href.includes('/registration/confirm-email?token=')) { + return; + } // NOTE: Signin has error.error or error.statusText available. // if statement is due to http/2 spec issue: https://github.com/angular/angular/issues/23334 this.accountService.logout(); diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.html b/UI/Web/src/app/admin/manage-users/manage-users.component.html index c4bbfe553..3fdc66a26 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.html +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.html @@ -12,8 +12,8 @@

{{invite.username | titlecase}}
- - + +

diff --git a/UI/Web/src/app/announcements/changelog/changelog.component.html b/UI/Web/src/app/announcements/changelog/changelog.component.html index caffe5d8c..0a8b8a1b2 100644 --- a/UI/Web/src/app/announcements/changelog/changelog.component.html +++ b/UI/Web/src/app/announcements/changelog/changelog.component.html @@ -1,19 +1,20 @@
+

If you do not see an Installed tag, you are on a nightly release. Only major versions will show as available.

{{update.updateTitle}}  - Installed - Available + Installed + Available

-
Published: {{update.publishDate | date: 'short'}}
+
Published: {{update.publishDate | date: 'short'}}
             
           
- Installed - Download + Installed + Download
diff --git a/UI/Web/src/app/announcements/changelog/changelog.component.ts b/UI/Web/src/app/announcements/changelog/changelog.component.ts index 86724e92e..1df65496c 100644 --- a/UI/Web/src/app/announcements/changelog/changelog.component.ts +++ b/UI/Web/src/app/announcements/changelog/changelog.component.ts @@ -11,23 +11,14 @@ export class ChangelogComponent implements OnInit { updates: Array = []; isLoading: boolean = true; - installedVersion: string = ''; constructor(private serverService: ServerService) { } ngOnInit(): void { - this.serverService.getServerInfo().subscribe(info => { - this.installedVersion = info.kavitaVersion; - this.serverService.getChangelog().subscribe(updates => { - this.updates = updates; - this.isLoading = false; - - if (this.updates.filter(u => u.updateVersion === this.installedVersion).length === 0) { - // User is on a nightly version. Tell them the last stable is installed - this.installedVersion = this.updates[0].updateVersion; - } - }); + this.serverService.getChangelog().subscribe(updates => { + this.updates = updates; + this.isLoading = false; }); diff --git a/UI/Web/src/app/nav-header/nav-header.component.html b/UI/Web/src/app/nav-header/nav-header.component.html index c4149cd25..d61e6bc60 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav-header/nav-header.component.html @@ -1,7 +1,7 @@