mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Misc Polish and Fixes (#1542)
* Moved LibraryWatcher to utilize a queue for calculating the change event to ensure the Watcher doesn't get overwhelmed on large moves. * Fixed a security vulnerability (https://huntr.dev/bounties/8a3e652f-d6bf-436e-877e-0eaf5c69ef95/). This will be disclosed in Stable release changelog. * Tweaked the log message template * Removed some dead code from Configuration json patcher * Fixed a bug with the ComicInfo finding to properly handle root level. Fixed a bug where sometimes scanner wouldn't choose the first file with ComicInfo for filling out information. * Added new setting for managing how many logs files are allowed, just like how backups work. * Added unit tests for new CleanupLogs code * Fixed a bug where manga reader background color wasn't actually sending from the UI * Added new stats for tracking to help understand usage in the app and what features are used or not. * Fixed Stats url * Fixed a bug where volumes that had larger than 1 difference wouldn't properly return next/prev chapter (for continuous reader) * Remove a redundant test step in build pipeline, since it's already done at PR stage. * Updated dockerfile to use the new Heath check endpoint * Allow force to pass through to scan loop * Removed some old config stuff from a safety check on config in entrypoint.sh * Fixed broken unit tests due to new RBS check and how we setup mock data.
This commit is contained in:
parent
c58c7deaf9
commit
e89a06865c
4
.github/workflows/sonar-scan.yml
vendored
4
.github/workflows/sonar-scan.yml
vendored
@ -123,7 +123,7 @@ jobs:
|
||||
|
||||
develop:
|
||||
name: Build Nightly Docker if Develop push
|
||||
needs: [ build, test, version ]
|
||||
needs: [ build, version ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
steps:
|
||||
@ -232,7 +232,7 @@ jobs:
|
||||
|
||||
stable:
|
||||
name: Build Stable Docker if Main push
|
||||
needs: [ build, test ]
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
steps:
|
||||
|
@ -38,6 +38,7 @@ public class CleanupServiceTests
|
||||
private const string CacheDirectory = "C:/kavita/config/cache/";
|
||||
private const string CoverImageDirectory = "C:/kavita/config/covers/";
|
||||
private const string BackupDirectory = "C:/kavita/config/backups/";
|
||||
private const string LogDirectory = "C:/kavita/config/logs/";
|
||||
private const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
||||
|
||||
|
||||
@ -84,6 +85,9 @@ public class CleanupServiceTests
|
||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync();
|
||||
setting.Value = BookmarkDirectory;
|
||||
|
||||
setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync();
|
||||
setting.Value = "10";
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
_context.Library.Add(new Library()
|
||||
@ -412,6 +416,59 @@ public class CleanupServiceTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region CleanupLogs
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupLogs_LeaveOneFile_SinceAllAreExpired()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
foreach (var i in Enumerable.Range(1, 10))
|
||||
{
|
||||
var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}");
|
||||
filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("")
|
||||
{
|
||||
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31))
|
||||
});
|
||||
}
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
await cleanupService.CleanupLogs();
|
||||
Assert.Single(ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupLogs_LeaveLestExpired()
|
||||
{
|
||||
var filesystem = CreateFileSystem();
|
||||
foreach (var i in Enumerable.Range(1, 9))
|
||||
{
|
||||
var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}");
|
||||
filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("")
|
||||
{
|
||||
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - i))
|
||||
});
|
||||
}
|
||||
filesystem.AddFile($"{LogDirectory}kavita20200910.log", new MockFileData("")
|
||||
{
|
||||
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - 10))
|
||||
});
|
||||
filesystem.AddFile($"{LogDirectory}kavita20200911.log", new MockFileData("")
|
||||
{
|
||||
CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - 11))
|
||||
});
|
||||
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub,
|
||||
ds);
|
||||
await cleanupService.CleanupLogs();
|
||||
Assert.True(filesystem.File.Exists($"{LogDirectory}kavita20200911.log"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// #region CleanupBookmarks
|
||||
//
|
||||
// [Fact]
|
||||
|
@ -895,6 +895,60 @@ public class ReaderServiceTests
|
||||
Assert.Equal("1", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume_2()
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("40", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("50", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("60", false, new List<MangaFile>(), 1),
|
||||
EntityFactory.CreateChapter("Some Special Title", true, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1997", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2001", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2005", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||
|
||||
// prevChapter should be id from ch.21 from volume 2001
|
||||
var prevChapter = await readerService.GetPrevChapterIdAsync(1, 4, 7, 1);
|
||||
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
|
||||
Assert.NotNull(actualChapter);
|
||||
Assert.Equal("21", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume()
|
||||
{
|
||||
|
@ -85,19 +85,19 @@ public class SeriesServiceTests
|
||||
|
||||
_context.ServerSetting.Update(setting);
|
||||
|
||||
var lib = new Library()
|
||||
{
|
||||
Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||
};
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007",
|
||||
Libraries = new List<Library>()
|
||||
{
|
||||
lib
|
||||
}
|
||||
});
|
||||
// var lib = new Library()
|
||||
// {
|
||||
// Name = "Manga", Folders = new List<FolderPath>() {new FolderPath() {Path = "C:/data/"}}
|
||||
// };
|
||||
//
|
||||
// _context.AppUser.Add(new AppUser()
|
||||
// {
|
||||
// UserName = "majora2007",
|
||||
// Libraries = new List<Library>()
|
||||
// {
|
||||
// lib
|
||||
// }
|
||||
// });
|
||||
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
@ -109,6 +109,7 @@ public class SeriesServiceTests
|
||||
_context.Genre.RemoveRange(_context.Genre.ToList());
|
||||
_context.CollectionTag.RemoveRange(_context.CollectionTag.ToList());
|
||||
_context.Person.RemoveRange(_context.Person.ToList());
|
||||
_context.Library.RemoveRange(_context.Library.ToList());
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
@ -135,33 +136,45 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
new AppUser()
|
||||
{
|
||||
EntityFactory.CreateChapter("Omake", true, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("Something SP02", true, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||
}),
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("Omake", true, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("Something SP02", true, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var expectedRanges = new[] {"Omake", "Something SP02"};
|
||||
@ -177,30 +190,41 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
new AppUser()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||
}),
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -220,28 +244,39 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
new AppUser()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
}),
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -261,28 +296,39 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
new AppUser()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
}),
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
|
||||
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -305,26 +351,38 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
new AppUser()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||
@ -339,26 +397,39 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
new AppUser()
|
||||
{
|
||||
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", true, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", false, new List<MangaFile>()),
|
||||
}),
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 1.epub", true, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("Ano Orokamono ni mo Kyakkou wo! - Volume 2.epub", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||
@ -379,36 +450,48 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Book,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
new AppUser()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1.2", new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1.2", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
EntityFactory.CreateChapter("0", false, new List<MangaFile>()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var detail = await _seriesService.GetSeriesDetail(1, 1);
|
||||
Assert.Equal("1", detail.Volumes.ElementAt(0).Name);
|
||||
Assert.Equal("1.2", detail.Volumes.ElementAt(1).Name);
|
||||
Assert.Equal("2", detail.Volumes.ElementAt(2).Name);
|
||||
Assert.Equal("Volume 1", detail.Volumes.ElementAt(0).Name);
|
||||
Assert.Equal("Volume 1.2", detail.Volumes.ElementAt(1).Name);
|
||||
Assert.Equal("Volume 2", detail.Volumes.ElementAt(2).Name);
|
||||
}
|
||||
|
||||
|
||||
@ -422,28 +505,34 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
new Volume()
|
||||
new AppUser()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Chapter()
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
Pages = 1
|
||||
}
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
@ -470,23 +559,28 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
new Volume()
|
||||
new AppUser()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Chapter()
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
Pages = 1
|
||||
}
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -536,23 +630,28 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
new Volume()
|
||||
new AppUser()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Chapter()
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
Pages = 1
|
||||
}
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -583,23 +682,28 @@ public class SeriesServiceTests
|
||||
{
|
||||
await ResetDb();
|
||||
|
||||
_context.Series.Add(new Series()
|
||||
_context.Library.Add(new Library()
|
||||
{
|
||||
Name = "Test",
|
||||
Library = new Library() {
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
},
|
||||
Volumes = new List<Volume>()
|
||||
AppUsers = new List<AppUser>()
|
||||
{
|
||||
new Volume()
|
||||
new AppUser()
|
||||
{
|
||||
Chapters = new List<Chapter>()
|
||||
UserName = "majora2007"
|
||||
}
|
||||
},
|
||||
Name = "Test LIb",
|
||||
Type = LibraryType.Manga,
|
||||
Series = new List<Series>()
|
||||
{
|
||||
new Series()
|
||||
{
|
||||
Name = "Test",
|
||||
Volumes = new List<Volume>()
|
||||
{
|
||||
new Chapter()
|
||||
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||
{
|
||||
Pages = 1
|
||||
}
|
||||
EntityFactory.CreateChapter("1", false, new List<MangaFile>(), 1),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -626,18 +730,6 @@ public class SeriesServiceTests
|
||||
|
||||
#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()
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ namespace API.Controllers;
|
||||
public class HealthController : BaseApiController
|
||||
{
|
||||
|
||||
[HttpGet()]
|
||||
[HttpGet]
|
||||
public ActionResult GetHealth()
|
||||
{
|
||||
return Ok("Ok");
|
||||
|
@ -223,6 +223,16 @@ public class SettingsController : BaseApiController
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalLogs && updateSettingsDto.TotalLogs + string.Empty != setting.Value)
|
||||
{
|
||||
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
|
||||
{
|
||||
return BadRequest("Total Logs must be between 1 and 30");
|
||||
}
|
||||
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.EmailServiceUrl && updateSettingsDto.EmailServiceUrl + string.Empty != setting.Value)
|
||||
{
|
||||
setting.Value = string.IsNullOrEmpty(updateSettingsDto.EmailServiceUrl) ? EmailService.DefaultApiUrl : updateSettingsDto.EmailServiceUrl;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using API.Services;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using API.Services;
|
||||
|
||||
namespace API.DTOs.Settings;
|
||||
|
||||
@ -50,7 +51,6 @@ public class ServerSettingDto
|
||||
/// If the Swagger UI Should be exposed. Does not require authentication, but does require a JWT.
|
||||
/// </summary>
|
||||
public bool EnableSwaggerUi { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of Backups before cleanup
|
||||
/// </summary>
|
||||
@ -60,4 +60,9 @@ public class ServerSettingDto
|
||||
/// If Kavita should watch the library folders and process changes
|
||||
/// </summary>
|
||||
public bool EnableFolderWatching { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Total number of days worth of logs to keep at a given time.
|
||||
/// </summary>
|
||||
/// <remarks>Value should be between 1 and 30</remarks>
|
||||
public int TotalLogs { get; set; }
|
||||
}
|
||||
|
15
API/DTOs/Stats/FileFormatDto.cs
Normal file
15
API/DTOs/Stats/FileFormatDto.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.DTOs.Stats;
|
||||
|
||||
public class FileFormatDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The extension with the ., in lowercase
|
||||
/// </summary>
|
||||
public string Extension { get; set; }
|
||||
/// <summary>
|
||||
/// Format of extension
|
||||
/// </summary>
|
||||
public MangaFormat Format { get; set; }
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using API.Entities.Enums;
|
||||
using System.Collections.Generic;
|
||||
using API.Entities.Enums;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace API.DTOs.Stats;
|
||||
|
||||
@ -118,4 +120,24 @@ public class ServerInfoDto
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.5.4</remarks>
|
||||
public bool UsingSeriesRelationships { get; set; }
|
||||
/// <summary>
|
||||
/// A list of background colors set on the instance
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.6.0</remarks>
|
||||
public IEnumerable<string> MangaReaderBackgroundColors { get; set; }
|
||||
/// <summary>
|
||||
/// A list of Page Split defaults being used on the instance
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.6.0</remarks>
|
||||
public IEnumerable<PageSplitOption> MangaReaderPageSplittingModes { get; set; }
|
||||
/// <summary>
|
||||
/// A list of Layout Mode defaults being used on the instance
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.6.0</remarks>
|
||||
public IEnumerable<LayoutMode> MangaReaderLayoutModes { get; set; }
|
||||
/// <summary>
|
||||
/// A list of file formats existing in the instance
|
||||
/// </summary>
|
||||
/// <remarks>Introduced in v0.6.0</remarks>
|
||||
public IEnumerable<FileFormatDto> FileFormats { get; set; }
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ public interface ILibraryRepository
|
||||
Task<IEnumerable<Library>> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None);
|
||||
Task<bool> DeleteLibrary(int libraryId);
|
||||
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
|
||||
Task<IEnumerable<int>> GetLibraryIdsForUserIdAsync(int userId);
|
||||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> libraryIds, LibraryIncludes includes = LibraryIncludes.None);
|
||||
Task<int> GetTotalFiles();
|
||||
@ -111,6 +112,11 @@ public class LibraryRepository : ILibraryRepository
|
||||
return await _context.SaveChangesAsync() > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This does not track
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId)
|
||||
{
|
||||
return await _context.Library
|
||||
@ -120,6 +126,14 @@ public class LibraryRepository : ILibraryRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<int>> GetLibraryIdsForUserIdAsync(int userId)
|
||||
{
|
||||
return await _context.Library
|
||||
.Where(l => l.AppUsers.Select(ap => ap.Id).Contains(userId))
|
||||
.Select(l => l.Id)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<LibraryType> GetLibraryTypeAsync(int libraryId)
|
||||
{
|
||||
return await _context.Library
|
||||
|
@ -103,6 +103,7 @@ public static class Seed
|
||||
new() {Key = ServerSettingKey.ConvertBookmarkToWebP, Value = "false"},
|
||||
new() {Key = ServerSettingKey.EnableSwaggerUi, Value = "false"},
|
||||
new() {Key = ServerSettingKey.TotalBackups, Value = "30"},
|
||||
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
}.ToArray());
|
||||
|
||||
|
@ -96,4 +96,9 @@ public enum ServerSettingKey
|
||||
/// </summary>
|
||||
[Description("EnableFolderWatching")]
|
||||
EnableFolderWatching = 17,
|
||||
/// <summary>
|
||||
/// Total number of days worth of logs to keep
|
||||
/// </summary>
|
||||
[Description("TotalLogs")]
|
||||
TotalLogs = 18,
|
||||
}
|
||||
|
@ -63,6 +63,9 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
|
||||
case ServerSettingKey.EnableFolderWatching:
|
||||
destination.EnableFolderWatching = bool.Parse(row.Value);
|
||||
break;
|
||||
case ServerSettingKey.TotalLogs:
|
||||
destination.TotalLogs = int.Parse(row.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ public static class LogLevelOptions
|
||||
.WriteTo.File(LogFile,
|
||||
shared: true,
|
||||
rollingInterval: RollingInterval.Day,
|
||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId} {Level}] {Message:lj}{NewLine}{Exception}");
|
||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {CorrelationId}] [{Level}] {Message:lj}{NewLine}{Exception}");
|
||||
}
|
||||
|
||||
public static void SwitchLogLevel(string level)
|
||||
@ -60,26 +60,31 @@ public static class LogLevelOptions
|
||||
{
|
||||
case "Debug":
|
||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Information;
|
||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||
break;
|
||||
case "Information":
|
||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
break;
|
||||
case "Trace":
|
||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Verbose;
|
||||
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Information;
|
||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Debug;
|
||||
break;
|
||||
case "Warning":
|
||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Warning;
|
||||
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
break;
|
||||
case "Critical":
|
||||
LogLevelSwitch.MinimumLevel = LogEventLevel.Fatal;
|
||||
MicrosoftLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
MicrosoftHostingLifetimeLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
AspNetCoreLogLevelSwitch.MinimumLevel = LogEventLevel.Error;
|
||||
break;
|
||||
|
@ -332,7 +332,7 @@ public class ArchiveService : IArchiveService
|
||||
{
|
||||
var filenameWithoutExtension = Path.GetFileNameWithoutExtension(name).ToLower();
|
||||
return !Tasks.Scanner.Parser.Parser.HasBlacklistedFolderInPath(fullName)
|
||||
&& fullName.Equals(ComicInfoFilename)
|
||||
&& (fullName.Equals(ComicInfoFilename) || (string.IsNullOrEmpty(fullName) && name.Equals(ComicInfoFilename)))
|
||||
&& !filenameWithoutExtension.StartsWith(Tasks.Scanner.Parser.Parser.MacOsMetadataFileStartsWith);
|
||||
}
|
||||
|
||||
|
@ -313,6 +313,7 @@ public class ReaderService : IReaderService
|
||||
if (chapterId > 0) return chapterId;
|
||||
}
|
||||
|
||||
var next = false;
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
if (volume.Number == currentVolume.Number && volume.Chapters.Count > 1)
|
||||
@ -322,10 +323,17 @@ public class ReaderService : IReaderService
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparer),
|
||||
currentChapter.Range, dto => dto.Range);
|
||||
if (chapterId > 0) return chapterId;
|
||||
|
||||
next = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (volume.Number != currentVolume.Number + 1) continue;
|
||||
if (volume.Number == currentVolume.Number)
|
||||
{
|
||||
next = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!next) continue;
|
||||
|
||||
// Handle Chapters within next Volume
|
||||
// ! When selecting the chapter for the next volume, we need to make sure a c0 comes before a c1+
|
||||
@ -389,6 +397,7 @@ public class ReaderService : IReaderService
|
||||
if (chapterId > 0) return chapterId;
|
||||
}
|
||||
|
||||
var next = false;
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
if (volume.Number == currentVolume.Number)
|
||||
@ -396,8 +405,10 @@ public class ReaderService : IReaderService
|
||||
var chapterId = GetNextChapterId(currentVolume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).Reverse(),
|
||||
currentChapter.Range, dto => dto.Range);
|
||||
if (chapterId > 0) return chapterId;
|
||||
next = true; // When the diff between volumes is more than 1, we need to explicitly tell that next volume is our use case
|
||||
continue;
|
||||
}
|
||||
if (volume.Number == currentVolume.Number - 1)
|
||||
if (next)
|
||||
{
|
||||
if (currentVolume.Number - 1 == 0) break; // If we have walked all the way to chapter volume, then we should break so logic outside can work
|
||||
var lastChapter = volume.Chapters.MaxBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
|
||||
|
@ -13,6 +13,7 @@ using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
@ -462,6 +463,9 @@ public class SeriesService : ISeriesService
|
||||
public async Task<SeriesDetailDto> GetSeriesDetail(int seriesId, int userId)
|
||||
{
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
var libraryIds = (await _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId));
|
||||
if (!libraryIds.Contains(series.LibraryId))
|
||||
throw new UnauthorizedAccessException("User does not have access to the library this series belongs to");
|
||||
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||
|
@ -53,6 +53,7 @@ public class TaskScheduler : ITaskScheduler
|
||||
public const string CleanupTaskId = "cleanup";
|
||||
public const string BackupTaskId = "backup";
|
||||
public const string ScanLibrariesTaskId = "scan-libraries";
|
||||
public const string ReportStatsTaskId = "report-stats";
|
||||
|
||||
private static readonly ImmutableArray<string> ScanTasks = ImmutableArray.Create("ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries");
|
||||
|
||||
@ -123,7 +124,7 @@ public class TaskScheduler : ITaskScheduler
|
||||
}
|
||||
|
||||
_logger.LogDebug("Scheduling stat collection daily");
|
||||
RecurringJob.AddOrUpdate("report-stats", () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local);
|
||||
}
|
||||
|
||||
public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false)
|
||||
@ -131,11 +132,14 @@ public class TaskScheduler : ITaskScheduler
|
||||
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, forceUpdate));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upon cancelling stat, we do report to the Stat service that we are no longer going to be reporting
|
||||
/// </summary>
|
||||
public void CancelStatsTasks()
|
||||
{
|
||||
_logger.LogDebug("Cancelling/Removing StatsTasks");
|
||||
|
||||
RecurringJob.RemoveIfExists("report-stats");
|
||||
_logger.LogDebug("Stopping Stat collection as user has opted out");
|
||||
RecurringJob.RemoveIfExists(ReportStatsTaskId);
|
||||
_statsService.SendCancellation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -25,6 +25,7 @@ public interface ICleanupService
|
||||
Task DeleteChapterCoverImages();
|
||||
Task DeleteTagCoverImages();
|
||||
Task CleanupBackups();
|
||||
Task CleanupLogs();
|
||||
void CleanupTemp();
|
||||
/// <summary>
|
||||
/// Responsible to remove Series from Want To Read when user's have fully read the series and the series has Publication Status of Completed or Cancelled.
|
||||
@ -76,6 +77,8 @@ public class CleanupService : ICleanupService
|
||||
await SendProgress(0.7F, "Cleaning deleted cover images");
|
||||
await DeleteTagCoverImages();
|
||||
await DeleteReadingListCoverImages();
|
||||
await SendProgress(0.8F, "Cleaning old logs");
|
||||
await CleanupLogs();
|
||||
await SendProgress(1F, "Cleanup finished");
|
||||
_logger.LogInformation("Cleanup finished");
|
||||
}
|
||||
@ -189,6 +192,29 @@ public class CleanupService : ICleanupService
|
||||
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
|
||||
}
|
||||
|
||||
public async Task CleanupLogs()
|
||||
{
|
||||
_logger.LogInformation("Performing cleanup of logs directory");
|
||||
var dayThreshold = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).TotalLogs;
|
||||
var deltaTime = DateTime.Today.Subtract(TimeSpan.FromDays(dayThreshold));
|
||||
var allLogs = _directoryService.GetFiles(_directoryService.LogDirectory).ToList();
|
||||
var expiredLogs = allLogs.Select(filename => _directoryService.FileSystem.FileInfo.FromFileName(filename))
|
||||
.Where(f => f.CreationTime < deltaTime)
|
||||
.ToList();
|
||||
|
||||
if (expiredLogs.Count == allLogs.Count)
|
||||
{
|
||||
_logger.LogInformation("All expired backups are older than {Threshold} days. Removing all but last backup", dayThreshold);
|
||||
var toDelete = expiredLogs.OrderBy(f => f.CreationTime).ToList();
|
||||
_directoryService.DeleteFiles(toDelete.Take(toDelete.Count - 1).Select(f => f.FullName));
|
||||
}
|
||||
else
|
||||
{
|
||||
_directoryService.DeleteFiles(expiredLogs.Select(f => f.FullName));
|
||||
}
|
||||
_logger.LogInformation("Finished cleanup of logs at {Time}", DateTime.Now);
|
||||
}
|
||||
|
||||
public void CleanupTemp()
|
||||
{
|
||||
_logger.LogInformation("Performing cleanup of Temp directory");
|
||||
|
@ -91,8 +91,11 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
/// This is just here to prevent GC from Disposing our watchers
|
||||
/// </summary>
|
||||
private readonly IList<FileSystemWatcher> _fileWatchers = new List<FileSystemWatcher>();
|
||||
private IList<string> _libraryFolders = new List<string>();
|
||||
|
||||
private static IList<string> _libraryFolders = new List<string>();
|
||||
/// <summary>
|
||||
/// The amount of time until the Schedule ScanFolder task should be executed
|
||||
/// </summary>
|
||||
/// <remarks>The Job will be enqueued instantly</remarks>
|
||||
private readonly TimeSpan _queueWaitTime;
|
||||
|
||||
|
||||
@ -109,7 +112,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
|
||||
public async Task StartWatching()
|
||||
{
|
||||
_logger.LogInformation("Starting file watchers");
|
||||
_logger.LogInformation("[LibraryWatcher] Starting file watchers");
|
||||
|
||||
_libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||
.SelectMany(l => l.Folders)
|
||||
@ -119,7 +122,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
.ToList();
|
||||
foreach (var libraryFolder in _libraryFolders)
|
||||
{
|
||||
_logger.LogDebug("Watching {FolderPath}", libraryFolder);
|
||||
_logger.LogDebug("[LibraryWatcher] Watching {FolderPath}", libraryFolder);
|
||||
var watcher = new FileSystemWatcher(libraryFolder);
|
||||
|
||||
watcher.Changed += OnChanged;
|
||||
@ -138,17 +141,19 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
|
||||
_watcherDictionary[libraryFolder].Add(watcher);
|
||||
}
|
||||
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", _fileWatchers.Count);
|
||||
}
|
||||
|
||||
public void StopWatching()
|
||||
{
|
||||
_logger.LogInformation("Stopping watching folders");
|
||||
_logger.LogInformation("[LibraryWatcher] Stopping watching folders");
|
||||
foreach (var fileSystemWatcher in _watcherDictionary.Values.SelectMany(watcher => watcher))
|
||||
{
|
||||
fileSystemWatcher.EnableRaisingEvents = false;
|
||||
fileSystemWatcher.Changed -= OnChanged;
|
||||
fileSystemWatcher.Created -= OnCreated;
|
||||
fileSystemWatcher.Deleted -= OnDeleted;
|
||||
fileSystemWatcher.Error -= OnError;
|
||||
fileSystemWatcher.Dispose();
|
||||
}
|
||||
_fileWatchers.Clear();
|
||||
@ -165,13 +170,13 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
{
|
||||
if (e.ChangeType != WatcherChangeTypes.Changed) return;
|
||||
_logger.LogDebug("[LibraryWatcher] Changed: {FullPath}, {Name}", e.FullPath, e.Name);
|
||||
ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name)));
|
||||
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name))));
|
||||
}
|
||||
|
||||
private void OnCreated(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Created: {FullPath}, {Name}", e.FullPath, e.Name);
|
||||
ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name));
|
||||
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, !_directoryService.FileSystem.File.Exists(e.Name)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -183,7 +188,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
var isDirectory = string.IsNullOrEmpty(_directoryService.FileSystem.Path.GetExtension(e.Name));
|
||||
if (!isDirectory) return;
|
||||
_logger.LogDebug("[LibraryWatcher] Deleted: {FullPath}, {Name}", e.FullPath, e.Name);
|
||||
ProcessChange(e.FullPath, true);
|
||||
BackgroundJob.Enqueue(() => ProcessChange(e.FullPath, true));
|
||||
}
|
||||
|
||||
|
||||
@ -198,35 +203,42 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
/// Processes the file or folder change. If the change is a file change and not from a supported extension, it will be ignored.
|
||||
/// </summary>
|
||||
/// <remarks>This will ignore image files that are added to the system. However, they may still trigger scans due to folder changes.</remarks>
|
||||
/// <remarks>This is public only because Hangfire will invoke it. Do not call external to this class.</remarks>
|
||||
/// <param name="filePath">File or folder that changed</param>
|
||||
/// <param name="isDirectoryChange">If the change is on a directory and not a file</param>
|
||||
private void ProcessChange(string filePath, bool isDirectoryChange = false)
|
||||
public void ProcessChange(string filePath, bool isDirectoryChange = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
_logger.LogDebug("[LibraryWatcher] Processing change of {FilePath}", filePath);
|
||||
try
|
||||
{
|
||||
// We need to check if directory or not
|
||||
// If not a directory change AND file is not an archive or book, ignore
|
||||
if (!isDirectoryChange &&
|
||||
!(Parser.Parser.IsArchive(filePath) || Parser.Parser.IsBook(filePath))) return;
|
||||
!(Parser.Parser.IsArchive(filePath) || Parser.Parser.IsBook(filePath)))
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Change from {FilePath} is not an archive or book, ignoring change", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
var parentDirectory = _directoryService.GetParentDirectoryName(filePath);
|
||||
if (string.IsNullOrEmpty(parentDirectory)) return;
|
||||
// var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||
// .SelectMany(l => l.Folders)
|
||||
// .Distinct()
|
||||
// .Select(Parser.Parser.NormalizePath)
|
||||
// .Where(_directoryService.Exists)
|
||||
// .ToList();
|
||||
|
||||
// We need to find the library this creation belongs to
|
||||
// Multiple libraries can point to the same base folder. In this case, we need use FirstOrDefault
|
||||
var libraryFolder = _libraryFolders.FirstOrDefault(f => parentDirectory.Contains(f));
|
||||
if (string.IsNullOrEmpty(libraryFolder)) return;
|
||||
var fullPath = GetFolder(filePath, _libraryFolders);
|
||||
if (string.IsNullOrEmpty(fullPath))
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Change from {FilePath} could not find root level folder, ignoring change", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
|
||||
if (!rootFolder.Any()) return;
|
||||
|
||||
// Select the first folder and join with library folder, this should give us the folder to scan.
|
||||
var fullPath =
|
||||
Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder.First()));
|
||||
// Check if this task has already enqueued or is being processed, before enquing
|
||||
|
||||
var alreadyScheduled =
|
||||
TaskScheduler.HasAlreadyEnqueuedTask(ScannerService.Name, "ScanFolder", new object[] {fullPath});
|
||||
_logger.LogDebug("{FullPath} already enqueued: {Value}", fullPath, alreadyScheduled);
|
||||
_logger.LogDebug("[LibraryWatcher] {FullPath} already enqueued: {Value}", fullPath, alreadyScheduled);
|
||||
if (!alreadyScheduled)
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Scheduling ScanFolder for {Folder}", fullPath);
|
||||
@ -242,9 +254,27 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
{
|
||||
_logger.LogError(ex, "[LibraryWatcher] An error occured when processing a watch event");
|
||||
}
|
||||
_logger.LogDebug("ProcessChange occured in {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
|
||||
_logger.LogDebug("[LibraryWatcher] ProcessChange ran in {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
private string GetFolder(string filePath, IList<string> libraryFolders)
|
||||
{
|
||||
var parentDirectory = _directoryService.GetParentDirectoryName(filePath);
|
||||
if (string.IsNullOrEmpty(parentDirectory))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (string.IsNullOrEmpty(parentDirectory)) return string.Empty;
|
||||
|
||||
// We need to find the library this creation belongs to
|
||||
// Multiple libraries can point to the same base folder. In this case, we need use FirstOrDefault
|
||||
var libraryFolder = libraryFolders.FirstOrDefault(f => parentDirectory.Contains(f));
|
||||
if (string.IsNullOrEmpty(libraryFolder)) return string.Empty;
|
||||
|
||||
var rootFolder = _directoryService.GetFoldersTillRoot(libraryFolder, filePath).ToList();
|
||||
if (!rootFolder.Any()) return string.Empty;
|
||||
|
||||
// Select the first folder and join with library folder, this should give us the folder to scan.
|
||||
return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder.First()));
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,8 @@ public class ProcessSeries : IProcessSeries
|
||||
{
|
||||
_logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName);
|
||||
|
||||
var firstParsedInfo = parsedInfos[0];
|
||||
// parsedInfos[0] is not the first volume or chapter. We need to find it using a ComicInfo check (as it uses firstParsedInfo for series sort)
|
||||
var firstParsedInfo = parsedInfos.FirstOrDefault(p => p.ComicInfo != null, parsedInfos[0]);
|
||||
|
||||
UpdateVolumes(series, parsedInfos);
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
@ -479,10 +480,10 @@ public class ProcessSeries : IProcessSeries
|
||||
var deletedVolumes = series.Volumes.Except(nonDeletedVolumes);
|
||||
foreach (var volume in deletedVolumes)
|
||||
{
|
||||
var file = volume.Chapters.FirstOrDefault()?.Files?.FirstOrDefault()?.FilePath ?? "";
|
||||
var file = volume.Chapters.FirstOrDefault()?.Files?.FirstOrDefault()?.FilePath ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(file) && _directoryService.FileSystem.File.Exists(file))
|
||||
{
|
||||
_logger.LogError(
|
||||
_logger.LogInformation(
|
||||
"[ScannerService] Volume cleanup code was trying to remove a volume with a file still existing on disk. File: {File}",
|
||||
file);
|
||||
}
|
||||
|
@ -458,7 +458,7 @@ public class ScannerService : IScannerService
|
||||
}
|
||||
|
||||
|
||||
var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles);
|
||||
var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles, forceUpdate);
|
||||
|
||||
|
||||
await Task.WhenAll(processTasks);
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
@ -21,6 +23,7 @@ public interface IStatsService
|
||||
{
|
||||
Task Send();
|
||||
Task<ServerInfoDto> GetServerInfo();
|
||||
Task SendCancellation();
|
||||
}
|
||||
public class StatsService : IStatsService
|
||||
{
|
||||
@ -127,6 +130,10 @@ public class StatsService : IStatsService
|
||||
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
||||
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
||||
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
||||
MangaReaderBackgroundColors = await AllMangaReaderBackgroundColors(),
|
||||
MangaReaderPageSplittingModes = await AllMangaReaderPageSplitting(),
|
||||
MangaReaderLayoutModes = await AllMangaReaderLayoutModes(),
|
||||
FileFormats = AllFormats(),
|
||||
};
|
||||
|
||||
var usersWithPref = (await _unitOfWork.UserRepository.GetAllUsersAsync(AppUserIncludes.UserPreferences)).ToList();
|
||||
@ -149,6 +156,39 @@ public class StatsService : IStatsService
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
public async Task SendCancellation()
|
||||
{
|
||||
_logger.LogInformation("Informing KavitaStats that this instance is no longer sending stats");
|
||||
var installId = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).InstallId;
|
||||
|
||||
var responseContent = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await (ApiUrl + "/api/v2/stats/opt-out?installId=" + installId)
|
||||
.WithHeader("Accept", "application/json")
|
||||
.WithHeader("User-Agent", "Kavita")
|
||||
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
|
||||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||
.WithHeader("Content-Type", "application/json")
|
||||
.WithTimeout(TimeSpan.FromSeconds(30))
|
||||
.PostAsync();
|
||||
|
||||
if (response.StatusCode != StatusCodes.Status200OK)
|
||||
{
|
||||
_logger.LogError("KavitaStats did not respond successfully. {Content}", response);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
_logger.LogError(e, "KavitaStats did not respond successfully. {Response}", responseContent);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "An error happened during the request to KavitaStats");
|
||||
}
|
||||
}
|
||||
|
||||
private Task<bool> GetIfUsingSeriesRelationship()
|
||||
{
|
||||
return _context.SeriesRelation.AnyAsync();
|
||||
@ -190,4 +230,35 @@ public class StatsService : IStatsService
|
||||
.SelectMany(v => v.Chapters)
|
||||
.Count());
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<string>> AllMangaReaderBackgroundColors()
|
||||
{
|
||||
return await _context.AppUserPreferences.Select(p => p.BackgroundColor).Distinct().ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<PageSplitOption>> AllMangaReaderPageSplitting()
|
||||
{
|
||||
return await _context.AppUserPreferences.Select(p => p.PageSplitOption).Distinct().ToListAsync();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<LayoutMode>> AllMangaReaderLayoutModes()
|
||||
{
|
||||
return await _context.AppUserPreferences.Select(p => p.LayoutMode).Distinct().ToListAsync();
|
||||
}
|
||||
|
||||
private IEnumerable<FileFormatDto> AllFormats()
|
||||
{
|
||||
var results = _context.MangaFile
|
||||
.AsNoTracking()
|
||||
.AsEnumerable()
|
||||
.Select(m => new FileFormatDto()
|
||||
{
|
||||
Format = m.Format,
|
||||
Extension = Path.GetExtension(m.FilePath)?.ToLowerInvariant()
|
||||
})
|
||||
.DistinctBy(f => f.Extension)
|
||||
.ToList();
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ EXPOSE 5000
|
||||
|
||||
WORKDIR /kavita
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=15s --start-period=30s --retries=3 CMD curl --fail http://localhost:5000 || exit 1
|
||||
HEALTHCHECK --interval=30s --timeout=15s --start-period=30s --retries=3 CMD curl --fail http://localhost:5000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT [ "/bin/bash" ]
|
||||
CMD ["/entrypoint.sh"]
|
||||
|
@ -11,12 +11,6 @@ public static class Configuration
|
||||
{
|
||||
public static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
|
||||
|
||||
public static string Branch
|
||||
{
|
||||
get => GetBranch(GetAppSettingFilename());
|
||||
set => SetBranch(GetAppSettingFilename(), value);
|
||||
}
|
||||
|
||||
public static int Port
|
||||
{
|
||||
get => GetPort(GetAppSettingFilename());
|
||||
@ -146,42 +140,4 @@ public static class Configuration
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string GetBranch(string filePath)
|
||||
{
|
||||
const string defaultBranch = "main";
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
|
||||
const string key = "Branch";
|
||||
|
||||
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
|
||||
{
|
||||
return tokenElement.GetString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Error reading app settings: " + ex.Message);
|
||||
}
|
||||
|
||||
return defaultBranch;
|
||||
}
|
||||
|
||||
private static void SetBranch(string filePath, string updatedBranch)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentBranch = GetBranch(filePath);
|
||||
var json = File.ReadAllText(filePath)
|
||||
.Replace("\"Branch\": " + currentBranch, "\"Branch\": " + updatedBranch);
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* Swallow Exception */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,5 +12,6 @@ export interface ServerSettings {
|
||||
convertBookmarkToWebP: boolean;
|
||||
enableSwaggerUi: boolean;
|
||||
totalBackups: number;
|
||||
totalLogs: number;
|
||||
enableFolderWatching: boolean;
|
||||
}
|
||||
|
@ -21,14 +21,14 @@
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="settings-port" class="form-label">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
||||
<span class="visually-hidden" id="settings-port-help">Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</span>
|
||||
<input id="settings-port" aria-describedby="settings-port-help" class="form-control" formControlName="port" type="number" step="1" min="1" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2">
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="backup-tasks" class="form-label">Days of Backups</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="backupTasksTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #backupTasksTooltip>The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</ng-template>
|
||||
<span class="visually-hidden" id="backup-tasks-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
|
||||
@ -45,8 +45,26 @@
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-12 pe-2">
|
||||
<label for="log-tasks" class="form-label">Days of Logs</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="logTasksTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #logTasksTooltip>The number of logs to maintain. Default is 30, minumum is 1, maximum is 30.</ng-template>
|
||||
<span class="visually-hidden" id="log-tasks-help">The number of backups to maintain. Default is 30, minumum is 1, maximum is 30.</span>
|
||||
<input id="log-tasks" aria-describedby="log-tasks-help" class="form-control" formControlName="totalLogs" type="number" step="1" min="1" max="30" onkeypress="return event.charCode >= 48 && event.charCode <= 57">
|
||||
<ng-container *ngIf="settingsForm.get('totalLogs')?.errors as errors">
|
||||
<p class="invalid-feedback" *ngIf="errors.min">
|
||||
You must have at least 1 log
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.max">
|
||||
You cannot have more than {{errors.max.max}} logs
|
||||
</p>
|
||||
<p class="invalid-feedback" *ngIf="errors.required">
|
||||
This field is required
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<div class="col-md-3 col-sm-12">
|
||||
<label for="logging-level-port" class="form-label">Logging Level</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="loggingLevelTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #loggingLevelTooltip>Use debug to help identify issues. Debug can eat up a lot of disk space.</ng-template>
|
||||
<span class="visually-hidden" id="logging-level-port-help">Port the server listens on.</span>
|
||||
|
@ -49,6 +49,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
||||
this.settingsForm.addControl('enableSwaggerUi', new FormControl(this.serverSettings.enableSwaggerUi, [Validators.required]));
|
||||
this.settingsForm.addControl('totalBackups', new FormControl(this.serverSettings.totalBackups, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||
this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||
this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
||||
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, []));
|
||||
});
|
||||
@ -67,6 +68,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||
this.settingsForm.get('emailServiceUrl')?.setValue(this.serverSettings.emailServiceUrl);
|
||||
this.settingsForm.get('enableSwaggerUi')?.setValue(this.serverSettings.enableSwaggerUi);
|
||||
this.settingsForm.get('totalBackups')?.setValue(this.serverSettings.totalBackups);
|
||||
this.settingsForm.get('totalLogs')?.setValue(this.serverSettings.totalLogs);
|
||||
this.settingsForm.get('enableFolderWatching')?.setValue(this.serverSettings.enableFolderWatching);
|
||||
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
||||
this.settingsForm.markAsPristine();
|
||||
|
@ -3,7 +3,7 @@ import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { catchError, forkJoin, of, Subject } from 'rxjs';
|
||||
import { take, takeUntil } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||
@ -511,7 +511,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
||||
}
|
||||
});
|
||||
|
||||
this.seriesService.getSeriesDetail(this.seriesId).subscribe(detail => {
|
||||
this.seriesService.getSeriesDetail(this.seriesId).pipe(catchError(err => {
|
||||
this.router.navigateByUrl('/libraries');
|
||||
return of(null);
|
||||
})).subscribe(detail => {
|
||||
if (detail == null) return;
|
||||
this.hasSpecials = detail.specials.length > 0;
|
||||
this.specials = detail.specials;
|
||||
|
||||
|
@ -210,7 +210,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
readerMode: parseInt(modelSettings.readerMode, 10),
|
||||
layoutMode: parseInt(modelSettings.layoutMode, 10),
|
||||
showScreenHints: modelSettings.showScreenHints,
|
||||
backgroundColor: modelSettings.backgroundColor,
|
||||
backgroundColor: this.user.preferences.backgroundColor,
|
||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
||||
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
||||
bookReaderFontSize: modelSettings.bookReaderFontSize,
|
||||
|
@ -3,24 +3,7 @@
|
||||
if [ ! -f "/kavita/config/appsettings.json" ]; then
|
||||
echo "Kavita configuration file does not exist, creating..."
|
||||
echo '{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data source=config//kavita.db"
|
||||
},
|
||||
"TokenKey": "super secret unguessable key",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Error",
|
||||
"Hangfire": "Information",
|
||||
"Microsoft.AspNetCore.Hosting.Internal.WebHost": "Information"
|
||||
},
|
||||
"File": {
|
||||
"Path": "config//logs/kavita.log",
|
||||
"Append": "True",
|
||||
"FileSizeLimitBytes": 26214400,
|
||||
"MaxRollingFiles": 2
|
||||
}
|
||||
},
|
||||
"Port": 5000
|
||||
}' >> /kavita/config/appsettings.json
|
||||
@ -28,4 +11,4 @@ fi
|
||||
|
||||
chmod +x Kavita
|
||||
|
||||
./Kavita
|
||||
./Kavita
|
||||
|
Loading…
x
Reference in New Issue
Block a user